Bun.sleepSync: make worker.terminate() interrupt a worker blocked in sleepSync

Closed
RO
2mo ago
+69-3

Problem

worker.terminate() never completes while the worker thread is parked in Bun.sleepSync(N). The close event does not fire until the full N milliseconds elapse, so a supervisor that relies on terminate() to bound hostile code cannot reclaim a worker that calls Bun.sleepSync with a large argument.

const w = new Worker(
  "data:text/javascript," +
    encodeURIComponent('postMessage("sleeping"); Bun.sleepSync(600000);'),
);
w.addEventListener("close", () => { console.log("CLOSED"); process.exit(0); });
w.addEventListener("message", () => w.terminate());
setTimeout(() => { console.log("HUNG"); process.exit(1); }, 5000);
// bun 1.4.0: prints HUNG, 4/4

Cause

sleep_sync is a single std::thread::sleep. The parent-thread WebWorker::notify_need_termination only sets requested_terminate, fires the JSC NeedTermination VMTrap (serviced at JS safepoints), and wakes the uws/uv event-loop poll. None of those can unblock a thread parked in nanosleep/Sleep, so the worker stays asleep for the full duration.

Fix

When sleepSync runs on a worker thread, slice the sleep against an Instant deadline and poll WebWorker::has_requested_terminate() between slices. Once terminate() sets the flag, sleepSync returns at the next slice boundary and the already-fired VMTrap throws the TerminationException at the next JS safepoint. Main-thread sleepSync keeps the single uninterruptible sleep (there is nothing to poll).

The slice is 100 ms: termination latency is bounded by that, and the extra wakeups are negligible (a 1 h sleep wakes ~36000 times, microseconds each).

Verification

New test in test/js/web/workers/worker-terminate-lifetime.test.ts:

buildresult
USE_SYSTEM_BUN=1 bun test -t "Bun.sleepSync"fail (HUNG)
bun bd test -t "Bun.sleepSync"pass (~0.8 s), 4/4

test/js/bun/util/sleepSync.test.ts (5 tests) passes unchanged, and sleepSync(250) in a worker still measures 250 ms (deadline loop preserves accuracy).

Related

This came out of a supervisor kill-switch audit that also flagged Atomics.wait (#32802) and nested-worker orphans. Those are independent changes.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/web/workers/worker-terminate-lifetime.test.ts