Skip to content

Commit 8758556

Browse files
Mike PallBuristan
Mike Pall
authored andcommitted
Rework stack overflow handling.
Reported by pwnhacker0x18. Fixed by Peter Cawley. (cherry picked from commit defe61a) In case of the Lua stack overflow error, LuaJIT restores the `L->top` value and pushes the error message above. It is possible that the restored value is greater than `L->maxstack`, so pushing the error message causes dirty write out-of-bounds. This patch prevents it by overwriting stack overflow handling machinery. Now, in the aforementioned case, the last frame is replaced with a dummy frame to avoid dirty writes. Sergey Kaplun: * added the description and the test for the problem Part of tarantool/tarantool#11278
1 parent 577aa32 commit 8758556

File tree

6 files changed

+142
-21
lines changed

6 files changed

+142
-21
lines changed

src/lj_debug.c

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ static BCPos debug_framepc(lua_State *L, GCfunc *fn, cTValue *nextframe)
6464
if (cf == NULL || (char *)cframe_pc(cf) == (char *)cframe_L(cf))
6565
return NO_BCPOS;
6666
ins = cframe_pc(cf); /* Only happens during error/hook handling. */
67+
if (!ins) return NO_BCPOS;
6768
} else {
6869
if (frame_islua(nextframe)) {
6970
ins = frame_pc(nextframe);

src/lj_err.c

+20-3
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,14 @@ LJ_NOINLINE void lj_err_mem(lua_State *L)
784784
TValue *base = tvref(G(L)->jit_base);
785785
if (base) L->base = base;
786786
}
787-
if (curr_funcisL(L)) L->top = curr_topL(L);
787+
if (curr_funcisL(L)) {
788+
L->top = curr_topL(L);
789+
if (LJ_UNLIKELY(L->top > tvref(L->maxstack))) {
790+
/* The current Lua frame violates the stack. Replace it with a dummy. */
791+
L->top = L->base;
792+
setframe_gc(L->base - 1 - LJ_FR2, obj2gco(L), LJ_TTHREAD);
793+
}
794+
}
788795
setstrV(L, L->top++, lj_err_str(L, LJ_ERR_ERRMEM));
789796
lj_err_throw(L, LUA_ERRMEM);
790797
}
@@ -845,9 +852,11 @@ LJ_NOINLINE void LJ_FASTCALL lj_err_run(lua_State *L)
845852
{
846853
ptrdiff_t ef = (LJ_HASJIT && tvref(G(L)->jit_base)) ? 0 : finderrfunc(L);
847854
if (ef) {
848-
TValue *errfunc = restorestack(L, ef);
849-
TValue *top = L->top;
855+
TValue *errfunc, *top;
856+
lj_state_checkstack(L, LUA_MINSTACK * 2); /* Might raise new error. */
850857
lj_trace_abort(G(L));
858+
errfunc = restorestack(L, ef);
859+
top = L->top;
851860
if (!tvisfunc(errfunc) || L->status == LUA_ERRERR) {
852861
setstrV(L, top-1, lj_err_str(L, LJ_ERR_ERRERR));
853862
lj_err_throw(L, LUA_ERRERR);
@@ -862,7 +871,15 @@ LJ_NOINLINE void LJ_FASTCALL lj_err_run(lua_State *L)
862871
lj_err_throw(L, LUA_ERRRUN);
863872
}
864873

874+
/* Stack overflow error. */
875+
void LJ_FASTCALL lj_err_stkov(lua_State *L)
876+
{
877+
lj_debug_addloc(L, err2msg(LJ_ERR_STKOV), L->base-1, NULL);
878+
lj_err_run(L);
879+
}
880+
865881
#if LJ_HASJIT
882+
/* Rethrow error after doing a trace exit. */
866883
LJ_NOINLINE void LJ_FASTCALL lj_err_trace(lua_State *L, int errcode)
867884
{
868885
if (errcode == LUA_ERRRUN)

src/lj_err.h

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ LJ_DATA const char *lj_err_allmsg;
2323
LJ_FUNC GCstr *lj_err_str(lua_State *L, ErrMsg em);
2424
LJ_FUNCA_NORET void LJ_FASTCALL lj_err_throw(lua_State *L, int errcode);
2525
LJ_FUNC_NORET void lj_err_mem(lua_State *L);
26+
LJ_FUNC_NORET void LJ_FASTCALL lj_err_stkov(lua_State *L);
2627
LJ_FUNC_NORET void LJ_FASTCALL lj_err_run(lua_State *L);
2728
#if LJ_HASJIT
2829
LJ_FUNCA_NORET void LJ_FASTCALL lj_err_trace(lua_State *L, int errcode);

src/lj_state.c

+40-18
Original file line numberDiff line numberDiff line change
@@ -120,27 +120,49 @@ void lj_state_shrinkstack(lua_State *L, MSize used)
120120
/* Try to grow stack. */
121121
void LJ_FASTCALL lj_state_growstack(lua_State *L, MSize need)
122122
{
123-
MSize n;
124-
if (L->stacksize >= LJ_STACK_MAXEX) {
125-
/* 4. Throw 'error in error handling' when we are _over_ the limit. */
126-
if (L->stacksize > LJ_STACK_MAXEX)
123+
MSize n = L->stacksize + need;
124+
if (LJ_LIKELY(n < LJ_STACK_MAX)) { /* The stack can grow as requested. */
125+
if (n < 2 * L->stacksize) { /* Try to double the size. */
126+
n = 2 * L->stacksize;
127+
if (n > LJ_STACK_MAX)
128+
n = LJ_STACK_MAX;
129+
}
130+
resizestack(L, n);
131+
} else { /* Request would overflow. Raise a stack overflow error. */
132+
if (LJ_HASJIT) {
133+
TValue *base = tvref(G(L)->jit_base);
134+
if (base) L->base = base;
135+
}
136+
if (curr_funcisL(L)) {
137+
L->top = curr_topL(L);
138+
if (L->top > tvref(L->maxstack)) {
139+
/* The current Lua frame violates the stack, so replace it with a
140+
** dummy. This can happen when BC_IFUNCF is trying to grow the stack.
141+
*/
142+
L->top = L->base;
143+
setframe_gc(L->base - 1 - LJ_FR2, obj2gco(L), LJ_TTHREAD);
144+
}
145+
}
146+
if (L->stacksize <= LJ_STACK_MAXEX) {
147+
/* An error handler might want to inspect the stack overflow error, but
148+
** will need some stack space to run in. We give it a stack size beyond
149+
** the normal limit in order to do so, then rely on lj_state_relimitstack
150+
** calls during unwinding to bring us back to a convential stack size.
151+
** The + 1 is space for the error message, and 2 * LUA_MINSTACK is for
152+
** the lj_state_checkstack() call in lj_err_run().
153+
*/
154+
resizestack(L, LJ_STACK_MAX + 1 + 2 * LUA_MINSTACK);
155+
lj_err_stkov(L); /* May invoke an error handler. */
156+
} else {
157+
/* If we're here, then the stack overflow error handler is requesting
158+
** to grow the stack even further. We have no choice but to abort the
159+
** error handler.
160+
*/
161+
GCstr *em = lj_err_str(L, LJ_ERR_STKOV); /* Might OOM. */
162+
setstrV(L, L->top++, em); /* There is always space to push an error. */
127163
lj_err_throw(L, LUA_ERRERR); /* Does not invoke an error handler. */
128-
/* 1. We are _at_ the limit after the last growth. */
129-
if (L->status < LUA_ERRRUN) { /* 2. Throw 'stack overflow'. */
130-
L->status = LUA_ERRRUN; /* Prevent ending here again for pushed msg. */
131-
lj_err_msg(L, LJ_ERR_STKOV); /* May invoke an error handler. */
132164
}
133-
/* 3. Add space (over the limit) for pushed message and error handler. */
134-
}
135-
n = L->stacksize + need;
136-
if (n > LJ_STACK_MAX) {
137-
n += 2*LUA_MINSTACK;
138-
} else if (n < 2*L->stacksize) {
139-
n = 2*L->stacksize;
140-
if (n >= LJ_STACK_MAX)
141-
n = LJ_STACK_MAX;
142165
}
143-
resizestack(L, n);
144166
}
145167

146168
void LJ_FASTCALL lj_state_growstack1(lua_State *L)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
local tap = require('tap')
2+
local allocinject = require('allocinject')
3+
4+
local test = tap.test('lj-1152-stack-buffer-overflow-on-error')
5+
test:plan(6)
6+
7+
local LJ_MAX_LOCVAR = 200
8+
9+
-- Generate the following Lua chunk:
10+
-- local function recursive_f()
11+
-- local _
12+
-- ...
13+
-- recursive_f()
14+
-- end
15+
-- return recursive_f
16+
local function generate_recursive_f(n_locals)
17+
local chunk = 'local function recursive_f()\n'
18+
for _ = 1, n_locals do
19+
chunk = chunk .. 'local _\n'
20+
end
21+
chunk = chunk .. [[
22+
recursive_f()
23+
end
24+
return recursive_f
25+
]]
26+
local f = assert(loadstring(chunk))
27+
return f()
28+
end
29+
30+
-- Use `coroutine.wrap()` for functions to use newly created stack
31+
-- with fixed number of stack slots.
32+
33+
-- Check that we still got the correct error message in case of
34+
-- the unsafe error handler function.
35+
coroutine.wrap(function()
36+
-- XXX: Use different recursive functions as callee and handler
37+
-- to be sure to get the invalide stack value instead of `nil`.
38+
local function recursive() recursive() end
39+
local function bad_errfunc() bad_errfunc() end
40+
local r, e = xpcall(recursive, bad_errfunc)
41+
-- XXX: Don't create a constant string that is anchored to the
42+
-- prototype. It is necessary to make the error message freed by
43+
-- the GC and OOM raising in the last test.
44+
local EXPECTED_MSG = 'stack ' .. 'overflow'
45+
test:ok(not r, 'correct status')
46+
test:like(e, EXPECTED_MSG, 'correct error message')
47+
end)()
48+
49+
coroutine.wrap(function()
50+
-- Collect all strings including the possibly-existed string
51+
-- with the 'stack overflow' error message.
52+
collectgarbage()
53+
local function recursive() recursive() end
54+
-- Avoid trace recording. A trace can't be allocated anyway.
55+
jit.off(recursive)
56+
57+
-- Check the case when the error
58+
allocinject.enable_null_alloc()
59+
local r, e = pcall(recursive)
60+
allocinject.disable()
61+
62+
test:ok(not r, 'correct status')
63+
test:like(e, 'not enough memory', 'correct error message')
64+
end)()
65+
66+
-- Check overflow of the buffer related to the Lua stack.
67+
-- This test fails under ASAN without the patch.
68+
local recursive_f = generate_recursive_f(LJ_MAX_LOCVAR)
69+
coroutine.wrap(function()
70+
local r, e = pcall(recursive_f)
71+
test:ok(not r, 'correct status')
72+
-- XXX: Don't create a constant string that is anchored to the
73+
-- prototype. It is necessary to make the error message freed by
74+
-- the GC and OOM raising in the last test.
75+
local EXPECTED_MSG = 'stack ' .. 'overflow'
76+
test:like(e, EXPECTED_MSG, 'correct error message')
77+
end)()
78+
79+
test:done(true)

test/tarantool-tests/utils/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
list(APPEND tests
2+
lj-1152-stack-buffer-overflow-on-error.test.lua
23
lj-1166-error-stitch-oom-ir-buff.test.lua
34
lj-1166-error-stitch-oom-snap-buff.test.lua
45
lj-1247-fin-tab-rehashing-on-trace.test.lua

0 commit comments

Comments
 (0)