diff --git a/tests/lapi/builtin_assert_test.lua b/tests/lapi/builtin_assert_test.lua new file mode 100644 index 0000000..3566a1b --- /dev/null +++ b/tests/lapi/builtin_assert_test.lua @@ -0,0 +1,27 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Synopsis: assert(v [, message]) +]] + +local luzer = require("luzer") +local test_lib = require("lib") +local MAX_INT = test_lib.MAX_INT + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local max_len = fdp:consume_integer(0, MAX_INT) + local message = fdp:consume_string(max_len) + local v = fdp:consume_boolean() + local ok, _ = pcall(assert, v, message) + assert(ok == v) +end + +local args = { + artifact_prefix = "builtin_assert_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_collectgarbage_test.lua b/tests/lapi/builtin_collectgarbage_test.lua new file mode 100644 index 0000000..2ed4f43 --- /dev/null +++ b/tests/lapi/builtin_collectgarbage_test.lua @@ -0,0 +1,60 @@ +--[=[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Synopsis: collectgarbage([opt [, arg]]) +]]=] + +local luzer = require("luzer") +local test_lib = require("lib") + +local unpack = unpack or table.unpack + +local gc_mode = { + "collect", + "count", + "restart", + "step", + "stop", +} + +if test_lib.lua_version() == "LuaJIT" then + table.insert(gc_mode, "setpause") + table.insert(gc_mode, "setstepmul") +else + table.insert(gc_mode, "isrunning") + table.insert(gc_mode, "incremental") + table.insert(gc_mode, "generational") +end + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local mode = fdp:oneof(gc_mode) + local MAX_INT = test_lib.MAX_INT + local arg = {} + if mode == "step" or + mode == "setpause" or + mode == "setstepmul" then + table.insert(arg, fdp:consume_integer(0, MAX_INT)) + end + -- This option can be followed by two numbers: the + -- garbage-collector minor multiplier and the major multiplier. + if mode == "generational" then + table.insert(arg, fdp:consume_integer(0, MAX_INT)) + end + -- This option can be followed by three numbers: the + -- garbage-collector pause, the step multiplier, and the step + -- size + if mode == "incremental" then + table.insert(arg, fdp:consume_integer(0, MAX_INT)) + end + collectgarbage(mode, unpack(arg)) +end + +local args = { + artifact_prefix = "builtin_collectgarbage_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_concat_test.lua b/tests/lapi/builtin_concat_test.lua new file mode 100644 index 0000000..7a37b1c --- /dev/null +++ b/tests/lapi/builtin_concat_test.lua @@ -0,0 +1,55 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +2.5.4 – Concatenation +https://www.lua.org/manual/5.1/manual.html + +Recording of __concat in GC64 mode, +https://github.com/LuaJIT/LuaJIT/issues/839 + +Bug: Unbalanced Stack After Hot Instruction error on table concatenation, +https://github.com/LuaJIT/LuaJIT/issues/690 + +LJ_GC64: Fix lua_concat(), +https://github.com/LuaJIT/LuaJIT/issues/881 + +Buffer overflow in string concatenation, +https://github.com/lua/lua/commit/5853c37a83ec66ccb45094f9aeac23dfdbcde671 + +Wrong assert when reporting concatenation errors +(manifests only when Lua is compiled in debug mode). +https://www.lua.org/bugs.html#5.2.2-3 + +Wrong error message in some concatenations, +https://www.lua.org/bugs.html#5.1.2-5 + +Concat metamethod converts numbers to strings, +https://www.lua.org/bugs.html#5.1.1-8 + +String concatenation may cause arithmetic overflow, leading to +a buffer overflow, +https://www.lua.org/bugs.html#5.0.2-1 +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local str = fdp:consume_string(test_lib.MAX_STR_LEN) + if str == nil then return -1 end + local str_chars = {} + str:gsub(".", function(c) table.insert(str_chars, c) end) + + local str_concat = "" + for _, c in ipairs(str_chars) do + str_concat = str_concat .. c + end + assert(str_concat == str) +end + +local args = { + artifact_prefix = "builtin_concat_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_dofile_test.lua b/tests/lapi/builtin_dofile_test.lua new file mode 100644 index 0000000..698de05 --- /dev/null +++ b/tests/lapi/builtin_dofile_test.lua @@ -0,0 +1,30 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Synopsis: dofile([filename]) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local function TestOneInput(buf) + local chunk_filename = os.tmpname() + local fh = io.open(chunk_filename, "w") + fh:write(buf) + fh:close() + pcall(dofile, chunk_filename) + os.remove(chunk_filename) +end + +local args = { + artifact_prefix = "builtin_dofile_", +} +-- lj_bcread.c:123: bcread_byte: buffer read overflow +if test_lib.lua_version() == "LuaJIT" then + args.only_ascii = 1 +end +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_dostring_test.lua b/tests/lapi/builtin_dostring_test.lua new file mode 100644 index 0000000..df44367 --- /dev/null +++ b/tests/lapi/builtin_dostring_test.lua @@ -0,0 +1,27 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html +]] + +local luzer = require("luzer") +local test_lib = require("lib") +local MAX_INT = test_lib.MAX_INT + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local max_len = fdp:consume_integer(0, MAX_INT) + local str = fdp:consume_string(max_len) + pcall(loadstring, str) +end + +local args = { + artifact_prefix = "builtin_dostring_", +} +-- lj_bcread.c:123: bcread_byte: buffer read overflow +if test_lib.lua_version() == "LuaJIT" then + args.only_ascii = 1 +end +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_error_test.lua b/tests/lapi/builtin_error_test.lua new file mode 100644 index 0000000..0332bed --- /dev/null +++ b/tests/lapi/builtin_error_test.lua @@ -0,0 +1,33 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Synopsis: error(message [, level]) +]] + +local luzer = require("luzer") +local test_lib = require("lib") +local MAX_INT = test_lib.MAX_INT + +local function escape_pattern(text) + return (text:gsub("[-.+%[%]()$^%%?*]", "%%%1")) +end + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local level = fdp:consume_integer(0, MAX_INT) + local message_len = fdp:consume_integer(0, MAX_INT) + local message = fdp:consume_string(message_len) + local ok, err = pcall(error, message, level) + assert(ok == false) + -- Escape message to avoid error "invalid pattern capture". + assert(err:match(escape_pattern(message)) == message) +end + +local args = { + artifact_prefix = "builtin_error_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_getfenv_test.lua b/tests/lapi/builtin_getfenv_test.lua new file mode 100644 index 0000000..8c909cc --- /dev/null +++ b/tests/lapi/builtin_getfenv_test.lua @@ -0,0 +1,46 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +debug.getfenv does not check whether it has an argument, +https://www.lua.org/bugs.html#5.1.4-5 + +module may change the environment of a C function, +https://www.lua.org/bugs.html#5.1.3-11 + +UBSan warning for too big/small getfenv/setfenv level, +https://github.com/LuaJIT/LuaJIT/issues/1329 + +Synopsis: getfenv([f]) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +-- Lua 5.2: Functions setfenv and getfenv were removed, because +-- of the changes in environments. +if test_lib.lua_current_version_ge_than(5, 2) then + print("Unsupported version.") + os.exit(0) +end + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local level = fdp:consume_integer(0, test_lib.MAX_INT) + local fenv, err = pcall(getfenv, level) + if err then + return -1 + end + local magic_str = fdp:consume_string(test_lib.MAX_STR_LEN) + fenv["magic"] = magic_str + setfenv(level, fenv) + assert(getfenv(level).magic == magic_str) +end + +local args = { + artifact_prefix = "builtin_getfenv_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_getmetatable_test.lua b/tests/lapi/builtin_getmetatable_test.lua new file mode 100644 index 0000000..12445ac --- /dev/null +++ b/tests/lapi/builtin_getmetatable_test.lua @@ -0,0 +1,33 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Incorrect recording of getmetatable() for IO handlers, +https://github.com/LuaJIT/LuaJIT/issues/1279 + +Synopsis: getmetatable(object) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local function TestOneInput(buf) + local tbl = {} + local fdp = luzer.FuzzedDataProvider(buf) + -- Build a random table and set a known key to it to check + -- it's presence in metatable. + local MAX_N = 100 + local count = fdp:consume_integer(0, MAX_N) + local mt = fdp:consume_integers(test_lib.MIN_INT, test_lib.MAX_INT, count) + setmetatable(tbl, mt) + local metatable = getmetatable(tbl) + assert(test_lib.arrays_equal(metatable, mt)) +end + +local args = { + artifact_prefix = "builtin_getmetatable_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_ipairs_test.lua b/tests/lapi/builtin_ipairs_test.lua new file mode 100644 index 0000000..3d8cc96 --- /dev/null +++ b/tests/lapi/builtin_ipairs_test.lua @@ -0,0 +1,28 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Synopsis: ipairs(t) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local MAX_N = 1000 + local count = fdp:consume_integer(0, MAX_N) + local tbl = fdp:consume_integers(test_lib.MIN_INT, test_lib.MAX_INT, count) + for i, v in ipairs(tbl) do + assert(type(i) == "number") + assert(type(v) == "number") + end +end + +local args = { + artifact_prefix = "builtin_ipairs_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_length_test.lua b/tests/lapi/builtin_length_test.lua new file mode 100644 index 0000000..335e6e5 --- /dev/null +++ b/tests/lapi/builtin_length_test.lua @@ -0,0 +1,34 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +2.5.5 – The Length Operator +https://www.lua.org/manual/5.1/manual.html + +Table length computation overflows for sequences larger than +2^31 elements, https://www.lua.org/bugs.html#5.3.4-3 +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local unpack = unpack or table.unpack + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + + local str = fdp:consume_string(test_lib.MAX_STR_LEN) + if str == nil then return -1 end + local str_chars = {} + str:gsub(".", function(c) table.insert(str_chars, c) end) + + assert(#str == select("#", unpack(str_chars))) + assert(#str == string.len(str)) + assert(#str == str:len()) + assert(#str_chars == str:len()) +end + +local args = { + artifact_prefix = "builtin_length_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_load_test.lua b/tests/lapi/builtin_load_test.lua new file mode 100644 index 0000000..a43ee3c --- /dev/null +++ b/tests/lapi/builtin_load_test.lua @@ -0,0 +1,56 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Loading a corrupted binary file can segfault, +https://github.com/lua/lua/commit/ab859fe59b464a038a45552921cb2b23892343af + +Long string can be collected while its contents is being read when +loading a binary file, +https://github.com/lua/lua/commit/6bc0f13505bf5d4f613d725fe008c79e72f83ddf + +Long brackets with a huge number of '=' overflow some internal buffer arithmetic. + +Chunk with too many lines may crash Lua, +https://www.lua.org/bugs.html#5.2.3-3 + +An emergency collection when handling an error while loading the +upvalues of a function can cause a segfault, +https://www.lua.org/bugs.html#5.4.0-3 +https://github.com/lua/lua/commit/422ce50d2e8856ed789d1359c673122dbb0088ea + +load and loadfile return wrong result when given an environment +for a binary chunk with no upvalues, +https://www.lua.org/bugs.html#5.2.1-4 + +When loading a file, Lua may call the reader function again after +it returned end of input, +https://www.lua.org/bugs.html#5.1.5-2 + +Maliciously crafted precompiled code can crash Lua, +https://www.lua.org/bugs.html#5.1.4-1 + +Synopsis: load(func [, chunkname]) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local mode = fdp:oneof({"t", "b"}) + -- LuaJIT ASSERT lj_bcread.c:123: bcread_byte: buffer read overflow. + if test_lib.lua_version() == "LuaJIT" then + mode = "t" + end + local func = load(buf, "luzer", mode) + pcall(func) +end + +local args = { + artifact_prefix = "builtin_load_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_loadfile_test.lua b/tests/lapi/builtin_loadfile_test.lua new file mode 100644 index 0000000..4616236 --- /dev/null +++ b/tests/lapi/builtin_loadfile_test.lua @@ -0,0 +1,38 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Synopsis: loadfile([filename]) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local function TestOneInput(buf) + local chunk_filename = os.tmpname() + local fh = io.open(chunk_filename, "w") + local chunk = buf + if test_lib.lua_version() == "LuaJIT" then + -- LuaJIT ASSERT lj_bcread.c:123: bcread_byte: buffer read overflow. + local pattern = "[^%z\1-\127][\128-\255][\192-\255][\128-\191]" + chunk = string.gsub(chunk, pattern, "") + end + fh:write(chunk) + fh:close() + + pcall(loadfile, chunk_filename) + + os.remove(chunk_filename) +end + +local args = { + artifact_prefix = "builtin_loadfile_", +} +-- lj_bcread.c:123: bcread_byte: buffer read overflow +if test_lib.lua_version() == "LuaJIT" then + args.only_ascii = 1 +end +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_loadstring_test.lua b/tests/lapi/builtin_loadstring_test.lua new file mode 100644 index 0000000..9dae7fd --- /dev/null +++ b/tests/lapi/builtin_loadstring_test.lua @@ -0,0 +1,50 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +PIL: "8 – Compilation, Execution, and Errors" +https://www.lua.org/pil/8.html + +Maliciously crafted precompiled code can crash Lua, +https://www.lua.org/bugs.html#5.1.3-5 + +Maliciously crafted precompiled code can blow the C stack, +https://www.lua.org/bugs.html#5.1.3-6 + +Code validator may reject (maliciously crafted) correct code, +https://www.lua.org/bugs.html#5.1.3-7 + +Maliciously crafted precompiled code can inject invalid boolean +values into Lua code, +https://www.lua.org/bugs.html#5.1.3-8 + +Synopsis: loadstring(string [, chunkname]) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local function TestOneInput(buf) + local chunk = buf + if test_lib.lua_version() == "LuaJIT" then + -- LuaJIT ASSERT lj_bcread.c:123: bcread_byte: buffer read overflow. + local pattern = "[^%z\1-\127][\128-\255][\192-\255][\128-\191]" + chunk = string.gsub(chunk, pattern, "") + end + local ok, res = pcall(loadstring, chunk) + if ok then + pcall(res) + end +end + +local args = { + artifact_prefix = "builtin_loadstring_", +} +-- lj_bcread.c:123: bcread_byte: buffer read overflow +if test_lib.lua_version() == "LuaJIT" then + args.only_ascii = 1 +end +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_next_test.lua b/tests/lapi/builtin_next_test.lua new file mode 100644 index 0000000..b5545fb --- /dev/null +++ b/tests/lapi/builtin_next_test.lua @@ -0,0 +1,44 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +recff_next does not take into account the situation when all next results are consumed, +https://github.com/LuaJIT/LuaJIT/issues/753 + +Key removed from a table during traversal may not be accepted by 'next', +https://github.com/lua/lua/commit/52c86797608f1bf927be5bab1e9b97b7d35bdf2c + +Synopsis: next(table [, index]) +]] + +local luzer = require("luzer") +local test_lib = require("lib") +local MAX_INT = test_lib.MAX_INT + +local ignored_msgs = { + "invalid key to 'next'", +} + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local MAX_N = 1000 + local count = fdp:consume_integer(0, MAX_N) + local tbl = fdp:consume_integers(test_lib.MIN_INT, test_lib.MAX_INT, count) + -- Use string keys to activate hash part of the table. + tbl.a = fdp:consume_string(test_lib.MAX_STR_LEN) + tbl.b = fdp:consume_string(test_lib.MAX_STR_LEN) + local index = fdp:consume_integer(0, MAX_INT) + + local err_handler = test_lib.err_handler(ignored_msgs) + local ok, res = xpcall(next, err_handler, tbl, index) + if not ok then return end + assert(type(res) == "number" or type(res) == "string") +end + +local args = { + artifact_prefix = "builtin_next_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_pairs_test.lua b/tests/lapi/builtin_pairs_test.lua new file mode 100644 index 0000000..00b4120 --- /dev/null +++ b/tests/lapi/builtin_pairs_test.lua @@ -0,0 +1,43 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Assertion failure 'rec_check_slots: slot type mismatch' during +pairs iteration, https://github.com/LuaJIT/LuaJIT/issues/796 + +infinite loop with pairs() and profiling, +https://github.com/LuaJIT/LuaJIT/issues/754 + +JIT On with pairs() leads to infinite(ish?) loop, +https://github.com/LuaJIT/LuaJIT/issues/744 + +Table iteration with `pairs()` does not result in the same order? +https://luajit.org/faq.html + +Synopsis: pairs(t) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local MAX_N = 1000 + local count = fdp:consume_integer(0, MAX_N) + local tbl = fdp:consume_integers(test_lib.MIN_INT, test_lib.MAX_INT, count) + -- Use string keys to activate hash part of the table. + tbl.a = fdp:consume_string(test_lib.MAX_STR_LEN) + tbl.b = fdp:consume_string(test_lib.MAX_STR_LEN) + for key, value in pairs(tbl) do + assert(key ~= nil) + assert(value ~= nil) + end +end + +local args = { + artifact_prefix = "builtin_pairs_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_rawequal_test.lua b/tests/lapi/builtin_rawequal_test.lua new file mode 100644 index 0000000..f7ef075 --- /dev/null +++ b/tests/lapi/builtin_rawequal_test.lua @@ -0,0 +1,71 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Synopsis: rawequal(v1, v2) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local MAX_INT = test_lib.MAX_INT +local MIN_INT = test_lib.MIN_INT +local MAX_STR_LEN = test_lib.MAX_STR_LEN + +-- "A metamethod only is selected when both objects being compared +-- have the same type and the same metamethod for the selected +-- operation.", https://www.lua.org/manual/5.1/manual.html#2.8. +local function random_pair_values(fdp) + local item_type = fdp:oneof({ + "boolean", + "integer", + "number", + "string", + "table", + }) + local item1, item2 + if item_type == "string" then + item1 = fdp:consume_string(MAX_STR_LEN) + item2 = fdp:consume_string(MAX_STR_LEN) + elseif item_type == "boolean" then + item1 = fdp:consume_boolean() + item2 = fdp:consume_boolean() + elseif item_type == "integer" then + item1 = fdp:consume_integer(MIN_INT, MAX_INT) + item2 = fdp:consume_integer(MIN_INT, MAX_INT) + elseif item_type == "number" then + item1 = fdp:consume_number(MIN_INT, MAX_INT) + item2 = fdp:consume_number(MIN_INT, MAX_INT) + elseif item_type == "table" then + item1 = fdp:consume_numbers(MIN_INT, MAX_INT, 10) + item2 = fdp:consume_numbers(MIN_INT, MAX_INT, 10) + else + assert("Unsupported type") + end + return item1, item2 +end + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local v1, v2 = random_pair_values(fdp) + local comp1 = rawequal(v1, v2) + assert(type(comp1 == "boolean")) + local mt = { + __eq = function(_op1, _op2) + assert(nil, "assertion is not reachable") + end, + } + debug.setmetatable(v1, mt) + debug.setmetatable(v2, mt) + local comp2 = rawequal(v1, v2) + assert(type(comp2 == "boolean")) + assert(comp1 == comp2) +end + +local args = { + artifact_prefix = "builtin_rawequal_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_rawget_test.lua b/tests/lapi/builtin_rawget_test.lua new file mode 100644 index 0000000..ef0f419 --- /dev/null +++ b/tests/lapi/builtin_rawget_test.lua @@ -0,0 +1,41 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +rawset and rawget do not ignore extra arguments, +https://www.lua.org/bugs.html#5.0.2-4 + +Synopsis: rawget(table, index) +]] + +local luzer = require("luzer") +local test_lib = require("lib") +local MAX_INT = test_lib.MAX_INT +local MIN_INT = test_lib.MIN_INT + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local MAX_N = 1000 + local count = fdp:consume_integer(0, MAX_N) + local tbl = fdp:consume_integers(MIN_INT, MAX_INT, count) + local value = fdp:consume_string(test_lib.MAX_STR_LEN) + tbl.field = value + local res = rawget(tbl, "field") + assert(res == value) + local mt = { + __index = function(_table, _key) + assert(nil, "assertion is not reachable") + end, + } + setmetatable(tbl, mt) + res = rawget(tbl, "field") + assert(res == value) +end + +local args = { + artifact_prefix = "builtin_rawget_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_rawset_test.lua b/tests/lapi/builtin_rawset_test.lua new file mode 100644 index 0000000..fdcdc11 --- /dev/null +++ b/tests/lapi/builtin_rawset_test.lua @@ -0,0 +1,51 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Memory-allocation error when resizing a table can leave it in an +inconsistent state, https://www.lua.org/bugs.html#5.3.4-7 + +rawset and rawget do not ignore extra arguments, +https://www.lua.org/bugs.html#5.0.2-4 + +Synopsis: rawset(table, index, value) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local MAX_INT = test_lib.MAX_INT +local MIN_INT = test_lib.MIN_INT +local MAX_STR_LEN = test_lib.MAX_STR_LEN + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local MAX_N = 1000 + local count = fdp:consume_integer(0, MAX_N) + local tbl = fdp:consume_integers(MIN_INT, MAX_INT, count) + local value = fdp:consume_string(MAX_STR_LEN) + + -- Set new value to a table without metatable. + local res = rawset(tbl, "field", value) + assert(res.field == value) + + local mt = { + __newindex = function(_table, _key, _value) + assert(nil, "assertion is not reachable") + end, + } + setmetatable(tbl, mt) + value = fdp:consume_string(MAX_STR_LEN) + + -- Set new value to a table *with* metatable. + res = rawset(tbl, "field", value) + assert(res.field == value) +end + +local args = { + artifact_prefix = "builtin_rawset_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_select_test.lua b/tests/lapi/builtin_select_test.lua new file mode 100644 index 0000000..2b72d9d --- /dev/null +++ b/tests/lapi/builtin_select_test.lua @@ -0,0 +1,79 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Synopsis: select(index, ...) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local unpack = unpack or table.unpack +local MAX_INT = test_lib.MAX_INT64 +local MIN_INT = test_lib.MIN_INT64 +local MAX_STR_LEN = test_lib.MAX_STR_LEN + +local function random_value(fdp) + local item_type = fdp:oneof({ + "boolean", + "integer", + "number", + "string", + "table", + }) + local item + if item_type == "string" then + item = fdp:consume_string(MAX_STR_LEN) + elseif item_type == "boolean" then + item = fdp:consume_boolean() + elseif item_type == "integer" then + item = fdp:consume_integer(MIN_INT, MAX_INT) + elseif item_type == "number" then + item = fdp:consume_number(MIN_INT, MAX_INT) + elseif item_type == "table" then + local MAX_N = 10 + item = fdp:consume_numbers(MIN_INT, MAX_INT, MAX_N) + else + assert("Unsupported type") + end + return item +end + +local ignored_msgs = { + "index out of range", +} + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + + local MAX_N = 1000 + local count = fdp:consume_integer(1, MAX_N) + local tbl = {} + for _ = 1, count do + table.insert(tbl, random_value(fdp)) + end + + local index = fdp:consume_boolean() and "#" or + fdp:consume_integer(0, MAX_INT) + if index == "#" then + assert(select(index, unpack(tbl)) == count) + end + local err_handler = test_lib.err_handler(ignored_msgs) + local ok, res = xpcall(select, err_handler, index, unpack(tbl)) + if not ok then return end + -- Don't want to test multiresults. + if index == "#" then + assert(res == count) + else + -- The value by the given index. + assert(res == tbl[index]) + end +end + +local args = { + artifact_prefix = "builtin_select_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_setmetatable_test.lua b/tests/lapi/builtin_setmetatable_test.lua new file mode 100644 index 0000000..593550b --- /dev/null +++ b/tests/lapi/builtin_setmetatable_test.lua @@ -0,0 +1,29 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +luaV_settable may invalidate a reference to a table and try to +reuse it, https://www.lua.org/bugs.html#5.1.4-4 + +Synopsis: setmetatable(table, metatable) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local MAX_N = 1000 + local count = fdp:consume_integer(0, MAX_N) + local tbl = fdp:consume_integers(test_lib.MIN_INT, test_lib.MAX_INT, count) + local res = setmetatable(tbl, {}) + assert(type(res) == "table") +end + +local args = { + artifact_prefix = "builtin_setmetatable_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_tonumber_test.lua b/tests/lapi/builtin_tonumber_test.lua new file mode 100644 index 0000000..e324f0e --- /dev/null +++ b/tests/lapi/builtin_tonumber_test.lua @@ -0,0 +1,63 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Fix binary number literal parsing, +https://www.freelists.org/post/luajit/Fractional-binary-number-literals +https://github.com/tarantool/luajit/commit/0ab421bc4077fa83039e653d4959fcdb2fa96ca6 + +tonumber produces wrong results for large exponents, +https://github.com/LuaJIT/LuaJIT/issues/788 + +tonumber("-0") returns 0, but it should be -0, +https://github.com/LuaJIT/LuaJIT/issues/528 + +Fix conversion for strings with null char, +https://github.com/LuaJIT/LuaJIT/pull/558 + +Synopsis: tonumber(e [, base]) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local MIN_BASE = 2 +local MAX_BASE = 36 + +-- 1. If one call `tonumber` with a specified base, then the first +-- argument must always be a string, otherwise one will get +-- surprises: +-- +-- $ luajit -e 'print(tonumber(0xa, 16))' +-- 16 +-- $ luajit -e 'print(tonumber("0xa", 16))' +-- 10 +-- +-- The Lua 5.1 documentation clearly states that: +-- If the argument is already a number or a string convertible to +-- a number, then tonumber returns this number; +-- +-- 2. The surprises do not end if the first argument is a string, +-- namely, take 2 in 16-digit exponential notation: +-- +-- $ luajit -e 'print(tonumber("0x1p1"), tonumber("0x1p1", 16))' +-- 2 nil +-- +-- If one explicitly specify the base, you get `nil`. This +-- behavior persists until the Lua 5.4. + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local base = fdp:consume_integer(MIN_BASE, MAX_BASE) + local e = fdp:consume_string(test_lib.MAX_STR_LEN) + local res = tonumber(e, base) + assert(type(res) == "number" or res == nil) +end + +local args = { + artifact_prefix = "builtin_tonumber_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_tostring_test.lua b/tests/lapi/builtin_tostring_test.lua new file mode 100644 index 0000000..bb010b8 --- /dev/null +++ b/tests/lapi/builtin_tostring_test.lua @@ -0,0 +1,34 @@ +--[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html + +Synopsis: tostring(e) +]] + +local luzer = require("luzer") +local test_lib = require("lib") + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + + -- Since Lua 5.5 conversion float to string ensures that, for + -- any float f, `tonumber(tostring(f)) == f`, but still + -- avoiding noise like 1.1 converting to "1.1000000000000001", + -- see [1]. + -- + -- 1. https://github.com/lua/lua/commit/1bf4b80f1ace8384eb9dd6f7f8b67256b3944a7a + local n1 = fdp:consume_number(test_lib.MIN_INT64, test_lib.MAX_INT64) + if test_lib.lua_version() == "LuaJIT" then + n1 = fdp:consume_integer(test_lib.MIN_INT, test_lib.MAX_INT) + end + local n2 = tonumber(tostring(n1)) + assert(n1 == n2) +end + +local args = { + artifact_prefix = "builtin_tostring_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/builtin_unpack_test.lua b/tests/lapi/builtin_unpack_test.lua new file mode 100644 index 0000000..90a9cbf --- /dev/null +++ b/tests/lapi/builtin_unpack_test.lua @@ -0,0 +1,74 @@ +--[=[[ +SPDX-License-Identifier: ISC +Copyright (c) 2023-2025, Sergey Bronnikov. + +5.1 – Basic Functions +https://www.lua.org/manual/5.1/manual.html +6.5 – Table Manipulation, +https://www.lua.org/manual/5.2/manual.html#6.5 + +Compiler can optimize away overflow check in table.unpack, +https://www.lua.org/bugs.html#5.2.3-1 + +Returns the type of its only argument, coded as a string. +The possible results of this function are "nil" +(a string, not the value nil), "number", "string", +"boolean", "table", "function", "thread", and "userdata". + +lua_checkstack may have arithmetic overflow for large 'size', +https://www.lua.org/bugs.html#5.1.3-3 + +unpack with maximum indices may crash due to arithmetic overflow, +https://www.lua.org/bugs.html#5.1.3-4 + +Fix overflow check in unpack(), +https://github.com/LuaJIT/LuaJIT/pull/574 + +Synopsis: unpack(list [, i [, j]]) +]]=] + +local luzer = require("luzer") +local test_lib = require("lib") + +local unpack = unpack or table.unpack +local MAX_INT = test_lib.MAX_INT64 +local MIN_INT = test_lib.MAX_INT64 + +local ignored_msgs = { + -- `i` and `j` cannot be bigger than INT_MAX, otherwise an + -- error "too many results to unpack" is raised. + "too many results to unpack", +} + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + + local str = fdp:consume_string(test_lib.MAX_STR_LEN) + local str_chars = {} + str:gsub(".", function(c) + local with_key = fdp:consume_boolean() + if with_key then + local idx = fdp:consume_integer(MIN_INT, MAX_INT) + str_chars[idx] = c + else + table.insert(str_chars, c) + end + end) + + -- By default, `i` is 1 and `j` is #list. + local default_indices = fdp:consume_boolean() + if default_indices then + unpack(str_chars) + end + + local i = fdp:consume_integer(MIN_INT, MAX_INT) + local j = fdp:consume_integer(MIN_INT, MAX_INT) + local err_handler = test_lib.err_handler(ignored_msgs) + local ok, _ = xpcall(unpack, err_handler, str_chars, i, j) + if not ok then return end +end + +local args = { + artifact_prefix = "builtin_unpack_", +} +luzer.Fuzz(TestOneInput, nil, args) diff --git a/tests/lapi/debug_torture_test.lua b/tests/lapi/debug_torture_test.lua index a9c59a9..1b0b864 100644 --- a/tests/lapi/debug_torture_test.lua +++ b/tests/lapi/debug_torture_test.lua @@ -171,7 +171,8 @@ local function TestOneInput(buf) local n_hook_mask = fdp:consume_integer(0, #hook_mask) local mask = {} for _ = 0, n_hook_mask do - table.insert(mask, fdp:oneof(hook_mask)) + local m = fdp:oneof(hook_mask) + table.insert(mask, m) end -- Turn on the hook. diff --git a/tests/lapi/os_time_test.lua b/tests/lapi/os_time_test.lua index cc6e742..981ffed 100644 --- a/tests/lapi/os_time_test.lua +++ b/tests/lapi/os_time_test.lua @@ -32,7 +32,11 @@ local function TestOneInput(buf) local err_handler = test_lib.err_handler(ignored_msgs) local ok, res = xpcall(os.time, err_handler, time) if not ok then return end - assert(type(res) == "number" or type(res) == "table") + io.stderr:write(type(res) .. "\n") + assert(type(res) == "number" or + type(res) == "table" or + -- Undocumented. + res == nil) end local args = { diff --git a/tests/lapi/string_pack_test.lua b/tests/lapi/string_pack_test.lua index 3ea30af..37ab7aa 100644 --- a/tests/lapi/string_pack_test.lua +++ b/tests/lapi/string_pack_test.lua @@ -17,6 +17,10 @@ if not test_lib.lua_current_version_ge_than(5, 3) then os.exit(0) end +local ignored_msgs = { + "invalid format option", +} + local function TestOneInput(buf, _size) local fdp = luzer.FuzzedDataProvider(buf) os.setlocale(test_lib.random_locale(fdp), "all") @@ -26,7 +30,10 @@ local function TestOneInput(buf, _size) end local n = fdp:consume_integer(1, test_lib.MAX_INT) local values = fdp:consume_strings(test_lib.MAX_STR_LEN, n) - string.pack(fmt_str, table.unpack(values)) + local err_handler = test_lib.err_handler(ignored_msgs) + local ok, _ = xpcall(string.pack, err_handler, fmt_str, + table.unpack(values)) + if not ok then return end end local args = { diff --git a/tests/lapi/utf8_offset_test.lua b/tests/lapi/utf8_offset_test.lua index 79c6623..d87a370 100644 --- a/tests/lapi/utf8_offset_test.lua +++ b/tests/lapi/utf8_offset_test.lua @@ -19,6 +19,10 @@ if test_lib.lua_current_version_lt_than(5, 3) then os.exit() end +local ignored_msgs = { + "initial position is a continuation byte", +} + local function TestOneInput(buf) local fdp = luzer.FuzzedDataProvider(buf) local max_len = fdp:consume_integer(0, MAX_INT) @@ -26,7 +30,9 @@ local function TestOneInput(buf) local n = fdp:consume_integer(MIN_INT, MAX_INT) local i = fdp:consume_integer(1, MAX_INT) os.setlocale(test_lib.random_locale(fdp), "all") - utf8.offset(s, n, i) + local err_handler = test_lib.err_handler(ignored_msgs) + local ok, _ = xpcall(utf8.offset, err_handler, s, n, i) + if not ok then return end end local args = {