From 5ea0de9fbb7d27b6195bcef45de1430df6c6967e Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 23:14:43 -0600 Subject: [PATCH] crash vm trace --- source/cell.c | 10 ++++++ source/cell_internal.h | 4 +++ source/quickjs.h | 5 +++ source/runtime.c | 71 +++++++++++++++++++++++++++++++++++++++--- source/scheduler.c | 5 +++ 5 files changed, 91 insertions(+), 4 deletions(-) diff --git a/source/cell.c b/source/cell.c index 4d244220..3f2190db 100644 --- a/source/cell.c +++ b/source/cell.c @@ -28,6 +28,7 @@ static int run_test_suite(size_t heap_size); cell_rt *root_cell = NULL; static char *shop_path = NULL; +volatile JSContext *g_crash_ctx = NULL; static char *core_path = NULL; static int native_mode = 0; static int warn_mode = 1; @@ -408,6 +409,13 @@ static void signal_handler(int sig) #endif if (!str) return; + /* Reset handler to default so a double-fault terminates immediately */ + signal(sig, SIG_DFL); + + /* Try to print the JS stack (best-effort, signal-safe) */ + if (g_crash_ctx) + JS_CrashPrintStack((JSContext *)g_crash_ctx); + exit_handler(); } @@ -689,7 +697,9 @@ int cell_init(int argc, char **argv) JS_DeleteGCRef(ctx, &args_ref); JSValue hidden_env = JS_Stone(ctx, env_ref.val); + g_crash_ctx = ctx; JSValue result = JS_RunMachBin(ctx, (const uint8_t *)bin_data, bin_size, hidden_env); + g_crash_ctx = NULL; JS_DeleteGCRef(ctx, &env_ref); free(bin_data); diff --git a/source/cell_internal.h b/source/cell_internal.h index 6f5d429c..77ed2f12 100644 --- a/source/cell_internal.h +++ b/source/cell_internal.h @@ -76,6 +76,10 @@ typedef struct cell_rt { cell_hook trace_hook; } cell_rt; +/* Set by actor_turn/CLI before entering the VM, cleared after. + Read by signal_handler to print JS stack on crash. */ +extern volatile JSContext *g_crash_ctx; + cell_rt *create_actor(void *wota); const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar); void actor_disrupt(cell_rt *actor); diff --git a/source/quickjs.h b/source/quickjs.h index de64a852..125da3c5 100644 --- a/source/quickjs.h +++ b/source/quickjs.h @@ -1054,6 +1054,11 @@ MachCode *mach_compile_mcode(struct cJSON *mcode_json); Does NOT clear reg_current_frame — caller is responsible if needed. */ JSValue JS_GetStack (JSContext *ctx); +/* Crash-safe stack printer: walks JS frames and writes to stderr. + Uses only write() (async-signal-safe). No GC allocations. + Safe to call from signal handlers. */ +void JS_CrashPrintStack(JSContext *ctx); + #undef js_unlikely #undef inline diff --git a/source/runtime.c b/source/runtime.c index 8847f571..99d9077a 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -25,6 +25,7 @@ #define BLOB_IMPLEMENTATION #include "quickjs-internal.h" +#include // #define DUMP_BUDDY #ifdef DUMP_BUDDY @@ -11640,10 +11641,10 @@ JSValue JS_GetStack(JSContext *ctx) { JSGCRef item_ref; JS_PushGCRef(ctx, &item_ref); item_ref.val = JS_NewObject(ctx); - JS_SetPropertyStr(ctx, item_ref.val, "fn", - JS_NewString(ctx, frames[i].fn ? frames[i].fn : "")); - JS_SetPropertyStr(ctx, item_ref.val, "file", - JS_NewString(ctx, frames[i].file ? frames[i].file : "")); + JSValue fn_str = JS_NewString(ctx, frames[i].fn ? frames[i].fn : ""); + JS_SetPropertyStr(ctx, item_ref.val, "fn", fn_str); + JSValue file_str = JS_NewString(ctx, frames[i].file ? frames[i].file : ""); + JS_SetPropertyStr(ctx, item_ref.val, "file", file_str); JS_SetPropertyStr(ctx, item_ref.val, "line", JS_NewInt32(ctx, frames[i].line)); JS_SetPropertyStr(ctx, item_ref.val, "col", JS_NewInt32(ctx, frames[i].col)); JS_SetPropertyNumber(ctx, arr_ref.val, i, item_ref.val); @@ -11653,3 +11654,65 @@ JSValue JS_GetStack(JSContext *ctx) { JS_PopGCRef(ctx, &arr_ref); return result; } + +/* Crash-safe stack printer: walks JS frames and writes to stderr + using only write() (async-signal-safe). No GC allocations. + Best-effort: if the heap is corrupted, we may double-fault. */ +void JS_CrashPrintStack(JSContext *ctx) { + if (!ctx) return; + if (JS_IsNull(ctx->reg_current_frame)) return; + + static const char hdr[] = "\n--- JS Stack at crash ---\n"; + write(STDERR_FILENO, hdr, sizeof(hdr) - 1); + + JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(ctx->reg_current_frame); + uint32_t cur_pc = ctx->current_register_pc; + int is_first = 1; + int depth = 0; + + while (frame && depth < 32) { + if (!JS_IsFunction(frame->function)) break; + JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); + + if (fn->kind == JS_FUNC_KIND_REGISTER && JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code) { + JSCodeRegister *code = JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code; + uint32_t pc = is_first ? cur_pc : (uint32_t)(JS_VALUE_GET_INT(frame->address) >> 16); + + const char *name = code->name_cstr ? code->name_cstr : ""; + const char *file = code->filename_cstr ? code->filename_cstr : ""; + int line = 0; + if (code->line_table && pc < code->instr_count) + line = code->line_table[pc].line; + + /* Format: " at name (file:line)\n" using only write() */ + char buf[512]; + int pos = 0; + buf[pos++] = ' '; buf[pos++] = ' '; buf[pos++] = ' '; buf[pos++] = ' '; + buf[pos++] = 'a'; buf[pos++] = 't'; buf[pos++] = ' '; + for (const char *p = name; *p && pos < 200; p++) buf[pos++] = *p; + buf[pos++] = ' '; buf[pos++] = '('; + for (const char *p = file; *p && pos < 440; p++) buf[pos++] = *p; + buf[pos++] = ':'; + /* itoa for line number */ + if (line == 0) { + buf[pos++] = '0'; + } else { + char digits[12]; + int d = 0; + int n = line; + while (n > 0) { digits[d++] = '0' + (n % 10); n /= 10; } + for (int i = d - 1; i >= 0; i--) buf[pos++] = digits[i]; + } + buf[pos++] = ')'; buf[pos++] = '\n'; + write(STDERR_FILENO, buf, pos); + } + + if (JS_IsNull(frame->caller)) break; + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); + is_first = 0; + depth++; + } + + static const char ftr[] = "--- End JS Stack ---\n"; + write(STDERR_FILENO, ftr, sizeof(ftr) - 1); +} diff --git a/source/scheduler.c b/source/scheduler.c index 97b4736f..415d694e 100644 --- a/source/scheduler.c +++ b/source/scheduler.c @@ -716,6 +716,7 @@ void actor_turn(cell_rt *actor) if (actor->vm_suspended) { /* RESUME path: continue suspended turn under kill timer only */ + g_crash_ctx = actor->context; atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed); JS_SetPauseFlag(actor->context, 0); actor->turn_start_ns = cell_ns(); @@ -783,6 +784,8 @@ void actor_turn(cell_rt *actor) pthread_cond_signal(&engine.timer_cond); pthread_mutex_unlock(&engine.lock); + g_crash_ctx = actor->context; + if (l.type == LETTER_BLOB) { size_t size = blob_length(l.blob_data) / 8; JSValue arg = js_new_blob_stoned_copy(actor->context, @@ -816,6 +819,7 @@ void actor_turn(cell_rt *actor) actor->slow_strikes = 0; /* completed within fast timer */ ENDTURN: + g_crash_ctx = NULL; /* Invalidate any outstanding pause/kill timers for this turn */ atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed); actor->state = ACTOR_IDLE; @@ -842,6 +846,7 @@ ENDTURN: return; ENDTURN_SLOW: + g_crash_ctx = NULL; #ifdef ACTOR_TRACE fprintf(stderr, "[ACTOR_TRACE] %s: suspended mid-turn -> SLOW\n", actor->name ? actor->name : actor->id);