Skip to content

Commit 0508724

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 2974b65 commit 0508724

19 files changed

+666
-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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ local MIN_INT64 = math.mininteger or -0x8000000000000000
5555
local MAX_INT = 0x7fffffff
5656
local MIN_INT = -0x80000000
5757

58+
local MAX_STR_LEN = 4096
59+
5860
local function bitwise_op(op_name)
5961
return function(...)
6062
local n = select("#", ...)
@@ -84,6 +86,16 @@ local function approx_equal(a, b, epsilon)
8486
return abs(a - b) <= ((abs(a) < abs(b) and abs(b) or abs(a)) * epsilon)
8587
end
8688

89+
local function random_locale(fdp)
90+
local locales = {}
91+
local locale_it = io.popen("locale -a"):read("*a"):gmatch("([^\n]*)\n?")
92+
for locale in locale_it do
93+
table.insert(locales, locale)
94+
end
95+
96+
return fdp:oneof(locales)
97+
end
98+
8799
return {
88100
approx_equal = approx_equal,
89101
bitwise_op = bitwise_op,
@@ -95,4 +107,8 @@ return {
95107
MIN_INT64 = MIN_INT64,
96108
MAX_INT = MAX_INT,
97109
MIN_INT = MIN_INT,
110+
MAX_STR_LEN = MAX_STR_LEN,
111+
112+
-- FDP.
113+
random_locale = random_locale,
98114
}

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 str = fdp:consume_string(test_lib.MAX_STR_LEN)
21+
local i = fdp:consume_integer(0, test_lib.MAX_INT)
22+
local j = fdp:consume_integer(0, test_lib.MAX_INT)
23+
-- `string.byte()` is the same as `str:byte()`.
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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 str = fdp:consume_string(test_lib.MAX_STR_LEN)
24+
local strip = fdp:consume_boolean()
25+
local ok, func = pcall(loadstring, str)
26+
if not ok or func == nil then
27+
return
28+
end
29+
local res = string.dump(func, strip)
30+
assert(#res ~= 0)
31+
end
32+
33+
local args = {
34+
artifact_prefix = "string_dump_",
35+
}
36+
-- LuaJIT ASSERT lj_bcread.c:123: bcread_byte: buffer read overflow.
37+
if test_lib.lua_version() == "LuaJIT" then
38+
args["only_ascii"] = 1
39+
end
40+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_find_test.lua

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 str = fdp:consume_string(test_lib.MAX_STR_LEN)
27+
local pattern = fdp:consume_string(test_lib.MAX_STR_LEN)
28+
local init = fdp:consume_integer(0, test_lib.MAX_INT)
29+
local plain = fdp:consume_boolean()
30+
-- Avoid errors like "malformed pattern (missing ']')".
31+
local ok, _ = pcall(string.find, str, pattern, init, plain)
32+
if not ok then
33+
return
34+
end
35+
local begin_pos, end_pos = string.find(str, pattern, init, plain)
36+
-- `string.format()` returns two numbers or "fail".
37+
assert((type(begin_pos) == "number" and type(end_pos) == "number") or
38+
(begin_pos == nil or end_pos == nil) or
39+
begin_pos == "fail")
40+
-- `string.format()` and `string:format()` is the same.
41+
assert(string.find(str, pattern, init, plain) ==
42+
str:find(pattern, init, plain))
43+
end
44+
45+
local args = {
46+
artifact_prefix = "string_find_",
47+
}
48+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_format_test.lua

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 str = fdp:consume_string(test_lib.MAX_STR_LEN)
71+
72+
os.setlocale(test_lib.random_locale(fdp), "all")
73+
local ok, res = pcall(string.format, format_string, str)
74+
assert(type(res) == "string")
75+
if ok then
76+
assert((format_string):format(str) == string.format(format_string, str))
77+
end
78+
end
79+
80+
local args = {
81+
artifact_prefix = "string_format_",
82+
}
83+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_gmatch_test.lua

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 s = fdp:consume_string(test_lib.MAX_STR_LEN)
25+
local pattern = fdp:consume_string(test_lib.MAX_STR_LEN)
26+
local init = fdp:consume_integer(0, test_lib.MAX_INT)
27+
string.gmatch(s, pattern, init)
28+
end
29+
30+
local args = {
31+
artifact_prefix = "string_gmatch_",
32+
}
33+
luzer.Fuzz(TestOneInput, nil, args)

tests/lapi/string_gsub_test.lua

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 str = fdp:consume_string(test_lib.MAX_STR_LEN)
27+
local pattern = fdp:consume_string(test_lib.MAX_STR_LEN)
28+
local repl = fdp:consume_string(test_lib.MAX_STR_LEN)
29+
local n = fdp:consume_integer(0, test_lib.MAX_INT)
30+
31+
os.setlocale(test_lib.random_locale(fdp), "all")
32+
-- Avoid errors like "malformed pattern (missing ']')".
33+
local ok, res = pcall(string.gsub, str, pattern, repl, n)
34+
if ok then
35+
assert(type(res) == "string")
36+
end
37+
end
38+
39+
local args = {
40+
artifact_prefix = "string_gsub_",
41+
}
42+
luzer.Fuzz(TestOneInput, nil, args)

0 commit comments

Comments
 (0)