Re: HARDMEMTESTS: apparent stale stack pointer written to L->top by checkGC (heap OOB read in traversethread)
Osman Ansari <[email protected]>
| Newsgroups | gmane.comp.lang.lua.general |
|---|---|
| Message-ID | <CABXqZpNGHNwtsO_8qcLSUJWmnxR-qmhkzZDGpBp7k_vGUs2zkQ@mail.gmail.com> |
Your newsletter is consistently outstanding. Thank you for setting the bar so high! On Wed, 19 Aug 2026 at 12:39, kid <[email protected]> wrote: > Hello, > > I would like to report a problem I ran into while testing the current > master (7579fc9d7ed90240487251dfb69168f8e64e9294) built with > -DHARDMEMTESTS: AddressSanitizer reports a heap out-of-bounds read in > traversethread. From my reading of the code, it looks as if checkGC(L, ra > + 1) wrote a stale stack pointer to L->top after the GC step moved the > stack, but I may well be missing something about the intended invariants > here, so I would appreciate your view. I found it while fuzzing with an > OSS-Fuzz-style harness, but it also reproduces with the plain interpreter. > > Reproduction > > Save as repro.lua: > local function f(n) > if n <= 0 then return 0 end > return f(n - 1) + 0 > end > f(100) > local function g() return 1 end > > *Build and run:* > git clone https://github.com/lua/lua && cd lua > make CC=clang \ > MYCFLAGS="-std=c99 -O1 -g -DLUA_USE_LINUX -DHARDMEMTESTS > -fsanitize=address -fno-omit-frame-pointer" \ > MYLDFLAGS="-fsanitize=address" > ./lua repro.lua > > > *Result:* > > ==7071==ERROR: AddressSanitizer: heap-buffer-overflow on address > 0x518000000ff8 at pc 0x598b2bf1a477 bp 0x7ffc26980780 sp 0x7ffc26980778 > READ of size 1 at 0x518000000ff8 thread T0 > #0 0x598b2bf1a476 (lua+0x143476) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => propagatemark lgc.c:705 > #1 0x598b2bf1646e (lua+0x13f46e) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => atomic lgc.c:745 > #2 0x598b2bf130b7 (lua+0x13c0b7) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => entergen lgc.c:1431 > #3 0x598b2bf158bb (lua+0x13e8bb) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => fullgen lgc.c:1460 > #4 0x598b2bf4a369 (lua+0x173369) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaV_execute lvm.c:1939 > #5 0x598b2bf09edd (lua+0x132edd) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_callnoyield ldo.c:768 > #6 0x598b2bf05b55 (lua+0x12eb55) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_rawrunprotected ldo.c:166 > #7 0x598b2bf0b355 (lua+0x134355) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_pcall ldo.c:1090 > #8 0x598b2befb9a8 (lua+0x1249a8) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => lua_pcallk lapi.c:1097 > #9 0x598b2bef02ca (lua+0x1192ca) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => docall lua.c:168 > #10 0x598b2beef59b (lua+0x11859b) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => pmain lua.c:272 > #11 0x598b2bf097b7 (lua+0x1327b7) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_precall ldo.c:657 > #12 0x598b2bf09eaf (lua+0x132eaf) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_callnoyield ldo.c:766 > #13 0x598b2bf05b55 (lua+0x12eb55) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_rawrunprotected ldo.c:166 > #14 0x598b2bf0b355 (lua+0x134355) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_pcall ldo.c:1090 > #15 0x598b2befb9a8 (lua+0x1249a8) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => lua_pcallk lapi.c:1097 > #16 0x598b2beeea5d (lua+0x117a5d) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => main lua.c:788 > #17 0x7dfe5e02a1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) > (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7) > #18 0x7dfe5e02a28a (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) > (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7) > #19 0x598b2be15604 (lua+0x3e604) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => _start ??:? > > 0x518000000ff8 is located 8 bytes after 880-byte region > [0x518000000c80,0x518000000ff0) > allocated by thread T0 here: > #0 0x598b2beb0870 (lua+0xd9870) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => ___interceptor_realloc ??:? > #1 0x598b2bf1beee (lua+0x144eee) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaM_realloc_ lmem.c:180 > #2 0x598b2bf062de (lua+0x12f2de) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_reallocstack ldo.c:339 > #3 0x598b2bf069ee (lua+0x12f9ee) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_shrinkstack ldo.c:426 > #4 0x598b2bf19764 (lua+0x142764) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => propagatemark lgc.c:710 > #5 0x598b2bf166f7 (lua+0x13f6f7) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => atomic lgc.c:745 > #6 0x598b2bf14978 (lua+0x13d978) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaC_step lgc.c:1348 > #7 0x598b2bf4a2c8 (lua+0x1732c8) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaV_execute lvm.c:1939 > #8 0x598b2bf09edd (lua+0x132edd) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_callnoyield ldo.c:768 > #9 0x598b2bf05b55 (lua+0x12eb55) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_rawrunprotected ldo.c:166 > #10 0x598b2bf0b355 (lua+0x134355) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_pcall ldo.c:1090 > #11 0x598b2befb9a8 (lua+0x1249a8) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => lua_pcallk lapi.c:1097 > #12 0x598b2bef02ca (lua+0x1192ca) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => docall lua.c:168 > #13 0x598b2beef59b (lua+0x11859b) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => pmain lua.c:272 > #14 0x598b2bf097b7 (lua+0x1327b7) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_precall ldo.c:657 > #15 0x598b2bf09eaf (lua+0x132eaf) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_callnoyield ldo.c:766 > #16 0x598b2bf05b55 (lua+0x12eb55) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_rawrunprotected ldo.c:166 > #17 0x598b2bf0b355 (lua+0x134355) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => luaD_pcall ldo.c:1090 > #18 0x598b2befb9a8 (lua+0x1249a8) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => lua_pcallk lapi.c:1097 > #19 0x598b2beeea5d (lua+0x117a5d) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => main lua.c:788 > #20 0x7dfe5e02a1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) > (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7) > #21 0x7dfe5e02a28a (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) > (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7) > #22 0x598b2be15604 (lua+0x3e604) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => _start ??:? > > SUMMARY: AddressSanitizer: heap-buffer-overflow (lua+0x143476) (BuildId: > cf47c548dcdedf37f5c5990c60b2f6da369f4d68) > => propagatemark lgc.c:705 > Shadow bytes around the buggy address: > 0x518000000d00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 > 0x518000000d80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 > 0x518000000e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 > 0x518000000e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 > 0x518000000f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 > =>0x518000000f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fa[fa] > 0x518000001000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa > 0x518000001080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa > 0x518000001100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa > 0x518000001180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa > 0x518000001200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa > Shadow byte legend (one shadow byte represents 8 application bytes): > Addressable: 00 > Partially addressable: 01 02 03 04 05 06 07 > Heap left redzone: fa > Freed heap region: fd > Stack left redzone: f1 > Stack mid redzone: f2 > Stack right redzone: f3 > Stack after return: f5 > Stack use after scope: f8 > Global redzone: f9 > Global init order: f6 > Poisoned by user: f7 > Container overflow: fc > Array cookie: ac > Intra object redzone: bb > ASan internal: fe > Left alloca redzone: ca > Right alloca redzone: cb > ==7071==ABORTING > > That is, the collection that over-reads the stack happens in the > luaC_fullgc forced by HARDMEMTESTS, and it reads from a stack that was > just shrunk/reallocated by the luaC_step of the very same checkGC. > > Explanation > > OP_CLOSURE (lvm.c:1935) and OP_NEWTABLE (lvm.c:1423) both end with checkGC(L, > ra + 1), which expands to luaC_condGC(L, (savepc(ci), L->top.p = ra+1), > updatetrap(ci)). > > With HARDMEMTESTS on, luaC_condGC evaluates pre twice (lgc.h:229-235): > once before luaC_step (when the debt is due) and once again before the > forced luaC_fullgc (condchangemem). > > In the repro, f(100) leaves the stack much larger than necessary (stacksize > > 3*inuse). At the OP_CLOSURE for g: > > 1. first pre: L->top.p = ra + 1 (valid); luaC_step runs, its atomic > phase calls traversethread -> luaD_shrinkstack, which reallocates the > stack. correctstack remaps L->top correctly and sets ci->u.l.trap = 1, > but the C registers base/ra of the running luaV_execute stay stale > until the next vmfetch; > 2. second pre (from condchangemem): re-evaluates L->top.p = ra + 1 with > the stale ra, overwriting the correctly remapped value with a dangling > pointer (I confirmed this write with a gdb watchpoint: L->top goes > from the remapped, in-bounds address to an address in the freed old stack, > PC at lvm.c:1939); > 3. luaC_fullgc then marks [th->stack, th->top) out of bounds > (lgc.c:704-705). > > Without HARDMEMTESTS, condchangemem compiles to nothing, pre runs only > once (before the step, with a valid ra), and the trap/vmfetch mechanism > repairs base before it is used again — so normal builds cannot reach > this. It is also rare with HARDMEMTESTS: the step of that particular > checkGC must actually shrink the stack, i.e. the stack must be oversized > relative to inuse at that point. Besides deep recursion, a call with many > arguments also works; this variant needs no recursion at all (crashes at > OP_NEWTABLE, lvm.c:1431): > local function sink(...) return 0 end > local t = {} > for i = 1, 20000 do t[i] = i end > sink(table.unpack(t)) > local x = {} > > Note that OP_CONCAT's checkGC(L, L->top.p) (lvm.c:1637) is not affected, > because re-reading L->top.p after the remap is safe. > > -- > You received this message because you are subscribed to the Google Groups > "lua-l" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to [email protected]. > To view this discussion visit > https://groups.google.com/d/msgid/lua-l/ec502a73-1dc1-4912-8ba1-959d467d0dcbn%40googlegroups.com > <https://groups.google.com/d/msgid/lua-l/ec502a73-1dc1-4912-8ba1-959d467d0dcbn%40googlegroups.com?utm_medium=email&utm_source=footer> > . > -- You received this message because you are subscribed to the Google Groups "lua-l" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion visit https://groups.google.com/d/msgid/lua-l/CABXqZpNGHNwtsO_8qcLSUJWmnxR-qmhkzZDGpBp7k_vGUs2zkQ%40mail.gmail.com.