On Thu, Jan 16, 2025 at 06:44:30PM -0800, Jizhou Chen wrote:
> Hi there,
>
> Just to report that Lua crashes with SEGV with the following test case.
>
> *Version: *5.4.7 (ASAN enabled)
>
> *Test case (fuzzer-generated):*
>
> *do local a = collectgarbage("setpause", 200) local b =
> collectgarbage("generational", 1) local t =
> {pcall(pcall,pcall,pcall,pcall,assert,pcall,pcall,error,"hi")} for i = 3,
> 5, 2 do local p = t[i] for i = 1.0, 10 do local r =
> debug.getregistry() local tt = {} tt.__gc = function (x)
> assert(debug.setlocal(2, #t, x) == "(vararg)") assert(thread_id <
> 1000) end local u = setmetatable({}, tt) ___Glob = {u}end end
> collectgarbage("generational", a) debug.getuservalue(b, -2)
> collectgarbage()end_G["while"] = 234*
>
> *Stack dump:*
tl;dr: this isn't a fault of Lua interpreter. The usage of
debug.setlocal() is unsafe. One should remember that functions in the
debug library are dangerous and may break expected behavior.
You should exert care when using this library. Several of its
functions violate basic assumptions about Lua code (e.g., that
variables local to a function cannot be accessed from outside;
that userdata metatables cannot be changed by Lua code; that
Lua programs do not crash) and therefore can compromise otherwise
secure code.[1]
Format of your testcase seems broken. For the interested reader, here's
a reformated version,
do
local a = collectgarbage("setpause", 200)
local b = collectgarbage("generational", 1)
local t = {pcall(pcall,pcall,pcall,pcall,assert,pcall,
pcall,error,"hi")}
for i = 3, 5, 2 do
local p = t[i]
for i = 1.0, 10 do
local r = debug.getregistry()
local tt = {}
tt.__gc = function (x)
assert(debug.setlocal(2, #t, x) == "(vararg)")
assert(thread_id < 1000)
end
local u = setmetatable({}, tt)
___Glob = {u}
end
end
collectgarbage("generational", a)
debug.getuservalue(b, -2)
collectgarbage()
end
_G["while"] = 234
which immediately results in a SIGSEGV on my x86_64-ewe-linux-musl
machine.
The cataclysm occurs around the call to debug.setlocal(). With this in
mind, I rewrote a smaller case,
do
collectgarbage("generational", 1);
local a, b, c, d
for i = 1, 9 do
local tt = {};
tt.__gc = function (x)
debug.setlocal(2, 5, x);
end
x = {setmetatable({}, tt)}
end
end
If you build Lua with assertion enabled (src/llimits.h:103), a failure
will be observed,
Assertion failed: (((((&(ra)->val)))->tt_) == (((3) | ((0) << 4))))
(lvm.c: luaV_execute: 1790)
Aborted
and lvm.c:1790 handles for-loop opcode. This assertion checks whether
for-loop control variable is of an integer type.
When compiling a for loop, Lua interpreter allocates locals (registers)
to hold the evaluated results of the control varible, limit and step
expressions[2].
Looking back to the case: since the minor multiplier for generational
gc is small, after some new tables created in several iterations, a GC
step will be triggered: the __gc metamethod will be invoked, with the
main chunk as the caller. Then setlocal(2, 5, x) will rewrite the fifth
local in the main chunk: a, b, c, d are the first four and we're
actually rewriting the one containing control variable, replacing the
integer with a table. This really shouldn't happen when running normal
code.
Thanks,
Yao Zi
[1]: Lua 5.4 Manual, 6.10 The Debug Library
[2]: Lua 5.4 Manual, 3.3.5: The loop starts by evaluating once the three
control expressions.