Repository navigation
Description
Summary
After a successful conversation, the OpenCode (bun --compile) process hangs on exit on FreeBSD. This is a separate bug from the compiled-binary freeze in #44572 (that one is the aarch64 Wasm instruction-cache publication and is fixed by the membarrier patch). This exit hang is architecture-independent — it reproduces on amd64 as well as aarch64 — and it is in the WTF/JSC thread-suspension handshake.
Environment
- FreeBSD 15.1-RELEASE aarch64 and FreeBSD 14.4-RELEASE amd64
- bun 1.4.2 (built from source; also with the official FreeBSD binary)
- OpenCode 2.0.22, TUI run as
opencode --standalone
Reproduction
Start the TUI, complete one or more turns, then exit (Ctrl-C twice). The process never exits; the main thread is parked in native code. It hangs with or without heap snapshots (SIGUSR1) — it is the exit path.
What it is
Symbolized lldb capture (all threads) at the hang:
- main:
JSC::MachineThreads::tryCopyOtherThreadStacks→ parked (wants the thread-suspend machinery) - Worker:
JSC::Heap::runTaskInParallel→WTF::Condition::waitUntilUnchecked(parked) - VMTraps SignalSender:
JSC::VMTraps::SignalSender::work→WTF::sendMessageScoped→WTF::Thread::suspend→sem_wait
So a VMTraps::SignalSender is trying to stop the VM owner thread and never completes.
Pinpointed with an instrumented build
With temporary logging in Thread::suspend / Thread::resume / signalHandlerSuspendResume (the handler logging via async-signal-safe write(2)) and VMTraps::SignalSender::work:
[VMTRAPS] suspend owner uid=13
[SUSP] enter target uid=13 cnt=0 handle=0x... name=[Worker] curUid=21 sig=30
[SUSP] killed uid=13 result=0
- the target is the
Workerthread (the owner of the Worker VM), - the signal number is 30 (SIGPWR),
pthread_killsucceeds (result=0),- but the signal handler never runs —
[HANDLER]count is 0 for the whole run, and[SUSP] wokenis never printed (theglobalSemaphoreForSuspendResume->wait()never returns).
The Worker thread is blocked in an interruptible __umtx_op_wait_uint_private (sleepq_wait_sig) at the time, so the signal should be deliverable. procstat sigfastblock shows a sigfastblock pointer for every thread, so this looks like the suspend signal is blocked or deferred on that thread (or its disposition differs), i.e. the signal is sent but never dispatched to signalHandlerSuspendResume.
Notes:
Thread::suspendonly signals when the target'sm_suspendCount == 0, so it is not an "already suspended" early-out.- This happens with the signal-based barrier patch reverted and with the
membarrierfix applied, so the Wasm path is not involved.
Why this matters
Thread::suspend is not just used by Wasm publishing — it is the GC's stop-the-world (stack scanning) and VMTraps mechanism on FreeBSD. A suspend that is sent but never acknowledged wedges the whole process. It is the remaining reason a compiled OpenCode binary cannot be exited cleanly on FreeBSD.
Possible directions
- Ensure the suspend signal is unblocked on every JSC/Worker/bun thread (it must be serviced even when the thread is parked in a
WTF::ParkingLot/ThreadConditionwait). - Backport the generation-based
SuspendResumeRequesthandler fromoven-sh/WebKitmain ([WTF] The thread suspend signal handler acts only on requests from suspend() and resume(), PRbun tsconfig#693) — it makes the handler attribute signals to the correct thread, but it only helps if the handler actually runs. - Have
VMTraps::SignalSenderavoid suspending a thread that is itself parked/in a GC (or make the sender tolerant of a target that does not respond).
Happy to provide the full lldb bt all captures and the instrumented-build logs.
Activity
samm-git commentedon Oct 5, 2026
Found it — bun overrides JSC's SIGUSR1 handler on FreeBSD, which is why
Thread::suspend()never gets its ack.src/jsc/bindings/BunProcess.cppguards JS signal listeners against JSC's suspend signal with an#if OS(LINUX):JSC uses SIGPWR on Linux but SIGUSR1 on FreeBSD (
wtf/posix/ThreadingPOSIX.cpp). So on FreeBSD, if JS registers aSIGUSR1listener, bun installs its own handler and clobbers JSC's. The suspend signal is then delivered to the wrong handler,globalSemaphoreForSuspendResumeis never posted, and VM teardown deadlocks (VMTraps::SignalSender→Thread::suspend→sem_wait, with a GC helper blocked on the suspend lock it holds).Evidence:
Thread::signalHandlerSuspendResume: the suspender'sS:enteris logged, the handler'sH:enternever is — so the handler never executes.ktrace:PSIG SIGUSR1 caught ... code=SI_LWPon the owner thread, delivered to a non-JSC handler; the mask is clear and the sigfastblock word is 0.oven-sh/WebKit#693does not help (it only reworks the handler's internals); the problem is that the handler is not installed.Fix: add
signalNumber != g_wtfConfig.sigThreadSuspendResumeto the FreeBSD branch (matching Linux). With it, the deterministic exit test isexit=OK15/15 (wasHANG5/5), and all conversation turns still pass.Fix PR: #44596
Fix the Android and FreeBSD Bun builds and the false windows-arm64 er…