socket/websocket: guard the error-handler dispatch against re-entering JS with a pending termination exception

Merged
RO
4mo ago
+248-0

What

test/js/node/test/parallel/test-http2-reset-flood.js started intermittently aborting on the asan lane with:

ASSERTION FAILED: (null)
!exception()
vendor/WebKit/Source/JavaScriptCore/runtime/ExceptionScope.h(61) : void JSC::ExceptionScope::assertNoException()

after #32488 landed (which changed the http2 teardown timing enough to surface this in CI; the underlying bug predates it).

Cause

When a Bun.listen / Bun.serve socket handler runs inside a worker and worker.terminate() fires while the handler is mid-call, the termination exception is raised at a JS safepoint inside the handler. The socket dispatch path (socket_body.rs on_close/on_data/etc., and ServerWebSocket.rs on_message/on_close/etc.) sees the handler return Err and follows up with the error handler:

if let Err(e) = callback.call(&global, this_value, &[...]) {
    let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(e)]);
}

take_error calls JSGlobalObject__tryTakeException, whose tryClearException() is a no-op for termination, so the exception stays pending on the VM. The error-handler dispatch then calls on_error.call(), which enters Interpreter::executeCallImpl:

auto scope = DECLARE_THROW_SCOPE(vm);
scope.assertNoException();  // aborts: termination is still pending

Found by instrumenting every Bun__JSValue__call entry with per-thread ring-buffer markers and stressing under contention; the last recorded callers were socket_body.rs:2058 (on_close's callback.call) followed directly by Handlers.rs:297 (on_error.call) with the exception already pending.

Fix

Add a has_exception() guard to the two error-handler dispatch functions that call JS after a preceding callback.call() -> Err:

  • Handlers::call_error_handler (src/runtime/socket/Handlers.rs), reached from every Bun.listen/Bun.connect socket lifecycle handler's Err branch
  • WebSocketServerContext::run_error_callback (src/runtime/server/WebSocketServerContext.rs), reached from every Bun.serve websocket handler's Err branch

This mirrors the existing guard in UDPSocket::call_error_handler (src/runtime/socket/udp_socket.rs:673) and EventLoop::run_callback (src/jsc/event_loop.rs:387). When the worker is terminating there is no point calling the error handler anyway; the VM is tearing down.

Verification

New test/js/bun/net/socket-handler-worker-terminate.test.ts reproduces the abort deterministically via Atomics coordination for both dispatch paths: a worker's handler signals the parent it is running, the parent terminates it, the handler spins on safepoints until termination lands. The test throws if the handler never fired or if terminate never landed inside it in any iteration, so a vacuous pass is impossible. Both variants fail on main with the exact assertion above (exit 134), and both pass with this patch.

Under the original stress harness (8 parallel runs of test-http2-reset-flood.js), main fails within ~10-300 rounds; with this patch no failure in 3200+ rounds.


no test proof · iteration 5 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/net/socket-handler-worker-terminate.test.ts