Skip to content

fix(jsc): retry traps after foreign-stack suspension refusal - #25

Merged
steipete merged 6 commits into
openclaw/release-5718a6ecfrom
fix/foreign-stack-trap-suspension
Oct 9, 2026
Merged

steipete merged 6 commits into
openclaw/release-5718a6ecfrom
fix/foreign-stack-trap-suspension

Conversation

@steipete

@steipete steipete commented Oct 9, 2026 •

Copy link
Copy Markdown

A Worker blocked in a native call on a foreign stack can deadlock every other VM's collector on Linux. The asynchronous trap sender holds WTF's process-wide suspension lock while Thread::suspend() retries an out-of-bounds stack-pointer refusal indefinitely. If the native call waits for the parent, and parent GC needs that lock before running its release callback, none can progress.

Add a distinct temporary-refusal result for one suspension attempt and use it only for asynchronous VM trap sampling. The existing work-queue retry releases the lock first; termination and stop-the-world traps remain pending. Blocking conservative GC suspension still requires a valid snapshot. Mach suspension retains its existing operation. Failed attempts do not invoke the inspection callback, change suspension nesting, or classify a live thread as exited.

The native Linux regression parks a VM-owned host function on a guarded custom stack, requests termination, observes the trap suspension signal, and requires parent full GC to finish before releasing the callback. It checks protected-root survival, canonical termination delivery, temporary refusal, later delivery, and nested suspension/resume. The Linux engine artifact check runs it. General same-VM foreign-stack scanning and the sampling profiler's separate blocking path remain outside this change.

Validation

Check Result
Original Koffi 3.3.1 termination probe, released Bun 42bd 134 pass, 66 timeout / 200 processes
Same probe, Node 24.21.0 200/200 pass
Same probe, Bun 42bd rebuilt with this owner patch 200/200 pass, zero timeout
Native regression, original engine Expected 30-second deadlock alarm; matching all-thread lock-cycle capture
Native regression, patched engine Both checks pass
Full-engine ASAN regression Pass; LLVM's standard incomplete makecontext/swapcontext-support warning retained
Full-engine TSAN regression Both behavior checks complete; exit 66 with 30 pre-existing race reports, not a clean sanitizer pass

Each Koffi process performs ten sequential Worker handshakes and retains its 120-second deadline. All arms use the same fixed schedule: 33 sequential processes, then 167 at four concurrent processes. All completed observations were retained. One baseline timeout includes controller-transfer overhead and is not used as timing evidence. The prebuilt baseline and rebuilt candidate differ in compiler settings; the native before/after pair uses identical compiler settings and supplies the controlled causal comparison.

Fresh symbolized captures use the matching released runtime/profile ELF build ID cfda5bddca52aafc395a18d608a9abadcbc82afe. A separate bounds diagnostic places the blocked Worker SP at 0x766e380ffe78, outside its registered stack (0x766e39bff000, 0x766e39fff000].

TSAN initially could not enter the test under Docker's default ASLR restriction. Running the identical binary with process-local ASLR disabled exposed eight unique report signatures in existing GC/thread code. A separately rebuilt unpatched engine running ordinary two-VM full GC, without traps or foreign stacks, reproduced all eight signatures (66 reports, exit 66). No suppressions, reduced GC concurrency, or timeout changes were used. This establishes baseline attribution for the observed reports, not general race freedom. Both nonzero results remain recorded.

P2 scoped review is clean; WebKit style reports 0 errors across all ten changed files. Exact-head checks: engine checks, Linux ARM64. All five checks passed on the final combined head. Linux paired qualification passed 46/46 result entries in each arm, sync passed 85/85, and both Linux architectures passed 1,779 stress and 1,639 module results. The native suspension regression passed in the final Linux build. The 600-process matrix and paired native/sanitizer proof were collected at isolated head 346ba3317411e1cc5967b0d3f5b7d70e5a290d13. Integration head 436d2b3058105c76334391c65c085b691c024c91 adds the landed WebKit#24 release base; all six suspension/trap source files and the native regression are byte-identical. The only manual merge resolution preserves both sets of CMake test targets. Final CI passed against that combined source state.

Upstream and integration

Related upstream work: WebKit#65562 (bug 315453) and oven-sh#235, oven-sh#741, oven-sh#742 handle alternate signal stacks or saved contexts in WTF signal handlers. Those still reject an interrupted SP genuinely on an unregistered foreign stack, such as Koffi's assembly stack switch; none supplies this best-effort trap-sampling contract.

This draft targets openclaw/release-5718a6ec. The production patch applies to openclaw/main at fbc05476b051c7d65d6f2c333f8705489353d6a8; that port still needs equivalent regression/build wiring. No Bun runtime adapter change is needed. The integration artifact is unpublished, and no consumer pin, merge or release is included.

@steipete
steipete marked this pull request as ready for review October 9, 2026 19:29
@steipete
steipete merged commit 791431d into openclaw/release-5718a6ec Oct 9, 2026
5 checks passed
@steipete
steipete deleted the fix/foreign-stack-trap-suspension branch October 9, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant