HARDMEMTESTS: apparent stale stack pointer written to L->top by checkGC (heap OOB read in traversethread)

kid <[email protected]>
Newsgroups gmane.comp.lang.lua.general
Message-ID <[email protected]>

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.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.