Commitf51778b2Recorded3 Aug 2026Repositorysigil

vm/native-bridge: review fixes — mixed-path double-pop, fourth dispatch site, cleanups

Message

From the adversarial review of the branch and the /simplify pass:

- bridgeraise popped the fired handler at dispatch even when the home guard was BYTECODE-pushed and non-tail. That home's delivery resumes the installer frame at its unconditional OPPOPHANDLER, so a native-compiled callee raising under a bytecode guard still removed an ENCLOSING guard (t-41f2 defect 3, mixed-path face). Bytecode non-tail homes now stay in-flight, symmetric with OP_RAISE.

- processerror's vm-error conversion was a FOURTH dispatch site with the temp-root/sp ordering bug ("all three sites" had the wrong denominator): the pops after the pushes left sp at handlerbp, so the rest-parameter push clobbered the closure register and the rest-list cons allocated with closure/exn unrooted. Reordered like the others.

- weh capture's malloc-failure path now truncates the global stack before degrading, instead of leaving the stale entries whose desync the helper exists to prevent.

- FRAMESMAX check hoisted before the stack pushes in OPRAISE's dispatch; dead sp bump in its rest branch removed; empty else debris removed.

- New sigil_ehconsumefired states the fired-entry invariant once (popped or flagged, never silently left live) for all four vm.c dispatch sites; fiberrestorehandlersonresume now calls the shared sigilnativewehrestore_segment instead of duplicating it.

Documented residual (pre-existing, NOT fixed here): the VM-side native-unwind consumption returns a single frame, so a native raiser sitting more than one bytecode frame below its guard thunk can resume the intermediate frame with the handler result (defect-2 shape, mixed path only). Follow-up filed in sigil-ecosystem.

Part of sigil-ecosystem t-41f2 / t-ecdf.

Changed
 packages/sigil-lib/src/native-bridge.c |  51 +++++++++++++++++++++++++++++++++++++--------------
 packages/sigil-lib/src/vm.c            | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------
 2 files changed, 90 insertions(+), 77 deletions(-)
Diff
packages/sigil-lib/src/native-bridge.cmodified
@@ -781,6 +781,12 @@ static void fiber_restore_windings_on_resume(NativeFiber *f, SigilVM *vm) {
781
static int native_weh_level_stack[SIGIL_EXCEPTION_HANDLERS_MAX];
782
static int native_weh_level_count = 0;
783
+784
/* Defined below (exported for vm.c's dcont capture); declared here for the
+785
* fiber save/restore twins. */
+786
int sigil_native_weh_capture_segment(int handler_start, int handler_count,
+787
int **out);
+788
void sigil_native_weh_restore_segment(const int *levels, int n, int base);
+789
790
/* Move the suspended fiber's own exception handlers off the shared
791
* handler stack into the fiber's save buffer, along with the matching
792
* native_weh_level_stack home-frame entries (t-79a0 residual).
@@ -907,17 +913,8 @@ static void fiber_restore_handlers_on_resume(NativeFiber *f, SigilVM *vm)
913
vm->exception_handler_count += count;
914
f->saved_handler_count = 0;
915
}
910
for (int i = 0; i < f->saved_weh_count; i++) {
911
if (native_weh_level_count >= SIGIL_EXCEPTION_HANDLERS_MAX) {
912
fprintf(stderr,
913
"FATAL: SIGIL_EXCEPTION_HANDLERS_MAX exceeded restoring "
914
"fiber weh entries (count=%d max=%d)\n",
915
native_weh_level_count, SIGIL_EXCEPTION_HANDLERS_MAX);
916
abort();
917
}
918
native_weh_level_stack[native_weh_level_count++] =
919
f->saved_weh_levels[i] + eh_base;
920
}
+916
sigil_native_weh_restore_segment(f->saved_weh_levels, f->saved_weh_count,
+917
eh_base);
918
f->saved_weh_count = 0;
919
}
920
@@ -1590,7 +1587,14 @@ int sigil_native_weh_capture_segment(int handler_start, int handler_count,
1587
}
1588
if (saved > 0) {
1589
int *buf = (int *)malloc((size_t)saved * sizeof(int));
1593
if (!buf) return 0; /* degrade: entries dropped, as pre-capture */
+1590
if (!buf) {
+1591
/* Degrade by DROPPING the entries (matching the truncation
+1592
* below) — leaving them on the stack with soon-to-be-stale
+1593
* indexes would recreate the exact desync this helper
+1594
* exists to prevent. */
+1595
native_weh_level_count = seg_base;
+1596
return 0;
+1597
}
1598
int w = 0;
1599
for (int i = 0; i < seg; i++) {
1600
int idx = native_weh_level_stack[seg_base + i];
@@ -1611,7 +1615,7 @@ void sigil_native_weh_restore_segment(const int *levels, int n, int base) {
1615
if (native_weh_level_count >= SIGIL_EXCEPTION_HANDLERS_MAX) {
1616
fprintf(stderr,
1617
"FATAL: SIGIL_EXCEPTION_HANDLERS_MAX exceeded restoring "
1614
"dcont weh entries (count=%d max=%d)\n",
+1618
"weh entries (count=%d max=%d)\n",
1619
native_weh_level_count, SIGIL_EXCEPTION_HANDLERS_MAX);
1620
abort();
1621
}
@@ -2348,7 +2352,26 @@ Value sigil_native_bridge_raise(SigilVM *vm, Value exn, bool continuable) {
2352
int handler_wind_mark = vm->exception_handlers[handler_idx].wind_mark;
2353
bool handler_pushed_by_bytecode =
2354
(vm->exception_handlers[handler_idx].push_ip != NULL);
2351
vm->exception_handler_count = handler_idx;
+2355
/* Handler-stack accounting, mirroring OP_RAISE (vm.c):
+2356
*
+2357
* - Native-pushed homes and tail homes: pop at dispatch. Native
+2358
* non-tail homes are balanced by the unwinding-flag guard in
+2359
* sigil_native_bridge_pop_handler (the home POP_HANDLER clears
+2360
* the flag instead of popping); tail homes have no POP at all.
+2361
* - BYTECODE-pushed NON-TAIL homes: the unwind is consumed on the
+2362
* VM side and delivery resumes the installer frame at its
+2363
* OP_POPHANDLER, which pops UNCONDITIONALLY. Popping here too
+2364
* removed an ENCLOSING guard — the same double-pop as t-41f2
+2365
* defect 3, reachable whenever a native-compiled callee raises
+2366
* under a bytecode guard. Keep the fired entry, marked
+2367
* in-flight, for that pop to consume. */
+2368
if (handler_pushed_by_bytecode &&
+2369
!vm->exception_handlers[handler_idx].tail && !continuable) {
+2370
vm->exception_handler_count = handler_idx + 1;
+2371
vm->exception_handlers[handler_idx].in_flight = true;
+2372
} else {
+2373
vm->exception_handler_count = handler_idx;
+2374
}
2375
2376
/* A non-continuable raise escapes every dynamic-wind entered inside
2377
* the handler's extent: run their after-thunks (innermost first)
packages/sigil-lib/src/vm.cmodified
@@ -59,6 +59,27 @@ static inline double get_time_ns(void) {
59
*
60
* Returns true on success, false if the handler stack would overflow.
61
*/
+62
/* Consume a fired exception-handler entry at dispatch time.
+63
*
+64
* Entries above idx are always dropped. The fired entry itself is either
+65
* popped with them (tail guards — no OP_POPHANDLER exists in their
+66
* continuation — and continuable raises, which keep the historical
+67
* behavior), or KEPT, marked in-flight, so the guard continuation's
+68
* OP_POPHANDLER pops exactly it instead of an enclosing guard (t-41f2
+69
* defect 3). A fired entry is always either popped or flagged — never
+70
* silently left live. Searches skip in-flight entries so a handler never
+71
* catches raises from its own extent (see SigilExceptionHandler.in_flight). */
+72
static inline void sigil__eh_consume_fired(SigilVM *vm, int idx,
+73
bool leave_in_flight)
+74
{
+75
if (leave_in_flight) {
+76
vm->exception_handler_count = idx + 1;
+77
vm->exception_handlers[idx].in_flight = true;
+78
} else {
+79
vm->exception_handler_count = idx;
+80
}
+81
}
+82
83
static bool sigil__dcont_restore_handlers(SigilVM *vm,
84
SigilDelimitedCont *dcont,
85
int saved_frame_count,
@@ -2338,7 +2359,6 @@ static Value sigil__regvm_run(SigilVM *vm)
2359
uint32_t dinst = rip[-1];
2360
uint8_t dest_a = DECODE_A(dinst);
2361
frame->bp[dest_a] = dcont_result;
2341
} else {
2362
}
2363
ip = rip;
2364
base = frame->bp;
@@ -3673,20 +3693,11 @@ static Value sigil__regvm_run(SigilVM *vm)
3693
bool handler_tail = eh->tail;
3694
size_t target_stack_mark = eh->stack_mark;
3695
int target_wind_mark = eh->wind_mark;
3676
/* Handler-stack accounting. Tail guards have no OP_POPHANDLER
3677
* in their continuation (the pop happens here); continuable
3678
* raises keep the historical pop-at-dispatch behavior. A
3679
* NON-TAIL non-continuable guard's continuation resumes at
3680
* the OP_POPHANDLER after the thunk call, so the fired entry
3681
* must STAY, marked in-flight, for that pop to consume —
3682
* otherwise it pops an enclosing guard and a later raise on
3683
* the same path escapes it (t-41f2 defect 3). */
3684
if (continuable || handler_tail) {
3685
vm->exception_handler_count = handler_idx;
3686
} else {
3687
vm->exception_handler_count = handler_idx + 1;
3688
eh->in_flight = true;
3689
}
+3696
/* Tail guards have no OP_POPHANDLER in their continuation (the
+3697
* pop happens here); continuable raises keep the historical
+3698
* pop-at-dispatch behavior. See sigil__eh_consume_fired. */
+3699
sigil__eh_consume_fired(vm, handler_idx,
+3700
!continuable && !handler_tail);
3701
3702
sigil__gc_push_temp_root(vm, handler_closure);
3703
sigil__gc_push_temp_root(vm, exn);
@@ -3919,6 +3930,15 @@ static Value sigil__regvm_run(SigilVM *vm)
3930
vm->exception_handlers[vm->exception_handler_count - 1].frame_mark > vm->frame_count)
3931
vm->exception_handler_count--;
3932
+3933
/* Frame-capacity check BEFORE touching the stack, so the
+3934
* regvm_error path is entered with no stray values above
+3935
* the unwound area. */
+3936
if (!handler_tail && vm->frame_count >= SIGIL_FRAMES_MAX) {
+3937
sigil__vm_error(vm, SIGIL_ERR_RUNTIME,
+3938
"stack overflow in exception handler");
+3939
goto regvm_error;
+3940
}
+3941
3942
/* Set up handler args on the value stack (rooted there). */
3943
push(vm, handler_closure);
3944
push(vm, exn);
@@ -3927,11 +3947,6 @@ static Value sigil__regvm_run(SigilVM *vm)
3947
if (handler_tail) {
3948
frame = &vm->frames[vm->frame_count - 1];
3949
} else {
3930
if (vm->frame_count >= SIGIL_FRAMES_MAX) {
3931
sigil__vm_error(vm, SIGIL_ERR_RUNTIME,
3932
"stack overflow in exception handler");
3933
goto regvm_error;
3934
}
3950
frame = &vm->frames[vm->frame_count++];
3951
}
3952
@@ -3941,11 +3956,11 @@ static Value sigil__regvm_run(SigilVM *vm)
3956
frame->bp = handler_bp;
3957
frame->srcloc = vm->current_srcloc;
3958
3944
/* Handle rest parameter */
+3959
/* Handle rest parameter (sp is set to the full register
+3960
* window just below, so no interim sp bump is needed). */
3961
if (handler_clo->code->has_rest && handler_clo->code->arity == 0) {
3962
Value rest = sigil_cons(vm, exn, SIGIL_EMPTY);
3963
handler_bp[1] = rest;
3948
vm->sp = handler_bp + 2;
3964
} else if (handler_clo->code->has_rest && handler_clo->code->arity == 1) {
3965
push(vm, SIGIL_EMPTY);
3966
}
@@ -4481,15 +4496,7 @@ static Value sigil__regvm_run(SigilVM *vm)
4496
bool handler_tail = eh->tail;
4497
size_t target_stack_mark = eh->stack_mark;
4498
int target_wind_mark = eh->wind_mark;
4484
/* Tail: pop the fired entry (no OP_POPHANDLER runs for it).
4485
* Non-tail: keep it, in-flight, for the continuation's
4486
* OP_POPHANDLER — see OP_RAISE. */
4487
if (handler_tail) {
4488
vm->exception_handler_count = eh_idx;
4489
} else {
4490
vm->exception_handler_count = eh_idx + 1;
4491
eh->in_flight = true;
4492
}
+4499
sigil__eh_consume_fired(vm, eh_idx, !handler_tail);
4500
4501
sigil__gc_push_temp_root(vm, handler_closure);
4502
sigil__gc_push_temp_root(vm, exn);
@@ -6825,19 +6832,10 @@ static Value sigil__process_abort(SigilVM *vm, int abort_argc,
6832
vm->exception_handler_count - captured_handler_start;
6833
dcont->handler_count = captured_handler_count;
6834
6828
/* Move the captured segment's native weh shadow entries with it.
6829
* Native non-tail guards record their handler's ABSOLUTE stack
6830
* index at push time (native_weh_level_stack) so the home
6831
* POP_HANDLER can be recognised during an unwind. Relocating the
6832
* handlers without their shadow entries left those indexes
6833
* stale: after one suspend/resume that re-based the segment, the
6834
* fired guard's POP_HANDLER no longer matched the recorded
6835
* unwind target, sigil_native_exception_unwinding stayed set
6836
* forever, and every subsequent native call silently
6837
* short-circuited (t-ecdf: kiln's static controller dying with
6838
* no log line, 7/9 setup-failure cases). Runs even with zero
6839
* captured handlers: entries above the segment shadow handlers
6840
* this unwind discards, and must be dropped either way. */
+6835
/* Move the captured segment's native weh shadow entries with it
+6836
* (full story: SigilDelimitedCont.weh_levels in sigil.h). Runs
+6837
* even with zero captured handlers: entries above the segment
+6838
* shadow handlers this unwind discards, and must drop with it. */
6839
dcont->weh_count = sigil_native_weh_capture_segment(
6840
captured_handler_start, captured_handler_count,
6841
&dcont->weh_levels);
@@ -7089,14 +7087,7 @@ process_error:
7087
bool handler_tail = peh->tail;
7088
size_t target_stack_mark = peh->stack_mark;
7089
int target_wind_mark = peh->wind_mark;
7092
/* Tail: pop the fired entry. Non-tail: keep it in-flight
7093
* for the continuation's OP_POPHANDLER — see OP_RAISE. */
7094
if (handler_tail) {
7095
vm->exception_handler_count = peh_idx;
7096
} else {
7097
vm->exception_handler_count = peh_idx + 1;
7098
peh->in_flight = true;
7099
}
+7090
sigil__eh_consume_fired(vm, peh_idx, !handler_tail);
7091
7092
sigil__gc_push_temp_root(vm, handler_closure);
7093
sigil__gc_push_temp_root(vm, exn);
@@ -7418,14 +7409,7 @@ process_error:
7409
bool handler_tail = eh->tail;
7410
size_t target_stack_mark = eh->stack_mark;
7411
int target_wind_mark = eh->wind_mark;
7421
/* Tail: pop the fired entry. Non-tail: keep it in-flight for
7422
* the continuation's OP_POPHANDLER — see OP_RAISE. */
7423
if (handler_tail) {
7424
vm->exception_handler_count = eh_idx;
7425
} else {
7426
vm->exception_handler_count = eh_idx + 1;
7427
eh->in_flight = true;
7428
}
+7412
sigil__eh_consume_fired(vm, eh_idx, !handler_tail);
7413
7414
sigil__gc_push_temp_root(vm, handler_closure);
7415
sigil__gc_push_temp_root(vm, exn);
@@ -7445,6 +7429,15 @@ process_error:
7429
7430
SigilClosure *handler_clo = (SigilClosure *)sigil_as_ptr(handler_closure);
7431
+7432
/* Pop the temp roots BEFORE moving sp: temp roots live ON the
+7433
* value stack. The old order (pop after the pushes below) left
+7434
* vm->sp at handler_bp — the rest-parameter push then clobbered
+7435
* the handler-closure register, and the rest-list cons
+7436
* allocated while closure/exn sat ABOVE sp, unrooted. Same
+7437
* class as the other three dispatch sites. */
+7438
sigil__gc_pop_temp_root(vm); /* exn */
+7439
sigil__gc_pop_temp_root(vm); /* handler_closure */
+7440
7441
int target_fc = (vm->frame_count < target_frame_mark)
7442
? vm->frame_count : target_frame_mark;
7443
vm->frame_count = target_fc;
@@ -7468,9 +7461,6 @@ process_error:
7461
push(vm, exn);
7462
Value *handler_bp = vm->sp - 2;
7463
7471
sigil__gc_pop_temp_root(vm); /* exn */
7472
sigil__gc_pop_temp_root(vm); /* handler_closure */
7473
7464
if (handler_tail) {
7465
SigilFrame *frame = &vm->frames[vm->frame_count - 1];
7466
frame->closure = handler_clo;