Skip to content

process wedges ~60s after startup on FreeBSD: Heap.listen's SIGUSR1 listener clobbers JSC's thread-suspend signal #54928

Open

Description

@tuaris

Environment

  • FreeBSD 15.1-RELEASE amd64
  • opencode v2.0.27, built from the tag with a native FreeBSD Bun (bun 1.4.3, bun-freebsd-x64)
  • Also reproduces in dev mode (bun run src/index.ts serve), so this is not a bun build --compile artifact

Symptom

opencode serve (and the TUI, same code path) starts fine and answers requests, then ~60-120s later the whole process wedges: new connections are accepted at TCP level but never get a response; every thread is parked in uwait/usem. SIGTERM during a service stop never completes either - the process has to be kill -9'd. Compiled v2.0.27 binary, dev-mode source, and serve --service all wedge identically.

ktrace evidence

Immediately before the event loop stops polling kqueue there is a thr_kill(<lwp>, SIGUSR1), then PSIG SIGUSR1 caught ... code=SI_LWP handled by a non-JSC handler, after which the loop never calls kevent() again and all threads sit in UMTX_OP_WAIT_UINT_PRIVATE/UMTX_OP_SEM2_WAIT. A janitor thread keeps running (madvise churn), so the process is not fully dead - JSC's suspend/resume handshake is.

Root cause

JSC picks its thread suspend/resume signal as SIGPWR on Linux but SIGUSR1 on every other POSIX target, including FreeBSD (wtf/posix/ThreadingPOSIX.cpp). packages/cli/src/heap.ts (Heap.listen, started from the CLI's main init at packages/cli/src/index.ts:85) registers process.on("SIGUSR1", handler), which installs Bun's JS signal handler over JSC's. From then on, the first goroutine-level suspend (GC / VMTraps) signals into Bun's handler instead of JSC's; globalSemaphoreForSuspendResume is never posted and the suspender never resumes. This is exactly the mechanism described upstream:

v1.x does not have heap.ts, which matches the observation that v1.19.0 on the same Bun/FreeBSD runtime is long-lived while v2.0.27 wedges.

Repro

  1. Native FreeBSD Bun 1.4.x, opencode v2 tag.
  2. opencode serve &; one request (curl /api/info); wait ~2 minutes idle.
  3. Process never answers again. With a process.on("SIGUSR1", () => {}) patch removed from heap.ts, the same build stays alive (verified >4 minutes of repeated probing).

Suggested fix

Skip the SIGUSR1 listener on FreeBSD until bun lands #44596, e.g.:

if (process.platform === "win32" || process.platform === "freebsd") return

The only lost feature is heap-snapshot-on-signal on FreeBSD. (CpuProfile.listen's SIGPROF listener is fine - SIGPROF does not collide with JSC.)

Notes

Activity

  1. opencode-agent commented on Oct 12, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the ktrace evidence.

    I couldn't reproduce this here, because the sandbox only runs Linux. On Linux, JSC uses SIGPWR to suspend and resume threads, so the SIGUSR1 listener doesn't cause the hang there. This needs a maintainer with a FreeBSD machine and a native Bun build.

    I did check the code: on the latest dev (80fb31e), Heap.listen in packages/cli/src/heap.ts still skips the SIGUSR1 listener only on win32. It still runs at CLI startup, which fits your explanation.

    Related issues:

    I've left this for the maintainers to review, including your suggested platform check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions