Skip to content

Commit de2f1ab

Browse files
committed
tests/lapi: add coroutine test
The patch adds a fuzzing test for the coroutine library.
1 parent 8e35f0c commit de2f1ab

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed

tests/lapi/coroutine_torture_test.lua

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
--[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
2.6 – Coroutines
6+
https://www.lua.org/manual/5.3/manual.html#2.6
7+
8+
Computation of stack limit when entering a coroutine is wrong,
9+
https://github.com/lua/lua/commit/e1d8770f12542d34a3e32b825c95b93f8a341ee1
10+
11+
C-stack overflow with deep nesting of coroutine.close,
12+
https://www.lua.org/bugs.html#5.4.4-9
13+
14+
C stack overflow (again),
15+
https://github.com/lua/lua/commit/34affe7a63fc5d842580a9f23616d057e17dfe27
16+
17+
When a coroutine tries to resume a non-suspended coroutine,
18+
it can do some mess (and break C assertions) before detecting the error,
19+
https://www.lua.org/bugs.html#5.3.3-4
20+
21+
debug.getlocal on a coroutine suspended in a hook can crash the interpreter,
22+
https://www.lua.org/bugs.html#5.3.0-2
23+
24+
Suspended __le metamethod can give wrong result,
25+
https://www.lua.org/bugs.html#5.3.0-3
26+
27+
Resuming the running coroutine makes it unyieldable,
28+
https://www.lua.org/bugs.html#5.2.2-8
29+
30+
pcall may not restore previous error function when inside coroutines,
31+
https://www.lua.org/bugs.html#5.2.1-2
32+
33+
Wrong handling of nCcalls in coroutines,
34+
https://www.lua.org/bugs.html#5.2.0-4
35+
36+
coroutine.resume pushes element without ensuring stack size,
37+
https://www.lua.org/bugs.html#5.1.3-2
38+
39+
Recursive coroutines may overflow C stack,
40+
https://www.lua.org/bugs.html#5.1.2-4
41+
42+
Stand-alone interpreter shows incorrect error message when the
43+
"message" is a coroutine,
44+
https://www.lua.org/bugs.html#5.1.2-12
45+
46+
Debug hooks may get wrong when mixed with coroutines,
47+
https://www.lua.org/bugs.html#5.1-7
48+
49+
Values held in open upvalues of suspended threads may be
50+
incorrectly collected,
51+
https://www.lua.org/bugs.html#5.0.2-3
52+
53+
Attempt to resume a running coroutine crashes Lua,
54+
https://www.lua.org/bugs.html#5.0-2
55+
56+
debug.getlocal on a coroutine suspended in a hook can crash the interpreter,
57+
https://www.lua.org/bugs.html#5.3.0-2
58+
59+
debug.sethook/gethook may overflow the thread's stack,
60+
https://www.lua.org/bugs.html#5.1.2-13
61+
62+
Memory hoarding when creating Lua hooks for coroutines,
63+
https://www.lua.org/bugs.html#5.2.0-1
64+
65+
Synopsis:
66+
67+
coroutine.close(co)
68+
coroutine.create(f)
69+
coroutine.isyieldable([co])
70+
coroutine.resume(co [, val1, ...])
71+
coroutine.running()
72+
coroutine.status(co)
73+
coroutine.wrap(f)
74+
coroutine.yield(...)
75+
]]
76+
77+
local luzer = require("luzer")
78+
local test_lib = require("lib")
79+
80+
local CORO_OBJECTS = {}
81+
82+
-- The function `coroutine.isyieldable()` is checked together with
83+
-- `coroutine.yield()`, `coroutine.status()` is used inside the
84+
-- target function.
85+
local CORO_ACTION_NAME = {
86+
"create",
87+
"resume",
88+
"running",
89+
"yield",
90+
}
91+
-- `coroutine.close()` is introduced in the Lua 5.4,
92+
-- https://www.lua.org/manual/5.4/manual.html#pdf-coroutine.close.
93+
if test_lib.lua_current_version_ge_than(5, 4) then
94+
table.insert(CORO_ACTION_NAME, "close")
95+
end
96+
97+
-- Forward declaration.
98+
local coro_function
99+
100+
local function hook_func(_event)
101+
-- Accessing Locals,
102+
-- https://www.lua.org/pil/23.1.1.html.
103+
local level = 2
104+
local i = 1
105+
while true do
106+
local name, _ = debug.getlocal(level, i)
107+
if not name then break end
108+
i = i + 1
109+
end
110+
-- Accessing Upvalues,
111+
-- https://www.lua.org/pil/23.1.2.html.
112+
local func = debug.getinfo(level).func
113+
i = 1
114+
while true do
115+
local name, _ = debug.getupvalue(func, i)
116+
if not name then break end
117+
i = i + 1
118+
end
119+
end
120+
121+
local function sethook(co, fdp)
122+
local set_hook = fdp:consume_boolean()
123+
local hook_args = {}
124+
if not set_hook then
125+
return
126+
end
127+
table.insert(hook_args, hook_func)
128+
table.insert(hook_args, fdp:oneof({"c", "r", "l"}))
129+
debug.sethook(co, unpack(hook_args))
130+
end
131+
132+
local function coro_random_action(fdp, coro_max_number)
133+
local action = fdp:oneof(CORO_ACTION_NAME)
134+
if action == "create" and #CORO_OBJECTS < coro_max_number then
135+
local co = coroutine.create(coro_function)
136+
table.insert(CORO_OBJECTS, co)
137+
return
138+
end
139+
action = fdp:oneof(CORO_ACTION_NAME)
140+
local co, co_idx = fdp:oneof(CORO_OBJECTS)
141+
sethook(co, fdp)
142+
if coroutine.status(co) == "dead" then
143+
table.remove(CORO_OBJECTS, co_idx)
144+
return
145+
elseif action == "close" then
146+
coroutine.close(co)
147+
elseif action == "yield" and coroutine.isyieldable(co) then
148+
coroutine.yield(co)
149+
elseif action == "resume" then
150+
coroutine.resume(co)
151+
elseif action == "running" then
152+
local c = coroutine.running()
153+
assert(c == nil or type(c) == "thread")
154+
end
155+
end
156+
157+
coro_function = function(fdp, coro_max_number)
158+
local MAX_N = 1000
159+
local iter = fdp:consume_integer(1, MAX_N)
160+
for _ = 1, iter do
161+
coro_random_action(fdp, coro_max_number)
162+
end
163+
end
164+
165+
local function TestOneInput(buf, _size)
166+
CORO_OBJECTS = {}
167+
local fdp = luzer.FuzzedDataProvider(buf)
168+
local MAX_N = 1000
169+
local coro_max_number = fdp:consume_integer(1, MAX_N)
170+
local co = coroutine.create(coro_function)
171+
table.insert(CORO_OBJECTS, co)
172+
-- The function `coroutine.resume` starts the execution of
173+
-- a coroutine, changing its state from suspended to running.
174+
coroutine.resume(co, fdp, coro_max_number)
175+
end
176+
177+
local args = {
178+
artifact_prefix = "coroutine_",
179+
}
180+
luzer.Fuzz(TestOneInput, nil, args)

0 commit comments

Comments
 (0)