Skip to content

Commit 22b310e

Browse files
committed
tests/lapi: add string tests
The patch adds a fuzzing tests for Lua string functions and two helpers: `random_locale()`.
1 parent c3c3f05 commit 22b310e

19 files changed

+660
-0
lines changed

cmake/BuildLua.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ macro(build_lua LUA_VERSION)
6464
set(CFLAGS "${CFLAGS} -DLUA_USE_DLOPEN")
6565
endif()
6666

67+
# `io.popen()` is not supported by default, it is enabled
68+
# by `LUA_USE_POSIX` flag. Required by a function `random_locale()`.
69+
set(CFLAGS "${CFLAGS} -DLUA_USE_POSIX")
70+
6771
include(ExternalProject)
6872

6973
set(LUA_LIBRARY ${PROJECT_BINARY_DIR}/lua-${LUA_VERSION}/source/liblua.a)

tests/lapi/lib.lua

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,24 @@ local function bitwise_op(op_name)
5656
end
5757
end
5858

59+
local function random_locale(fdp)
60+
local locales = {}
61+
local locale_it = io.popen("locale -a"):read("*a"):gmatch("([^\n]*)\n?")
62+
for locale in locale_it do
63+
table.insert(locales, locale)
64+
end
65+
66+
return fdp:oneof(locales)
67+
end
68+
5969
return {
6070
lua_version = lua_version,
6171
bitwise_op = bitwise_op,
6272
MAX_INT64 = MAX_INT64,
6373
MIN_INT64 = MIN_INT64,
6474
MAX_INT = MAX_INT,
6575
MIN_INT = MIN_INT,
76+
77+
-- FDP.
78+
random_locale = random_locale,
6679
}

tests/lapi/string_byte_test.lua

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
6.4 – String Manipulation
6+
https://www.lua.org/manual/5.3/manual.html#6.4
7+
8+
string.byte gets confused with some out-of-range negative indices,
9+
https://www.lua.org/bugs.html#5.1.3-9
10+
]]
11+
12+
-- Synopsis: string.byte(s [, i [, j]])
13+
14+
local luzer = require("luzer")
15+
local test_lib = require("lib")
16+
17+
local function TestOneInput(buf, _size)
18+
local fdp = luzer.FuzzedDataProvider(buf)
19+
os.setlocale(test_lib.random_locale(fdp), "all")
20+
local max_len = fdp:consume_integer(0, test_lib.MAX_INT)
21+
local str = fdp:consume_string(max_len)
22+
local i = fdp:consume_integer(0, test_lib.MAX_INT)
23+
local j = fdp:consume_integer(0, test_lib.MAX_INT)
24+
assert(string.byte(str, i, j) == str:byte(i, j))
25+
local char_code = string.byte(str, i, j)
26+
if char_code then
27+
assert(type(char_code) == "number")
28+
local byte = string.char(char_code)
29+
assert(byte)
30+
assert(byte == str)
31+
end
32+
end
33+
34+
local args = {
35+
artifact_prefix = "string_byte_",
36+
}
37+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_char_test.lua

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
6.4 – String Manipulation
6+
https://www.lua.org/manual/5.3/manual.html#6.4
7+
8+
string.char bug,
9+
https://github.com/LuaJIT/LuaJIT/issues/375
10+
11+
Fix string.char() recording with no arguments,
12+
https://github.com/LuaJIT/LuaJIT/commit/dfa692b7
13+
14+
Synopsis: string.char(...)
15+
]]
16+
17+
local luzer = require("luzer")
18+
local test_lib = require("lib")
19+
20+
local unpack = unpack or table.unpack
21+
22+
local function TestOneInput(buf, _size)
23+
local fdp = luzer.FuzzedDataProvider(buf)
24+
os.setlocale(test_lib.random_locale(fdp), "all")
25+
-- `n` must be less than UINT_MAX and there are at least extra
26+
-- free stack slots in the stack, otherwise an error
27+
-- "too many results to unpack" is raised, see <ltablib.c>.
28+
local MAX_CHARS_NUM = 1024
29+
local n = fdp:consume_integer(1, MAX_CHARS_NUM)
30+
local CHAR_MAX = 255
31+
local chs = fdp:consume_integers(0, CHAR_MAX, n)
32+
local str = string.char(unpack(chs))
33+
-- Returns a string with length equal to the number of
34+
-- arguments.
35+
assert(#str == n)
36+
end
37+
38+
local args = {
39+
artifact_prefix = "string_char_",
40+
}
41+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_dump_test.lua

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
6.4 – String Manipulation
6+
https://www.lua.org/manual/5.3/manual.html#6.4
7+
8+
string.dump(table.foreach) will trigger an assert,
9+
https://github.com/LuaJIT/LuaJIT/issues/1038
10+
11+
An emergency collection when handling an error while loading the upvalues of a function can cause a segfault,
12+
https://github.com/lua/lua/commit/422ce50d2e8856ed789d1359c673122dbb0088ea
13+
14+
Synopsis: string.dump(function [, strip])
15+
]]
16+
17+
local luzer = require("luzer")
18+
local test_lib = require("lib")
19+
20+
local function TestOneInput(buf, _size)
21+
local fdp = luzer.FuzzedDataProvider(buf)
22+
os.setlocale(test_lib.random_locale(fdp), "all")
23+
local max_len = fdp:consume_string(1, test_lib.MAX_INT)
24+
local str = fdp:consume_string(1, max_len)
25+
local strip = fdp:consume_boolean()
26+
local ok, func = pcall(loadstring, str)
27+
if not ok or func == nil then
28+
return
29+
end
30+
local res = string.dump(func, strip)
31+
assert(#res ~= 0)
32+
end
33+
34+
local args = {
35+
artifact_prefix = "string_dump_",
36+
}
37+
-- LuaJIT ASSERT lj_bcread.c:123: bcread_byte: buffer read overflow.
38+
if test_lib.lua_version() == "LuaJIT" then
39+
args["only_ascii"] = 1
40+
end
41+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_find_test.lua

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
--[=[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
6.4 – String Manipulation
6+
https://www.lua.org/manual/5.3/manual.html#6.4
7+
8+
Bug in "Don't use STRREF for pointer diff in string.find().",
9+
https://github.com/LuaJIT/LuaJIT/issues/540
10+
11+
Some patterns can overflow the C stack, due to recursion,
12+
https://www.lua.org/bugs.html#5.2.1-1
13+
14+
Properly fix pointer diff in string.find(),
15+
https://github.com/LuaJIT/LuaJIT/commit/0bee44c9
16+
17+
Synopsis: string.find(s, pattern [, init [, plain]])
18+
]]=]
19+
20+
local luzer = require("luzer")
21+
local test_lib = require("lib")
22+
23+
local function TestOneInput(buf, _size)
24+
local fdp = luzer.FuzzedDataProvider(buf)
25+
os.setlocale(test_lib.random_locale(fdp), "all")
26+
local max_len = fdp:consume_integer(0, test_lib.MAX_INT)
27+
local str = fdp:consume_string(max_len)
28+
local pattern = fdp:consume_string(max_len)
29+
local init = fdp:consume_integer(0, test_lib.MAX_INT)
30+
local plain = fdp:consume_boolean()
31+
-- Avoid errors like "malformed pattern (missing ']')".
32+
local ok, _ = pcall(string.find, str, pattern, init, plain)
33+
if not ok then
34+
return
35+
end
36+
local begin_pos, end_pos = string.find(str, pattern, init, plain)
37+
-- `string.format()` returns two numbers or "fail".
38+
assert((type(begin_pos) == "number" and type(end_pos) == "number") or
39+
(begin_pos == nil or end_pos == nil) or
40+
begin_pos == "fail")
41+
-- `string.format()` and `string:format()` is the same.
42+
assert(string.find(str, pattern, init, plain) ==
43+
str:find(pattern, init, plain))
44+
end
45+
46+
local args = {
47+
artifact_prefix = "string_find_",
48+
}
49+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_format_test.lua

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
--[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
6.4 – String Manipulation
6+
https://www.lua.org/manual/5.3/manual.html#6.4
7+
8+
stack-buffer-overflow in lj_strfmt_wfnum,
9+
https://github.com/LuaJIT/LuaJIT/issues/1149
10+
string.format("%7g",0x1.144399609d407p+401)
11+
12+
string.format %c bug,
13+
https://github.com/LuaJIT/LuaJIT/issues/378
14+
15+
string.format doesn't take current locale decimal separator into account,
16+
https://github.com/LuaJIT/LuaJIT/issues/673
17+
18+
string.format("%f") can cause a buffer overflow (only when
19+
'lua_Number' is long double!),
20+
https://www.lua.org/bugs.html#5.3.0-1
21+
22+
string.format may get buffer as an argument when there are missing
23+
arguments and format string is too long,
24+
https://www.lua.org/bugs.html#5.1.4-7
25+
26+
string.format("%") may read past the string,
27+
https://www.lua.org/bugs.html#5.1.1-3
28+
29+
Option '%q' in string.formatE does not handle '\r' correctly,
30+
https://www.lua.org/bugs.html#5.1-4
31+
32+
FFI: Support FFI numbers in string.format() and buf:putf(),
33+
https://github.com/LuaJIT/LuaJIT/commit/1b7171c3
34+
35+
[0014] CRASH detected in lj_ir_kgc due to a fault at or
36+
near 0x00007ff7f3274008 leading to SIGSEGV,
37+
https://github.com/LuaJIT/LuaJIT/issues/1203
38+
39+
Synopsis: string.format(formatstring, ...)
40+
]]
41+
42+
43+
local luzer = require("luzer")
44+
local test_lib = require("lib")
45+
46+
local specifiers = {
47+
"a",
48+
"A",
49+
"c",
50+
"d",
51+
"e",
52+
"E",
53+
"f",
54+
"g",
55+
"G",
56+
"i",
57+
"o",
58+
"p",
59+
"q",
60+
"s",
61+
"u",
62+
"x",
63+
"X",
64+
}
65+
66+
local function TestOneInput(buf, _size)
67+
local fdp = luzer.FuzzedDataProvider(buf)
68+
local spec = fdp:oneof(specifiers)
69+
local format_string = ("%%%s"):format(spec)
70+
local max_len = fdp:consume_integer(1, 10000)
71+
local str = fdp:consume_string(1, max_len)
72+
73+
os.setlocale(test_lib.random_locale(fdp), "all")
74+
local ok, res = pcall(string.format, format_string, str)
75+
assert(type(res) == "string")
76+
if ok then
77+
assert((format_string):format(str) == string.format(format_string, str))
78+
end
79+
end
80+
81+
local args = {
82+
artifact_prefix = "string_format_",
83+
}
84+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_gmatch_test.lua

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
6.4 – String Manipulation
6+
https://www.lua.org/manual/5.3/manual.html#6.4
7+
8+
GC64: string.gmatch crash,
9+
https://github.com/LuaJIT/LuaJIT/issues/300
10+
11+
gmatch iterator fails when called from a coroutine different from
12+
the one that created it,
13+
https://www.lua.org/bugs.html#5.3.2-3
14+
15+
Synopsis: string.gmatch(s, pattern [, init])
16+
]]
17+
18+
local luzer = require("luzer")
19+
local test_lib = require("lib")
20+
21+
local function TestOneInput(buf, _size)
22+
local fdp = luzer.FuzzedDataProvider(buf)
23+
os.setlocale(test_lib.random_locale(fdp), "all")
24+
local len = fdp:consume_integer(0, test_lib.MAX_INT)
25+
local s = fdp:consume_string(len)
26+
local pattern = fdp:consume_string(len)
27+
local init = fdp:consume_integer(0, test_lib.MAX_INT)
28+
string.gmatch(s, pattern, init)
29+
end
30+
31+
local args = {
32+
artifact_prefix = "string_gmatch_",
33+
}
34+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_gsub_test.lua

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
6.4 – String Manipulation
6+
https://www.lua.org/manual/5.3/manual.html#6.4
7+
8+
Performance issue for reg expression with "$",
9+
https://github.com/LuaJIT/LuaJIT/issues/118
10+
11+
LuaJIT's gsub does not work with zero bytes in the pattern string,
12+
https://github.com/LuaJIT/LuaJIT/issues/860
13+
14+
gsub may go wild when wrongly called without its third argument
15+
and with a large subject,
16+
https://www.lua.org/bugs.html#5.1.2-9
17+
18+
Synopsis: string.gsub(s, pattern, repl [, n])
19+
]]
20+
21+
local luzer = require("luzer")
22+
local test_lib = require("lib")
23+
24+
local function TestOneInput(buf, _size)
25+
local fdp = luzer.FuzzedDataProvider(buf)
26+
local max_len = fdp:consume_string(0, test_lib.MAX_INT)
27+
local str = fdp:consume_string(0, max_len)
28+
local pattern = fdp:consume_string(0, max_len)
29+
local repl = fdp:consume_string(0, max_len)
30+
local n = fdp:consume_integer(0, test_lib.MAX_INT)
31+
32+
os.setlocale(test_lib.random_locale(fdp), "all")
33+
-- Avoid errors like "malformed pattern (missing ']')".
34+
local ok, res = pcall(string.gsub, str, pattern, repl, n)
35+
if ok then
36+
assert(type(res) == "string")
37+
end
38+
end
39+
40+
local args = {
41+
artifact_prefix = "string_gsub_",
42+
}
43+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_len_test.lua

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
6.4 – String Manipulation
6+
https://www.lua.org/manual/5.3/manual.html#6.4
7+
8+
Synopsis: string.len(s)
9+
]]
10+
11+
local luzer = require("luzer")
12+
local test_lib = require("lib")
13+
14+
local function TestOneInput(buf, _size)
15+
local fdp = luzer.FuzzedDataProvider(buf)
16+
os.setlocale(test_lib.random_locale(fdp), "all")
17+
local str_len = fdp:consume_integer(0, test_lib.MAX_INT)
18+
local str = fdp:consume_string(str_len)
19+
assert(string.len(str) == #str)
20+
end
21+
22+
local args = {
23+
artifact_prefix = "string_len_",
24+
}
25+
luzer.Fuzz(TestOneInput, nil, args)

0 commit comments

Comments
 (0)