Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/crash_handler/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
Expand Down
14 changes: 12 additions & 2 deletions src/errno/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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")
Expand Down
9 changes: 9 additions & 0 deletions src/jsc/bindings/JSCTaskScheduler.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "config.h"
#include <JavaScriptCore/VM.h>
#include <JavaScriptCore/TopExceptionScope.h>
#include <JavaScriptCore/GlobalObjectMethodTable.h>
#include "JSCTaskScheduler.h"
#include "BunClientData.h"

Expand Down Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/perf/tracy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,12 @@ fn dlsym<T: Copy>(symbol: &'static core::ffi::CStr) -> Option<T> {
];
#[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
Expand Down
5 changes: 4 additions & 1 deletion src/runtime/cli/Arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
45 changes: 36 additions & 9 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,10 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
/// not share `.text` pages with the hot `bun run <script>` dispatch path.
#[cold]
#[inline(never)]
#[cfg_attr(any(target_os = "linux", target_os = "android"), unsafe(link_section = ".text.unlikely"))]
#[cfg_attr(
any(target_os = "linux", target_os = "android"),
unsafe(link_section = ".text.unlikely")
)]
fn configure_run_transpiler_linker(this_transpiler: &mut Transpiler<'static>) {
this_transpiler.resolver.opts.load_tsconfig_json = true;
this_transpiler.options.load_tsconfig_json = true;
Expand Down Expand Up @@ -870,7 +873,10 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
/// initializing JSC.
#[cold]
#[inline(never)]
#[cfg_attr(any(target_os = "linux", target_os = "android"), unsafe(link_section = ".text.unlikely"))]
#[cfg_attr(
any(target_os = "linux", target_os = "android"),
unsafe(link_section = ".text.unlikely")
)]
fn boot_bun_shell(
ctx: &mut ContextData,
entry_path: &[u8],
Expand Down Expand Up @@ -1669,7 +1675,10 @@ fn log_clear_msgs(vm: &mut VirtualMachine) {

#[cold]
#[inline(never)]
#[cfg_attr(any(target_os = "linux", target_os = "android"), unsafe(link_section = ".text.unlikely"))]
#[cfg_attr(
any(target_os = "linux", target_os = "android"),
unsafe(link_section = ".text.unlikely")
)]
fn dump_build_error(vm: &mut VirtualMachine) {
Output::flush();
if let Some(log) = vm.log {
Expand All @@ -1689,7 +1698,10 @@ fn dump_build_error(vm: &mut VirtualMachine) {
/// `.text.hot` fault-around window the `require('fs')` startup path pulls in.
#[cold]
#[inline(never)]
#[cfg_attr(any(target_os = "linux", target_os = "android"), unsafe(link_section = ".text.unlikely"))]
#[cfg_attr(
any(target_os = "linux", target_os = "android"),
unsafe(link_section = ".text.unlikely")
)]
fn exit_with_unhandled_note(vm: &mut VirtualMachine) -> ! {
vm.exit_handler.exit_code = 1;
vm.on_exit();
Expand All @@ -1703,7 +1715,10 @@ fn exit_with_unhandled_note(vm: &mut VirtualMachine) -> ! {
/// Cold `Err(err)` arm of `vm.load_entry_point` in `Run::start`.
#[cold]
#[inline(never)]
#[cfg_attr(any(target_os = "linux", target_os = "android"), unsafe(link_section = ".text.unlikely"))]
#[cfg_attr(
any(target_os = "linux", target_os = "android"),
unsafe(link_section = ".text.unlikely")
)]
fn entry_point_load_failed(vm: &mut VirtualMachine, err: &bun_core::Error) -> ! {
if log_has_msgs(vm) {
dump_build_error(vm);
Expand All @@ -1722,7 +1737,10 @@ fn entry_point_load_failed(vm: &mut VirtualMachine, err: &bun_core::Error) -> !
/// exit: bump the exit code and print the sourcemap note + version string.
#[cold]
#[inline(never)]
#[cfg_attr(any(target_os = "linux", target_os = "android"), unsafe(link_section = ".text.unlikely"))]
#[cfg_attr(
any(target_os = "linux", target_os = "android"),
unsafe(link_section = ".text.unlikely")
)]
fn print_unhandled_version_note(vm: &mut VirtualMachine) {
vm.exit_handler.exit_code = 1;
bun_sourcemap::SavedSourceMap::MissingSourceMapNoteInfo::print();
Expand Down Expand Up @@ -1762,7 +1780,10 @@ impl RunCommand {
/// `.text.hot` fault-around window the `require('fs')` startup path pulls in.
#[cold]
#[inline(never)]
#[cfg_attr(any(target_os = "linux", target_os = "android"), unsafe(link_section = ".text.unlikely"))]
#[cfg_attr(
any(target_os = "linux", target_os = "android"),
unsafe(link_section = ".text.unlikely")
)]
fn boot_failed_exit(ctx: &mut ContextData, display_name: &[u8], err: &bun_core::Error) -> ! {
// SAFETY: `ctx.log` was set in `create_context_data` (single-threaded
// CLI startup) and is process-lifetime.
Expand Down Expand Up @@ -3022,7 +3043,10 @@ impl RunCommand {

#[cold]
#[inline(never)]
#[cfg_attr(any(target_os = "linux", target_os = "android"), unsafe(link_section = ".text.unlikely"))]
#[cfg_attr(
any(target_os = "linux", target_os = "android"),
unsafe(link_section = ".text.unlikely")
)]
fn exec_as_if_node_missing_script() -> ! {
Output::err_generic(
"Missing script to execute. Bun's provided 'node' cli wrapper does not support a repl.",
Expand All @@ -3033,7 +3057,10 @@ impl RunCommand {

#[cold]
#[inline(never)]
#[cfg_attr(any(target_os = "linux", target_os = "android"), unsafe(link_section = ".text.unlikely"))]
#[cfg_attr(
any(target_os = "linux", target_os = "android"),
unsafe(link_section = ".text.unlikely")
)]
fn exec_as_if_node_boot_failed(
ctx: &mut ContextData,
basename: &[u8],
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/cli/upgrade_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,12 @@ impl UpgradeCommand {
{
"powershell -c 'irm bun.sh/install.ps1|iex'"
}
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "windows")))]
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "windows"
)))]
{
// TODO(port): Environment.os.displayString() at comptime
"(TODO: Install script for this platform)"
Expand Down
7 changes: 4 additions & 3 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4499,13 +4499,14 @@ pub(crate) fn resolve_embedded_file_to_buf(
// Spec ModuleLoader.zig:43-45 — `tmpname(extname, buf, bun.hash(file.name))`.
let mut tmpname_buf = bun_paths::path_buffer_pool::get();
let tmpfilename =
Fs::FileSystem::tmpname(extname, &mut tmpname_buf[..], bun_wyhash::hash(file_name))
.ok()?;
Fs::FileSystem::tmpname(extname, &mut tmpname_buf[..], bun_wyhash::hash(file_name)).ok()?;

// Spec ModuleLoader.zig:47 — `bun.fs.FileSystem.instance.tmpdir()`.
// SAFETY: `FileSystem::instance()` returns the process-global singleton
// pointer (initialized at startup).
let tmpdir = (unsafe { &mut *Fs::FileSystem::instance() }).tmpdir().ok()?;
let tmpdir = (unsafe { &mut *Fs::FileSystem::instance() })
.tmpdir()
.ok()?;
let tmpdir_fd: bun_sys::Fd = tmpdir.fd;

// Spec ModuleLoader.zig:50-51 — `bun.Tmpfile.create(tmpdir, tmpfilename)`.
Expand Down
12 changes: 10 additions & 2 deletions src/runtime/webview/ChromeProcess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,10 @@ fn find_playwright_shell() -> Option<ZBox> {
}

// Fall back to the non-cft linux arm64 layout.
#[cfg(all(any(target_os = "linux", target_os = "android"), target_arch = "aarch64"))]
#[cfg(all(
any(target_os = "linux", target_os = "android"),
target_arch = "aarch64"
))]
{
let bin_parts2: [&[u8]; 3] = [
cache_dir,
Expand Down Expand Up @@ -633,7 +636,12 @@ fn read_dev_tools_active_port(out_buf: &mut Vec<u8>) -> Option<()> {
b"BraveSoftware\\Brave-Browser\\User Data\\DevToolsActivePort",
b"Microsoft\\Edge\\User Data\\DevToolsActivePort",
];
#[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
)))]
let candidates: &[&[u8]] = &[];

let mut path_buf = path_buffer_pool::get();
Expand Down
11 changes: 9 additions & 2 deletions src/spawn/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3224,7 +3224,10 @@ mod spawn_process_body {
// that are read *after* the wait, so falling through to the memfd block
// below is required.
let status: Status = 'blk: {
if no_orphans && (cfg!(any(target_os = "linux", target_os = "android")) || cfg!(target_os = "macos")) {
if no_orphans
&& (cfg!(any(target_os = "linux", target_os = "android"))
|| cfg!(target_os = "macos"))
{
let ppid = ParentDeathWatchdog::ppid_to_watch().unwrap_or(0);
#[cfg(target_os = "macos")]
let r: Option<Maybe<Status>> = wait_mac_kqueue(
Expand All @@ -3245,7 +3248,11 @@ mod spawn_process_body {
&mut out_fds_to_wait_for,
&mut out_fds,
);
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "macos"
)))]
let r: Option<Maybe<Status>> = {
let _ = ppid;
None
Expand Down
5 changes: 4 additions & 1 deletion src/spawn_sys/spawn_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,10 @@ pub fn spawn_process_posix(
let mut dup_stdout_to_stderr: bool = false;

// The label is only referenced from the Linux memfd fast-path below.
#[cfg_attr(not(any(target_os = "linux", target_os = "android")), allow(unused_labels))]
#[cfg_attr(
not(any(target_os = "linux", target_os = "android")),
allow(unused_labels)
)]
'stdio: for i in 0..3usize {
let fileno = Fd::from_native(FdT::try_from(i).unwrap());
let flag: u32 = (if i == 0 {
Expand Down
78 changes: 78 additions & 0 deletions test/js/bun/jsc/finalization-registry-throw.test.ts
Original file line number Diff line number Diff line change
@@ -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");
Comment on lines +39 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove negative checks for assertion/panic-style stderr strings.

These output-string negation checks are disallowed and brittle here; rely on behavior/assertions that must hold (signal/exit/expected stdout path) instead.

As per coding guidelines: "Never write tests that check for no 'panic' or 'uncaught exception' or similar in output - these will never fail in CI."

Also applies to: 72-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/bun/jsc/finalization-registry-throw.test.ts` around lines 39 - 40,
Remove the brittle negative stderr checks by deleting the two expect calls
expect(stderr).not.toContain("ASSERTION FAILED") and
expect(stderr).not.toContain("releaseAssertNoException") (and the duplicate pair
referenced at 72-73) from finalization-registry-throw.test.ts; instead assert
the concrete expected behavior for the test (e.g., existing exit/signal
expectations or expected stdout content) using the existing test harness
assertions so the test verifies positive outcomes rather than the absence of
panic strings.

Comment on lines +39 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit: Per root CLAUDE.md ("Writing Tests"), tests should never assert that stderr does not contain "panic"/"uncaught exception"/assertion strings — these checks never fail in CI. The expect(stderr).not.toContain("ASSERTION FAILED") / "releaseAssertNoException" lines here and at lines 72-73 can be dropped; expect(exitCode).toBe(0) and expect(proc.signalCode).toBeNull() already catch the regression (the release assert aborts via SIGABRT).

Extended reasoning...

What

Root CLAUDE.md, Writing Tests section, states:

NEVER write tests that check for no "panic" or "uncaught exception" or similar in the test output. These tests will never fail in CI.

This PR adds four such assertions in test/js/bun/jsc/finalization-registry-throw.test.ts:

  • Lines 39-40: expect(stderr).not.toContain("ASSERTION FAILED") / expect(stderr).not.toContain("releaseAssertNoException")
  • Lines 72-73: same pair in the second test

Grep across test/ shows no other file uses not.toContain("ASSERTION FAILED"), so this introduces a pattern the repo has explicitly avoided.

Why these assertions don't add signal

  1. CI builds don't print this string. ASSERTION FAILED: … releaseAssertNoException is the debug-build assertion banner. In CI's release builds, releaseAssertNoException calls RELEASE_ASSERTWTFCrash, which aborts without writing that exact text. So in the environment that matters, the negative substring match is vacuously true even when the bug regresses.
  2. They pass when the callback never fires. GC timing is non-deterministic (the test itself acknowledges this with the SKIPPED branch). If the cleanup callback never runs, stderr is empty and not.toContain(...) passes trivially — it's not actually exercising the fix.

Step-by-step on a regression

Suppose this PR's C++ change is reverted and the test runs in CI:

  1. Subprocess registers the FinalizationRegistry, GC fires the cleanup callback, callback throws.
  2. releaseAssertNoException triggers RELEASE_ASSERT → process receives SIGABRT.
  3. proc.exited resolves with exitCode === null, proc.signalCode === "SIGABRT".
  4. Test 1: expect(exitCode).toBe(0) fails → regression caught. ✅
  5. Test 2: expect(proc.signalCode).toBeNull() fails → regression caught. ✅
  6. Meanwhile, stderr in a release build does not contain the literal string "ASSERTION FAILED", so lines 39-40 / 72-73 still pass. They contributed nothing.

The exit-code/signal assertions are doing all the work; the stderr checks are dead weight that violate the documented convention.

Fix

Delete lines 39-40 and 72-73. The tests remain correct — expect(exitCode).toBe(0) (test 1) and expect(proc.signalCode).toBeNull() (test 2) are sufficient and are the assertions that actually fail on regression.

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");
}
Comment on lines +38 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert exitCode last in both subprocess tests.

expect(exitCode) is currently not the last assertion in either test, which weakens failure diagnostics for spawned-process failures.

As per coding guidelines: "Assert the exit code last in tests - this gives a more useful error message on test failure."

Also applies to: 71-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/bun/jsc/finalization-registry-throw.test.ts` around lines 38 - 46,
Move the exit code assertion to the end of each subprocess test so failures show
process output first: in the block where you await
Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]) and assign
stdout, stderr, exitCode, delay the line expect(exitCode).toBe(0) until after
all other expectations (e.g., after the stderr assertions and the stdout "CAUGHT
from cleanup callback" check); apply the same change to the second subprocess
test around the other Promise.all usage (the block referenced by stdout, stderr,
exitCode later in the file).

});

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();
});
});
Loading