Skip to content

Commit

Permalink
sysprof: fix crash during FFUNC stream
Browse files Browse the repository at this point in the history
Sometimes, the Lua stack can be inconsistent during
the FFUNC execution, which may lead to a sysprof
crash during the stack unwinding.

This patch replaces the `top_frame` property of `global_State`
with `lj_sysprof_topframe` structure, which contains `top_frame`
and `ffid` properties. `ffid` property makes sense only when the
LuaJIT VM state is set to `FFUNC`. That property is set to the
ffid of the fast function that VM is about to execute.
In the same time, `top_frame` property is not updated now, so
the top frame of the Lua stack can be streamed based on the ffid,
and the rest of the Lua stack can be streamed as usual.

Resolves tarantool/tarantool#8594
  • Loading branch information
mkokryashkin committed Jul 4, 2023
1 parent 8ddd7bc commit c3e43d6
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 13 deletions.
7 changes: 6 additions & 1 deletion src/lj_obj.h
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,11 @@ enum {
GCSmax
};

struct lj_sysprof_topframe {
TValue *top_frame; /* Top frame for sysprof. */
uint8_t ffid; /* FFID of the fast function VM is about to execute. */
};

typedef struct GCState {
GCSize total; /* Memory currently allocated. */
GCSize threshold; /* Memory threshold. */
Expand Down Expand Up @@ -675,7 +680,7 @@ typedef struct global_State {
MRef ctype_state; /* Pointer to C type state. */
GCRef gcroot[GCROOT_MAX]; /* GC roots. */
#ifdef LJ_HASSYSPROF
TValue *top_frame; /* Top frame for sysprof. */
struct lj_sysprof_topframe top_frame_info; /* Top frame info for sysprof. */
#endif
} global_State;

Expand Down
26 changes: 21 additions & 5 deletions src/lj_sysprof.c
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ static void stream_epilogue(struct sysprof *sp)
lj_wbuf_addbyte(&sp->out, LJP_EPILOGUE_BYTE);
}

static void stream_ffunc_impl(struct lj_wbuf *buf, uint8_t ffid)
{
lj_wbuf_addbyte(buf, LJP_FRAME_FFUNC);
lj_wbuf_addu64(buf, ffid);
}

static void stream_lfunc(struct lj_wbuf *buf, const GCfunc *func)
{
lua_assert(isluafunc(func));
Expand All @@ -129,8 +135,7 @@ static void stream_cfunc(struct lj_wbuf *buf, const GCfunc *func)
static void stream_ffunc(struct lj_wbuf *buf, const GCfunc *func)
{
lua_assert(isffunc(func));
lj_wbuf_addbyte(buf, LJP_FRAME_FFUNC);
lj_wbuf_addu64(buf, func->c.ffid);
stream_ffunc_impl(buf, func->c.ffid);
}

static void stream_frame_lua(struct lj_wbuf *buf, const cTValue *frame)
Expand All @@ -148,7 +153,7 @@ static void stream_frame_lua(struct lj_wbuf *buf, const cTValue *frame)
lua_assert(0);
}

static void stream_backtrace_lua(struct sysprof *sp)
static void stream_backtrace_lua(struct sysprof *sp, uint32_t vmstate)
{
global_State *g = sp->g;
struct lj_wbuf *buf = &sp->out;
Expand All @@ -158,8 +163,19 @@ static void stream_backtrace_lua(struct sysprof *sp)
lua_assert(g != NULL);
L = gco2th(gcref(g->cur_L));
lua_assert(L != NULL);
/*
** Lua stack may be inconsistent during the execution of a
** fast-function, so instead of updating the `top_frame` for
** it, its `ffid` is set instead. The first frame on the
** result stack is streamed manually, and the rest of the
** stack is streamed based on the previous `top_frame` value.
*/
if (vmstate == LJ_VMST_FFUNC) {
uint8_t ffid = g->top_frame_info.ffid;
stream_ffunc_impl(buf, ffid);
}

top_frame = g->top_frame - 1; //(1 + LJ_FR2)
top_frame = g->top_frame_info.top_frame - 1;

bot = tvref(L->stack) + LJ_FR2;
/* Traverse frames backwards */
Expand Down Expand Up @@ -234,7 +250,7 @@ static void stream_trace(struct sysprof *sp, uint32_t vmstate)
static void stream_guest(struct sysprof *sp, uint32_t vmstate)
{
lj_wbuf_addbyte(&sp->out, (uint8_t)vmstate);
stream_backtrace_lua(sp);
stream_backtrace_lua(sp, vmstate);
stream_backtrace_host(sp);
}

Expand Down
21 changes: 18 additions & 3 deletions src/vm_x64.dasc
Original file line number Diff line number Diff line change
Expand Up @@ -353,14 +353,29 @@
|// it syncs with the BASE register only when the control is passed to
|// user code. So we need to sync the BASE on each vmstate change to
|// keep it consistent.
|// The only execption are FFUNCs because sometimes even internal BASE
|// stash is inconsistent for them. To address that issue, their ffid
|// is stashed instead, so the corresponding frame can be streamed
|// manually.
|.macro set_vmstate_sync_base, st
|.if LJ_HASSYSPROF
| set_vmstate INTERP // Guard for non-atomic VM context restoration
| mov qword [DISPATCH+DISPATCH_GL(top_frame)], BASE
| mov qword [DISPATCH+DISPATCH_GL(top_frame_info.top_frame)], BASE
|.endif
| set_vmstate st
|.endmacro
|
|.macro set_vmstate_ffunc
|.if LJ_HASSYSPROF
| set_vmstate INTERP
| mov TMPR, [BASE - 16]
| cleartp LFUNC:TMPR
| mov r10b, LFUNC:TMPR->ffid // r10b is the byte-sized part of TMPR
| mov byte [DISPATCH+DISPATCH_GL(top_frame_info.ffid)], r10b
|.endif
| set_vmstate FFUNC
|.endmacro
|
|// Uses TMPRd (r10d).
|.macro save_vmstate
|.if not WIN
Expand All @@ -376,7 +391,7 @@
| set_vmstate INTERP
| mov TMPR, SAVE_L
| mov TMPR, L:TMPR->base
| mov qword [DISPATCH+DISPATCH_GL(top_frame)], TMPR
| mov qword [DISPATCH+DISPATCH_GL(top_frame_info.top_frame)], TMPR
|.endif
| mov TMPRd, SAVE_VMSTATE
| mov dword [DISPATCH+DISPATCH_GL(vmstate)], TMPRd
Expand Down Expand Up @@ -1188,7 +1203,7 @@ static void build_subroutines(BuildCtx *ctx)
|
|.macro .ffunc, name
|->ff_ .. name:
| set_vmstate_sync_base FFUNC
| set_vmstate_ffunc
|.endmacro
|
|.macro .ffunc_1, name
Expand Down
22 changes: 18 additions & 4 deletions src/vm_x86.dasc
Original file line number Diff line number Diff line change
Expand Up @@ -451,14 +451,28 @@
|// it syncs with the BASE register only when the control is passed to
|// user code. So we need to sync the BASE on each vmstate change to
|// keep it consistent.
|// The only execption are FFUNCs because sometimes even internal BASE
|// stash is inconsistent for them. To address that issue, their ffid
|// is stashed instead, so the corresponding frame can be streamed
|// manually.
|.macro set_vmstate_sync_base, st
|.if LJ_HASSYSPROF
| set_vmstate INTERP // Guard for non-atomic VM context restoration
| mov dword [DISPATCH+DISPATCH_GL(top_frame)], BASE
| mov dword [DISPATCH+DISPATCH_GL(top_frame_info.top_frame)], BASE
|.endif
| set_vmstate st
|.endmacro
|
|.macro set_vmstate_ffunc
|.if LJ_HASSYSPROF
| set_vmstate INTERP
| mov LFUNC:XCHGd, [BASE - 8]
| mov r11b, LFUNC:XCHGd->ffid // r11b is the byte-sized part of XCHGd
| mov byte [DISPATCH+DISPATCH_GL(top_frame_info.ffid)], r11b
|.endif
| set_vmstate FFUNC
|.endmacro
|
|// Uses spilled ecx on x86 or XCHGd (r11d) on x64.
|.macro save_vmstate
|.if not WIN
Expand All @@ -485,7 +499,7 @@
|.if LJ_HASSYSPROF
| mov ecx, SAVE_L
| mov ecx, L:ecx->base
| mov dword [DISPATCH+DISPATCH_GL(top_frame)], ecx
| mov dword [DISPATCH+DISPATCH_GL(top_frame_info.top_frame)], ecx
|.endif
| mov ecx, SAVE_VMSTATE
| mov dword [DISPATCH+DISPATCH_GL(vmstate)], ecx
Expand All @@ -494,7 +508,7 @@
|.if LJ_HASSYSPROF
| mov XCHGd, SAVE_L
| mov XCHGd, L:XCHGd->base
| mov dword [DISPATCH+DISPATCH_GL(top_frame)], XCHGd
| mov dword [DISPATCH+DISPATCH_GL(top_frame_info.top_frame)], XCHGd
|.endif
| mov XCHGd, SAVE_VMSTATE
| mov dword [DISPATCH+DISPATCH_GL(vmstate)], XCHGd
Expand Down Expand Up @@ -1488,7 +1502,7 @@ static void build_subroutines(BuildCtx *ctx)
|
|.macro .ffunc, name
|->ff_ .. name:
| set_vmstate_sync_base FFUNC
| set_vmstate_ffunc
|.endmacro
|
|.macro .ffunc_1, name
Expand Down
33 changes: 33 additions & 0 deletions test/tarantool-tests/gh-8594-sysprof-ffunc-crash.test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
local tap = require('tap')
local test = tap.test('gh-8594-sysprof-ffunc-crash'):skipcond({
['Sysprof is implemented for x86_64 only'] = jit.arch ~= 'x86' and
jit.arch ~= 'x64',
['Sysprof is implemented for Linux only'] = jit.os ~= "Linux",
})

test:plan(1)

jit.off()
-- XXX: Run JIT tuning functions in a safe frame to avoid errors
-- thrown when LuaJIT is compiled with JIT engine disabled.
pcall(jit.flush)

local TMP_BINFILE = '/dev/null'

local res, err = misc.sysprof.start{
mode = 'C',
interval = 10,
path = TMP_BINFILE,
}
assert(res, err)

for i = 1, 1e5 do
tostring(i)
end

res, err = misc.sysprof.stop()
assert(res, err)

test:ok(true, 'sysprof finished successfully')

os.exit(test:check() and 0 or 1)

0 comments on commit c3e43d6

Please sign in to comment.