You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to the crash triage that produced #50, #52, #54, #55, #56, #59 and #60. The containment fix for those is on main and ships in v0.2.1-beta.4; this issue tracks the underlying defect it contains rather than removes.
The defect
SnapHak's UI runs on its own worker thread (created in ui_bridge.c) and the SnapStack decl-edit operations commit inline on that thread, calling straight into engine code (apply_engine.c, slot_apply_sync). That is the shape behind the faults reported in #56 and #59.
beta.4 stops those faults from killing the process — the fault shield no longer converts them into an unhandled cross-thread exception, and the apply path's own __except guards can absorb them. But the faults still occur. This issue is about not having them in the first place.
Why the obvious fix is wrong
Deferring the commit to the main thread was the design once. It was removed on 2026-07-12 because it crashed, and the inline version that replaced it was a deliberate fix.
The recorded reason for that crash — that the decl-source block became "double-owned" across two threads — does not survive re-reading the code. ae_apply_one allocates the block and hands it to exactly one owner, on whichever single thread runs it. There is no second owner to perform a second free.
What actually differed between the two designs is which heap the block lands in:
Mem_Alloc's ambient-scope lookup is gated on being the main thread.
On the main thread with the editor up, the ambient scope is the map heap — which is destroyed wholesale at the next map load, leaving live-looking pointers into unmapped pages. A later free reads a destroyed or recycled header, which is exactly Memory corruption before block!.
Off the main thread the lookup silently no-ops and the allocation falls through to the process heap, which survives.
So the current inline design appears to work partly by accident: being on the "wrong" thread is what gives that block a heap that outlives the map.
Consequence: simply marshalling the work to the main thread would restore the correct thread and silently move the block back into the map heap, reproducing the 2026-07-12 crash. Doing it atomically does not help. The move has to explicitly pin that allocation to a surviving heap — ae_push_heap_global() / ae_pop_heap(), already used by a neighbouring operation in the same file.
What settles it
beta.4 ships a one-shot provenance probe. A single normal session that performs a decl edit writes one line to sh_backend.log:
thread=UI(off-main) ... heap=PROCESS confirms the reading above, after which the fix is small and evidenced instead of a guess. If it reports otherwise, the analysis needs revisiting before anything moves.
Then
Run the probe, record the result here.
If confirmed: move serialize + commit onto a main-thread execution point as one unit, with the commit's allocation explicitly pinned to a surviving heap.
Verify the 2026-07-12 signature does not return — specifically a play → teardown cycle after a decl edit.
commands.c calls the frontend's thread "the MAIN (UI) thread" and warns that heavy engine work "must run on the main thread, never the console thread". Conflating UI thread with DOOM main thread is very likely how this survived review. Worth correcting that comment.
The reflection context these operations use is a process-global singleton reachable from any thread, so reflect resolving is not the constraint. An older comment saying "the cause is the THREAD" was itself retracted later; the real cause in that instance was uninitialized stack buffers.
Serialize already runs on the main thread today for the kind=3 path, so the serialize half is not a blocker.
Follow-up to the crash triage that produced #50, #52, #54, #55, #56, #59 and #60. The containment fix for those is on
mainand ships in v0.2.1-beta.4; this issue tracks the underlying defect it contains rather than removes.The defect
SnapHak's UI runs on its own worker thread (created in
ui_bridge.c) and the SnapStack decl-edit operations commit inline on that thread, calling straight into engine code (apply_engine.c,slot_apply_sync). That is the shape behind the faults reported in #56 and #59.beta.4 stops those faults from killing the process — the fault shield no longer converts them into an unhandled cross-thread exception, and the apply path's own
__exceptguards can absorb them. But the faults still occur. This issue is about not having them in the first place.Why the obvious fix is wrong
Deferring the commit to the main thread was the design once. It was removed on 2026-07-12 because it crashed, and the inline version that replaced it was a deliberate fix.
The recorded reason for that crash — that the decl-source block became "double-owned" across two threads — does not survive re-reading the code.
ae_apply_oneallocates the block and hands it to exactly one owner, on whichever single thread runs it. There is no second owner to perform a second free.What actually differed between the two designs is which heap the block lands in:
Mem_Alloc's ambient-scope lookup is gated on being the main thread.Memory corruption before block!.So the current inline design appears to work partly by accident: being on the "wrong" thread is what gives that block a heap that outlives the map.
Consequence: simply marshalling the work to the main thread would restore the correct thread and silently move the block back into the map heap, reproducing the 2026-07-12 crash. Doing it atomically does not help. The move has to explicitly pin that allocation to a surviving heap —
ae_push_heap_global()/ae_pop_heap(), already used by a neighbouring operation in the same file.What settles it
beta.4 ships a one-shot provenance probe. A single normal session that performs a decl edit writes one line to
sh_backend.log:thread=UI(off-main) ... heap=PROCESSconfirms the reading above, after which the fix is small and evidenced instead of a guess. If it reports otherwise, the analysis needs revisiting before anything moves.Then
Notes for whoever picks this up
commands.ccalls the frontend's thread "the MAIN (UI) thread" and warns that heavy engine work "must run on the main thread, never the console thread". Conflating UI thread with DOOM main thread is very likely how this survived review. Worth correcting that comment.