kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 9fd3adda7106605e23b78d7c85f1237f1ea67801
parent cb8e08d7d89c0ce544883b60d2d04a5c883df51e
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  6 Jun 2026 04:53:54 -0700

Fix nested panic recovery

Diffstat:
Mdoc/plan/TODO.md | 9---------
Mmk/test.mk | 5++++-
Mmk/test_unit.mk | 2++
Msrc/api/compile.c | 20++++++++++----------
Msrc/api/frontend.c | 10+++++-----
Msrc/api/link.c | 10+++++-----
Msrc/api/object_file.c | 40++++++++++++++++++++++++++--------------
Msrc/core/core.c | 28+++++++++++++++++++++-------
Msrc/core/core.h | 21++++++++++++++-------
Msrc/emu/emu.c | 30+++++++++++++++---------------
Msrc/link/link_jit.c | 10+++++-----
Atest/api/panic_recovery_test.c | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 178 insertions(+), 78 deletions(-)

diff --git a/doc/plan/TODO.md b/doc/plan/TODO.md @@ -4,12 +4,3 @@ This file is an open-backlog catalog, not a completion ledger. When an item is fixed, remove it instead of checking it off or keeping a closed entry. Add new deferred fixes below as they are discovered. - -## ASan host build: some fatal diagnostics SEGV during `compiler_panic` recovery - -While reproducing the former x64 inline-asm fatal on the ASan host build, the -diagnostic printed, then `compiler_panicv`'s `longjmp` (`src/core/core.c:187`) -faulted in `_longjmp`, turning a user-facing `fatal:` into SIGABRT/SEGV. The -inline-asm trigger is fixed, but the panic recovery path still needs a focused -expected-fatal regression and an audit of panic save/restore lifetime under the -sanitized hosted driver. diff --git a/mk/test.mk b/mk/test.mk @@ -435,17 +435,20 @@ CG_FP_CMP_TEST_BIN = build/test/cg_fp_cmp_test STRENGTH_REDUCE_TEST_BIN = build/test/strength_reduce_test TARGET_TEST_BIN = build/test/target_test HASH_TEST_BIN = build/test/hash_test +PANIC_RECOVERY_TEST_BIN = build/test/panic_recovery_test ABI_CLASSIFY_TEST_BIN = build/test/abi_classify_test IR_RECORDER_TEST_BIN = build/test/ir_recorder_test NATIVE_DIRECT_TARGET_TEST_BIN = build/test/native_direct_target_test test-cg-api: $(TARGET_TEST_BIN) $(CG_API_TEST_BIN) $(CG_SWITCH_TEST_BIN) \ - $(CG_FP_CMP_TEST_BIN) $(STRENGTH_REDUCE_TEST_BIN) + $(CG_FP_CMP_TEST_BIN) $(STRENGTH_REDUCE_TEST_BIN) \ + $(PANIC_RECOVERY_TEST_BIN) $(TARGET_TEST_BIN) $(CG_API_TEST_BIN) $(CG_SWITCH_TEST_BIN) $(CG_FP_CMP_TEST_BIN) $(STRENGTH_REDUCE_TEST_BIN) + $(PANIC_RECOVERY_TEST_BIN) test-hash: $(HASH_TEST_BIN) $(HASH_TEST_BIN) diff --git a/mk/test_unit.mk b/mk/test_unit.mk @@ -31,11 +31,13 @@ UNIT_CFLAGS_INTERNAL = $(HOST_CFLAGS) -Iinclude -Isrc -Itest UNIT_TESTS_PUBLIC := \ ar_test target_test cg_api_test cg_switch_test cg_fp_cmp_test hash_test \ + panic_recovery_test \ rv64_jit_test rv32_jit_test aa64_inline_test rv64_inline_test x64_inline_test \ strength_reduce_test ar_test_SRC := test/ar/ar_test.c target_test_SRC := test/api/target_test.c hash_test_SRC := test/api/hash_test.c +panic_recovery_test_SRC := test/api/panic_recovery_test.c cg_api_test_SRC := test/api/cg_type_test.c cg_switch_test_SRC := test/api/cg_switch_test.c cg_fp_cmp_test_SRC := test/api/cg_fp_cmp_test.c diff --git a/src/api/compile.c b/src/api/compile.c @@ -302,7 +302,7 @@ static KitStatus kit_frontend_compile_obj(KitFrontend* frontend, const KitSourceInput* input, KitObjBuilder* out) { Compiler* c; - PanicSave saved; + PanicFrame panic; KitStatus st; if (!frontend || !frontend->c || !frontend->vtable || !frontend->state || @@ -312,15 +312,15 @@ static KitStatus kit_frontend_compile_obj(KitFrontend* frontend, if (input->lang != frontend->lang) return KIT_INVALID; if (!frontend->vtable->compile_obj) return KIT_UNSUPPORTED; c = (Compiler*)frontend->c; - compiler_panic_save(c, &saved); - if (setjmp(c->panic)) { + compiler_panic_push(c, &panic); + if (setjmp(panic.env)) { /* A genuine internal panic (CG/backend) longjmp'd here. Run cleanups, then * roll back any durable frontend state staged during this compile, and * propagate as a soft error. Ordinary diagnostic failures do NOT take this * path: the frontend returns KIT_ERR below without panicking. */ compiler_run_cleanups(c); kit_frontend_abort(frontend); - compiler_panic_restore(c, &saved); + compiler_panic_pop(c, &panic); return KIT_ERR; } validate_bytes(c, input); @@ -333,7 +333,7 @@ static KitStatus kit_frontend_compile_obj(KitFrontend* frontend, * frontend is left exactly as it was before this compile. On success the * transaction is left open for the caller to commit or abort. */ if (st != KIT_OK) kit_frontend_abort(frontend); - compiler_panic_restore(c, &saved); + compiler_panic_pop(c, &panic); return st; } @@ -342,7 +342,7 @@ static KitStatus kit_frontend_compile_cg(KitFrontend* frontend, const KitSourceInput* input, KitCg* cg) { Compiler* c; - PanicSave saved; + PanicFrame panic; KitStatus st; if (!frontend || !frontend->c || !frontend->vtable || !frontend->state || @@ -355,11 +355,11 @@ static KitStatus kit_frontend_compile_cg(KitFrontend* frontend, return KIT_UNSUPPORTED; } c = (Compiler*)frontend->c; - compiler_panic_save(c, &saved); - if (setjmp(c->panic)) { + compiler_panic_push(c, &panic); + if (setjmp(panic.env)) { compiler_run_cleanups(c); kit_frontend_abort(frontend); - compiler_panic_restore(c, &saved); + compiler_panic_pop(c, &panic); return KIT_ERR; } validate_bytes(c, input); @@ -370,7 +370,7 @@ static KitStatus kit_frontend_compile_cg(KitFrontend* frontend, metrics_scope_end(c, "compile.frontend"); metrics_scope_end(c, "compile.tu"); if (st != KIT_OK) kit_frontend_abort(frontend); - compiler_panic_restore(c, &saved); + compiler_panic_pop(c, &panic); return st; } diff --git a/src/api/frontend.c b/src/api/frontend.c @@ -6,17 +6,17 @@ #include "core/metrics.h" KitStatus kit_frontend_run(KitCompiler* c, KitFrontendRunFn fn, void* user) { - PanicSave saved; + PanicFrame panic; KitStatus rc; if (!c || !fn) return KIT_INVALID; - compiler_panic_save(c, &saved); - if (setjmp(c->panic)) { + compiler_panic_push(c, &panic); + if (setjmp(panic.env)) { compiler_run_cleanups(c); - compiler_panic_restore(c, &saved); + compiler_panic_pop(c, &panic); return KIT_ERR; } rc = fn(c, user); - compiler_panic_restore(c, &saved); + compiler_panic_pop(c, &panic); return rc; } diff --git a/src/api/link.c b/src/api/link.c @@ -29,18 +29,18 @@ KitJit* kit_jit_from_image(LinkImage*); static KitStatus link_session_guard(KitLinkSession* s, void (*fn)(KitLinkSession*, void*), void* arg) { - PanicSave saved; + PanicFrame panic; if (!s || !s->c || !s->linker || !fn) return KIT_INVALID; - compiler_panic_save(s->c, &saved); - if (setjmp(s->c->panic)) { + compiler_panic_push(s->c, &panic); + if (setjmp(panic.env)) { compiler_run_cleanups(s->c); s->linker = NULL; s->image = NULL; - compiler_panic_restore(s->c, &saved); + compiler_panic_pop(s->c, &panic); return KIT_ERR; } fn(s, arg); - compiler_panic_restore(s->c, &saved); + compiler_panic_pop(s->c, &panic); return KIT_OK; } diff --git a/src/api/object_file.c b/src/api/object_file.c @@ -68,14 +68,20 @@ KitStatus kit_obj_open(const KitContext* ctx, KitSlice name, h->free(h, f, sizeof(*f)); return st; } - if (setjmp(f->compiler.panic)) { - compiler_run_cleanups(&f->compiler); - compiler_fini(&f->compiler); - kit_target_free(f->resolved_target); - h->free(h, f, sizeof(*f)); - return KIT_MALFORMED; + { + PanicFrame panic; + compiler_panic_push(&f->compiler, &panic); + if (setjmp(panic.env)) { + compiler_run_cleanups(&f->compiler); + compiler_panic_pop(&f->compiler, &panic); + compiler_fini(&f->compiler); + kit_target_free(f->resolved_target); + h->free(h, f, sizeof(*f)); + return KIT_MALFORMED; + } + f->ob = impl->read(&f->compiler, name.s, input->data, input->len); + compiler_panic_pop(&f->compiler, &panic); } - f->ob = impl->read(&f->compiler, name.s, input->data, input->len); if (!f->ob) { compiler_fini(&f->compiler); kit_target_free(f->resolved_target); @@ -574,14 +580,20 @@ KitObjFile* kit_objfile_internal_new(const KitContext* ctx, return NULL; } } - if (setjmp(f->compiler.panic)) { - compiler_run_cleanups(&f->compiler); - compiler_fini(&f->compiler); - kit_target_free(f->resolved_target); - h->free(h, f, sizeof(*f)); - return NULL; + { + PanicFrame panic; + compiler_panic_push(&f->compiler, &panic); + if (setjmp(panic.env)) { + compiler_run_cleanups(&f->compiler); + compiler_panic_pop(&f->compiler, &panic); + compiler_fini(&f->compiler); + kit_target_free(f->resolved_target); + h->free(h, f, sizeof(*f)); + return NULL; + } + f->ob = obj_new(&f->compiler); + compiler_panic_pop(&f->compiler, &panic); } - f->ob = obj_new(&f->compiler); if (!f->ob) { compiler_fini(&f->compiler); kit_target_free(f->resolved_target); diff --git a/src/core/core.c b/src/core/core.c @@ -13,9 +13,6 @@ #include "core/heap.h" #include "core/pool.h" -_Static_assert(sizeof(jmp_buf) <= COMPILER_PANIC_BYTES, - "Compiler panic save buffer is too small for jmp_buf"); - #if defined(__GNUC__) || defined(__clang__) || defined(__kit__) __attribute__((weak)) #endif @@ -165,12 +162,28 @@ void compiler_run_cleanups(Compiler* c) { } } -void compiler_panic_save(Compiler* c, PanicSave* out) { - memcpy(out->buf, c->panic, sizeof(jmp_buf)); +void compiler_panic_push(Compiler* c, PanicFrame* frame) { + frame->prev = c->panic_frame; + c->panic_frame = frame; } -void compiler_panic_restore(Compiler* c, const PanicSave* saved) { - memcpy(c->panic, saved->buf, sizeof(jmp_buf)); +void compiler_panic_pop(Compiler* c, PanicFrame* frame) { + PanicFrame** link; + if (!c || !frame) return; + if (c->panic_frame == frame) { + c->panic_frame = frame->prev; + frame->prev = NULL; + return; + } + link = &c->panic_frame; + while (*link) { + if (*link == frame) { + *link = frame->prev; + frame->prev = NULL; + return; + } + link = &(*link)->prev; + } } void compiler_panic(Compiler* c, SrcLoc loc, const char* fmt, ...) { @@ -184,5 +197,6 @@ void compiler_panicv(Compiler* c, SrcLoc loc, const char* fmt, va_list ap) { if (c->ctx && c->ctx->diag && c->ctx->diag->emit) { c->ctx->diag->emit(c->ctx->diag, KIT_DIAG_FATAL, loc, fmt, ap); } + if (c->panic_frame) longjmp(c->panic_frame->env, 1); longjmp(c->panic, 1); } diff --git a/src/core/core.h b/src/core/core.h @@ -118,8 +118,12 @@ const SourceInclude* source_depiter_next(SourceDepIter*); void source_depiter_free(SourceDepIter*); typedef struct CompilerCleanup CompilerCleanup; +typedef struct PanicFrame PanicFrame; -#define COMPILER_PANIC_BYTES 256u +struct PanicFrame { + jmp_buf env; + PanicFrame* prev; +}; struct KitCompiler { const KitContext* ctx; @@ -147,9 +151,15 @@ struct KitCompiler { * this program for the threaded interpreter. Set by * kit_interp_program_attach; borrowed (the caller owns the program). */ void* interp_sink; + PanicFrame* panic_frame; /* Keep jmp_buf last: its size comes from the including C environment * (host libc for some tests, rt/include for libkit), and must not shift - * the offsets of the fields above across those builds. */ + * the offsets of the fields above across those builds. + * + * New nested panic boundaries should use PanicFrame, not this legacy slot: + * copying jmp_buf bytes is not a portable way to save nested handlers and + * can corrupt sanitizer-managed jump state. The slot stays for older + * internal tests/tools that install one direct setjmp on a compiler. */ jmp_buf panic; }; @@ -163,10 +173,7 @@ void compiler_run_cleanups(Compiler*); _Noreturn void compiler_panic(Compiler*, SrcLoc, const char* fmt, ...); _Noreturn void compiler_panicv(Compiler*, SrcLoc, const char* fmt, va_list); -typedef struct PanicSave { - unsigned char buf[COMPILER_PANIC_BYTES]; -} PanicSave; -void compiler_panic_save(Compiler*, PanicSave* out); -void compiler_panic_restore(Compiler*, const PanicSave* saved); +void compiler_panic_push(Compiler*, PanicFrame*); +void compiler_panic_pop(Compiler*, PanicFrame*); #endif diff --git a/src/emu/emu.c b/src/emu/emu.c @@ -223,7 +223,7 @@ static KitStatus emu_resolve_config(Compiler* c, const KitEmuOptions* opts, } KitStatus kit_emu_new(KitCompiler* c, const KitEmuOptions* opts, KitEmu** out) { - PanicSave saved; + PanicFrame panic; Heap* heap; KitEmu* e; EmuResolvedConfig resolved; @@ -234,10 +234,10 @@ KitStatus kit_emu_new(KitCompiler* c, const KitEmuOptions* opts, KitEmu** out) { if (!c || !opts || !out) return KIT_INVALID; if (!opts->guest_bytes.data || opts->guest_bytes.len == 0) return KIT_INVALID; - compiler_panic_save(c, &saved); - if (setjmp(c->panic)) { + compiler_panic_push(c, &panic); + if (setjmp(panic.env)) { compiler_run_cleanups(c); - compiler_panic_restore(c, &saved); + compiler_panic_pop(c, &panic); return KIT_ERR; } @@ -355,7 +355,7 @@ KitStatus kit_emu_new(KitCompiler* c, const KitEmuOptions* opts, KitEmu** out) { "emu: interpreter mode not enabled in this build"); #endif - compiler_panic_restore(c, &saved); + compiler_panic_pop(c, &panic); *out = e; return KIT_OK; } @@ -559,7 +559,7 @@ static void* translate_block(KitEmu* e, u64 guest_pc) { } void* kit_emu_lookup(KitEmu* e, uint64_t guest_pc) { - PanicSave saved; + PanicFrame panic; void* entry; if (!e) return NULL; @@ -579,16 +579,16 @@ void* kit_emu_lookup(KitEmu* e, uint64_t guest_pc) { if (ensure_runtime(e) != KIT_OK) return NULL; - compiler_panic_save(e->c, &saved); - if (setjmp(e->c->panic)) { + compiler_panic_push(e->c, &panic); + if (setjmp(panic.env)) { compiler_run_cleanups(e->c); - compiler_panic_restore(e->c, &saved); + compiler_panic_pop(e->c, &panic); return NULL; } entry = translate_block(e, guest_pc); - compiler_panic_restore(e->c, &saved); + compiler_panic_pop(e->c, &panic); return entry; } @@ -633,7 +633,7 @@ static u64 emu_interp_run_block(KitEmu* e, KitInterpFunc* ifn, #endif KitStatus kit_emu_step(KitEmu* e, uint32_t nblocks) { - PanicSave saved; + PanicFrame panic; uint32_t i; KitStatus st; @@ -642,10 +642,10 @@ KitStatus kit_emu_step(KitEmu* e, uint32_t nblocks) { st = ensure_runtime(e); if (st != KIT_OK) return st; - compiler_panic_save(e->c, &saved); - if (setjmp(e->c->panic)) { + compiler_panic_push(e->c, &panic); + if (setjmp(panic.env)) { compiler_run_cleanups(e->c); - compiler_panic_restore(e->c, &saved); + compiler_panic_pop(e->c, &panic); return KIT_ERR; } @@ -688,7 +688,7 @@ KitStatus kit_emu_step(KitEmu* e, uint32_t nblocks) { } } - compiler_panic_restore(e->c, &saved); + compiler_panic_pop(e->c, &panic); return KIT_OK; } diff --git a/src/link/link_jit.c b/src/link/link_jit.c @@ -1374,7 +1374,7 @@ static void jit_append_obj_inner(KitJit* jit, ObjBuilder* ob) { KitStatus kit_jit_publish(KitJit* jit, const KitJitPublishOptions* opts, KitJitPublishResult* result) { - PanicSave saved; + PanicFrame panic; Compiler* c; KitLinkSession* link; u32 i; @@ -1386,8 +1386,8 @@ KitStatus kit_jit_publish(KitJit* jit, const KitJitPublishOptions* opts, link = opts->link; if (link->non_obj_inputs) return KIT_UNSUPPORTED; c = jit->c; - compiler_panic_save(c, &saved); - if (setjmp(c->panic)) { + compiler_panic_push(c, &panic); + if (setjmp(panic.env)) { /* A failed append (e.g. duplicate global) panics from * jit_append_obj_inner. compiler_run_cleanups fires the borrowed link * session's deferred linker_cleanup, which frees its Linker; null the @@ -1396,12 +1396,12 @@ KitStatus kit_jit_publish(KitJit* jit, const KitJitPublishOptions* opts, compiler_run_cleanups(c); link->linker = NULL; link->image = NULL; - compiler_panic_restore(c, &saved); + compiler_panic_pop(c, &panic); return KIT_ERR; } for (i = 0; i < link->npublish_objs; ++i) jit_append_obj_inner(jit, link->publish_objs[i]); - compiler_panic_restore(c, &saved); + compiler_panic_pop(c, &panic); if (result) result->generation = jit->generation; return KIT_OK; } diff --git a/test/api/panic_recovery_test.c b/test/api/panic_recovery_test.c @@ -0,0 +1,71 @@ +/* panic_recovery_test - nested public panic boundaries under sanitizers. */ + +#include <kit/core.h> +#include <kit/frontend.h> +#include <string.h> + +#include "lib/kit_unit.h" + +static KitUnit g_u; +#define EXPECT(c, ...) CU_EXPECT(&g_u, c, __VA_ARGS__) + +typedef struct PanicRecovery { + int inner_ran; + int outer_recovered; +} PanicRecovery; + +static KitSrcLoc no_loc(void) { + KitSrcLoc loc; + loc.file_id = 0; + loc.line = 0; + loc.col = 0; + return loc; +} + +static KitStatus inner_fatal(KitCompiler* c, void* user) { + PanicRecovery* r = (PanicRecovery*)user; + r->inner_ran++; + kit_frontend_fatal(c, no_loc(), "inner expected fatal"); +} + +static KitStatus outer_then_fatal(KitCompiler* c, void* user) { + PanicRecovery* r = (PanicRecovery*)user; + KitStatus st = kit_frontend_run(c, inner_fatal, r); + EXPECT(st == KIT_ERR, "inner fatal recovered as KIT_ERR"); + EXPECT(strstr(g_u.last_diag, "inner expected fatal") != NULL, + "inner fatal diagnostic captured"); + + r->outer_recovered++; + kit_frontend_fatal(c, no_loc(), "outer expected fatal"); +} + +static void check_nested_recovery(void) { + KitCompiler* c = NULL; + PanicRecovery r; + KitStatus st; + + memset(&r, 0, sizeof r); + EXPECT(kit_unit_compiler_new( + &g_u, kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &c) == KIT_OK, + "compiler init"); + if (!c) return; + + g_u.suppress_fatal = 1; + st = kit_frontend_run(c, outer_then_fatal, &r); + g_u.suppress_fatal = 0; + + EXPECT(st == KIT_ERR, "outer fatal recovered as KIT_ERR"); + EXPECT(r.inner_ran == 1, "inner body ran once"); + EXPECT(r.outer_recovered == 1, "outer continued after inner recovery"); + EXPECT(strstr(g_u.last_diag, "outer expected fatal") != NULL, + "outer fatal diagnostic captured"); + kit_compiler_free(c); +} + +int main(void) { + kit_unit_init(&g_u); + check_nested_recovery(); + kit_unit_summary(&g_u, "panic_recovery_test"); + return kit_unit_status(&g_u); +}