commit 9fd3adda7106605e23b78d7c85f1237f1ea67801
parent cb8e08d7d89c0ce544883b60d2d04a5c883df51e
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 6 Jun 2026 04:53:54 -0700
Fix nested panic recovery
Diffstat:
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);
+}