From 9cab62fd491c3e4732df411767efa28e63662f8c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 15 May 2026 17:09:58 +0000 Subject: [PATCH 1/2] jsc: report exceptions thrown by deferred work tasks Bun overrides JSC's DeferredWorkTimer hooks and runs scheduled tasks (FinalizationRegistry cleanup, Atomics.waitAsync, etc.) via runPendingWork() in JSCTaskScheduler.cpp. Unlike JSC's stock DeferredWorkTimer::doWork(), this did not catch exceptions thrown by the task, so a throwing FinalizationRegistry cleanup callback left a pending exception on the VM which tripped releaseAssertNoException() in the event loop's validation scope. Match JSC's behavior: wrap the task in a top exception scope, clear any non-termination exception and route it through reportUncaughtExceptionAtEventLoop so it reaches process 'uncaughtException' handlers instead of aborting. --- src/jsc/bindings/JSCTaskScheduler.cpp | 9 +++ .../jsc/finalization-registry-throw.test.ts | 78 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 test/js/bun/jsc/finalization-registry-throw.test.ts diff --git a/src/jsc/bindings/JSCTaskScheduler.cpp b/src/jsc/bindings/JSCTaskScheduler.cpp index 171b5c4edc1..158f6560c01 100644 --- a/src/jsc/bindings/JSCTaskScheduler.cpp +++ b/src/jsc/bindings/JSCTaskScheduler.cpp @@ -1,5 +1,7 @@ #include "config.h" #include +#include +#include #include "JSCTaskScheduler.h" #include "BunClientData.h" @@ -87,7 +89,14 @@ static void runPendingWork(void* bunVM, Bun::JSCTaskScheduler& scheduler, JSCDef holder.unlockEarly(); if (pendingTicket && !pendingTicket->isCancelled()) { + auto& vm = job->vm(); + auto* globalObject = job->ticket->target()->realm(); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); job->task(job->ticket.ptr()); + if (JSC::Exception* exception = scope.exception()) { + if (scope.clearExceptionExceptTermination()) + globalObject->globalObjectMethodTable()->reportUncaughtExceptionAtEventLoop(globalObject, exception); + } } delete job; diff --git a/test/js/bun/jsc/finalization-registry-throw.test.ts b/test/js/bun/jsc/finalization-registry-throw.test.ts new file mode 100644 index 00000000000..d3808b6c620 --- /dev/null +++ b/test/js/bun/jsc/finalization-registry-throw.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +describe.concurrent("FinalizationRegistry", () => { + test("throwing from cleanup callback routes to uncaughtException", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + let caught; + process.on("uncaughtException", e => { caught = e; }); + const fr = new FinalizationRegistry(() => { + throw new TypeError("from cleanup callback"); + }); + (function register() { fr.register({ a: 1 }, "held"); })(); + let ticks = 0; + function tick() { + Bun.gc(true); + if (caught) { + console.log("CAUGHT", caught.message); + return; + } + if (++ticks > 50) { + console.log("SKIPPED"); + return; + } + setImmediate(tick); + } + tick(); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).not.toContain("ASSERTION FAILED"); + expect(stderr).not.toContain("releaseAssertNoException"); + expect(exitCode).toBe(0); + // GC timing is non-deterministic; if the callback ran it must have been + // routed through uncaughtException rather than crashing the process. + if (!stdout.includes("SKIPPED")) { + expect(stdout).toContain("CAUGHT from cleanup callback"); + } + }); + + test("throwing from cleanup callback without handler does not crash", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const fr = new FinalizationRegistry(() => { ArrayBuffer(); }); + (function register() { fr.register({ a: 1 }, "held"); })(); + let ticks = 0; + function tick() { + Bun.gc(true); + if (++ticks > 50) return; + setImmediate(tick); + } + tick(); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect(stderr).not.toContain("ASSERTION FAILED"); + expect(stderr).not.toContain("releaseAssertNoException"); + // Process must exit normally (not abort via signal). + expect(exitCode).not.toBeNull(); + expect(proc.signalCode).toBeNull(); + }); +}); From 8815ee1ebd2fd7a79236c294fbd8f4af22de3d25 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 17:12:22 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- src/crash_handler/lib.rs | 14 +++++++-- src/errno/lib.rs | 14 +++++++-- src/perf/tracy.rs | 7 ++++- src/runtime/cli/Arguments.rs | 5 +++- src/runtime/cli/run_command.rs | 45 ++++++++++++++++++++++------ src/runtime/cli/upgrade_command.rs | 7 ++++- src/runtime/jsc_hooks.rs | 7 +++-- src/runtime/webview/ChromeProcess.rs | 12 ++++++-- src/spawn/process.rs | 11 +++++-- src/spawn_sys/spawn_process.rs | 5 +++- 10 files changed, 103 insertions(+), 24 deletions(-) diff --git a/src/crash_handler/lib.rs b/src/crash_handler/lib.rs index e2773ce194d..4ff1536c53a 100644 --- a/src/crash_handler/lib.rs +++ b/src/crash_handler/lib.rs @@ -1766,7 +1766,12 @@ mod draft { .store(handle as *mut core::ffi::c_void, Ordering::Relaxed); } } - #[cfg(any(target_os = "macos", target_os = "linux", target_os = "android", target_os = "freebsd"))] + #[cfg(any( + target_os = "macos", + target_os = "linux", + target_os = "android", + target_os = "freebsd" + ))] { reset_on_posix(); } @@ -2907,7 +2912,12 @@ mod draft { let _ = spawn_result; let _ = url; } - #[cfg(any(target_os = "macos", target_os = "linux", target_os = "android", target_os = "freebsd"))] + #[cfg(any( + target_os = "macos", + target_os = "linux", + target_os = "android", + target_os = "freebsd" + ))] { let mut buf = bun_core::PathBuffer::default(); let mut buf2 = bun_core::PathBuffer::default(); diff --git a/src/errno/lib.rs b/src/errno/lib.rs index abe6ee0ccf7..8142a6c52ea 100644 --- a/src/errno/lib.rs +++ b/src/errno/lib.rs @@ -409,7 +409,12 @@ mod errno_name_tests { assert_eq!(Error::from_errno(0), Error::UNEXPECTED); assert_eq!(Error::from_errno(9999), Error::UNEXPECTED); // errno 11 is platform-specific: EAGAIN on linux/windows, EDEADLK on darwin/bsd. - #[cfg(any(target_os = "linux", target_os = "android", windows, target_family = "wasm"))] + #[cfg(any( + target_os = "linux", + target_os = "android", + windows, + target_family = "wasm" + ))] { assert_eq!(Error::from_errno(11), Error::intern("EAGAIN")); assert_eq!(Error::from_errno(104), Error::intern("ECONNRESET")); @@ -464,7 +469,12 @@ mod errno_name_tests { coreutils_error_map::get(2), Some("No such file or directory") ); - #[cfg(any(target_os = "linux", target_os = "android", windows, target_family = "wasm"))] + #[cfg(any( + target_os = "linux", + target_os = "android", + windows, + target_family = "wasm" + ))] assert_eq!( coreutils_error_map::get(11), Some("Resource temporarily unavailable") diff --git a/src/perf/tracy.rs b/src/perf/tracy.rs index a4d304fd3fa..16ee81aaddd 100644 --- a/src/perf/tracy.rs +++ b/src/perf/tracy.rs @@ -756,7 +756,12 @@ fn dlsym(symbol: &'static core::ffi::CStr) -> Option { ]; #[cfg(windows)] const PATHS_TO_TRY: &[&core::ffi::CStr] = &[c"tracy.dll"]; - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "android", windows)))] + #[cfg(not(any( + target_os = "macos", + target_os = "linux", + target_os = "android", + windows + )))] const PATHS_TO_TRY: &[&core::ffi::CStr] = &[]; // TODO(port): RTLD flags — Zig used `@bitCast(@as(i32, -2))` on diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 464ee8767ba..63c1a2520e8 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -679,7 +679,10 @@ pub const BASE_RUNTIME_TRANSPILER_PARAMS: &[ParamType] = // built with `comptime_table!(.., cold)` and stay in plain `.rodata`, where // `src/startup.order` can still cluster the ones a sampled cold path actually // hits without weighing down the `.rodata.startup` fault-around window. -#[cfg_attr(any(target_os = "linux", target_os = "android"), unsafe(link_section = ".rodata.startup"))] +#[cfg_attr( + any(target_os = "linux", target_os = "android"), + unsafe(link_section = ".rodata.startup") +)] pub static AUTO_TABLE: &clap::ConvertedTable = clap::comptime_table!(AUTO_PARAMS); pub static RUN_TABLE: &clap::ConvertedTable = clap::comptime_table!(RUN_PARAMS, cold); pub static BUILD_TABLE: &clap::ConvertedTable = clap::comptime_table!(BUILD_PARAMS, cold); diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index 03a0613055e..999e7d596a3 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -552,7 +552,10 @@ Full documentation is available at https://bun.com/docs/cli/run /// not share `.text` pages with the hot `bun run