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
11 changes: 6 additions & 5 deletions src/crash_handler/handle_oom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ use bun_core::Error;
/// let thing = match allocate_thing() { Ok(v) => v, Err(err) => bun::handle_oom(err) };
/// ```
///
/// In Rust, `Vec`/`Box` allocation already aborts on OOM via the
/// global allocator's `handle_alloc_error`. Per PORTING.md §Allocators,
/// callsites of `bun.handleOom(expr)` translate to bare `expr`. This function
/// remains for the residual cases where a `Result<T, AllocError>` is threaded
/// explicitly.
/// In Rust, `Vec`/`Box` allocation failure goes through the global
/// allocator's `handle_alloc_error`, which the alloc-error hook installed by
/// `install_hooks()` routes into the same `bun::out_of_memory()` crash report
/// as this function. Per PORTING.md §Allocators, callsites of
/// `bun.handleOom(expr)` translate to bare `expr`. This function remains for
/// the residual cases where a `Result<T, AllocError>` is threaded explicitly.
pub fn handle_oom<A: HandleOom>(error_union_or_set: A) -> A::Output {
error_union_or_set.handle_oom()
}
Expand Down
28 changes: 28 additions & 0 deletions src/crash_handler/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
// builds only. Declaring the feature where neither is compiled (Windows
// release) trips `unused_features`.
#![cfg_attr(any(not(windows), debug_assertions), feature(core_intrinsics))]
// `std::alloc::set_alloc_error_hook` — routes infallible-allocation failure
// (`handle_alloc_error`) into the crash handler. See `alloc_error_hook`.
#![feature(alloc_error_hook)]
#![allow(internal_features)]
#![allow(nonstandard_style, static_mut_refs, unexpected_cfgs)]
#![warn(unused_must_use)]
Expand Down Expand Up @@ -51,6 +54,26 @@ pub(crate) extern "Rust" fn __bun_crash_handler_out_of_memory() -> ! {
out_of_memory()
}

/// Alloc-error hook registered by `install_hooks()`. Infallible allocations
/// (`Vec`/`Box`/`String` growth) report failure through
/// `std::alloc::handle_alloc_error`, which calls this hook. Without it, std's
/// default hook prints "memory allocation of N bytes failed" and aborts —
/// no OOM message, no trace string, no crash report. Routing into
/// `out_of_memory()` gives those failures the same `CrashReason::OutOfMemory`
/// report as the explicit `Result<_, AllocError>` path (`bun_core::handle_oom`),
/// matching Zig, where every failed allocation bubbled to `bun.outOfMemory()`.
///
/// Runs under memory pressure, so it must not allocate: the body is a single
/// diverging call, and the crash handler itself writes through stack buffers
/// and raw stderr. If the report path does hit a second OOM, the
/// `PANIC_STAGE` re-entry guard in `crash_handler()` aborts instead of
/// looping.
#[cold]
#[inline(never)]
fn alloc_error_hook(_layout: core::alloc::Layout) {
out_of_memory()
}

/// `extern "Rust"` symbol resolved by `bun_core::dump_current_stack_trace()`
/// at link time. Lives in `.text` (read-only).
#[doc(hidden)]
Expand Down Expand Up @@ -1774,6 +1797,11 @@ mod draft {
// this hook, a bare `panic!` would print the std
// default hook + unwind with no trace string and no upload.
std::panic::set_hook(Box::new(rust_panic_hook));
// Route infallible-allocation failure (`handle_alloc_error`) through
// the same `CrashReason::OutOfMemory` path as `bun_core::handle_oom`;
// std's default hook would print "memory allocation of N bytes failed"
// and abort with no trace string and no report.
std::alloc::set_alloc_error_hook(crate::alloc_error_hook);
}

/// `std::panic` hook: emit the same trace-string + auto-report as the fatal
Expand Down
1 change: 1 addition & 0 deletions src/js/internal-for-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const crash_handler = $zig("crash_handler.zig", "js_bindings.generate") a
panic: () => void;
rootError: () => void;
outOfMemory: () => void;
infallibleOutOfMemory: () => void;
raiseIgnoringPanicHandler: () => void;
};

Expand Down
25 changes: 24 additions & 1 deletion src/runtime/api/crash_handler_jsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ pub mod js_bindings {
use super::*;

pub fn generate(global: &JSGlobalObject) -> JSValue {
let obj = JSValue::create_empty_object(global, 8);
// `#[bun_jsc::host_fn]` emits an `extern "C"` shim named `__jsc_host_<fn>`; that
// shim is the `JSHostFn` value passed to `JSFunction::create`.
const ENTRIES: &[(&str, bun_jsc::JSHostFn)] = &[
Expand All @@ -26,11 +25,16 @@ pub mod js_bindings {
("panic", __jsc_host_js_panic),
("rootError", __jsc_host_js_root_error),
("outOfMemory", __jsc_host_js_out_of_memory),
(
"infallibleOutOfMemory",
__jsc_host_js_infallible_out_of_memory,
),
Comment thread
claude[bot] marked this conversation as resolved.
(
"raiseIgnoringPanicHandler",
__jsc_host_js_raise_ignoring_panic_handler,
),
];
let obj = JSValue::create_empty_object(global, ENTRIES.len());
for &(name, func) in ENTRIES {
obj.put(
global,
Expand Down Expand Up @@ -113,6 +117,25 @@ pub mod js_bindings {
bun_core::out_of_memory();
}

#[bun_jsc::host_fn]
pub(crate) fn js_infallible_out_of_memory(
_global: &JSGlobalObject,
_frame: &CallFrame,
) -> JsResult<JSValue> {
crash_handler::suppress_core_dumps_if_necessary();
// Unlike `outOfMemory` above (the explicit `bun_core::out_of_memory()`
// entry), this exercises the global-allocator path: a reservation no
// 64-bit address space can satisfy makes the allocator return null, and
// `Vec` growth reports it via `std::alloc::handle_alloc_error`, which
// reaches the alloc-error hook installed by `crash_handler::init()`.
// Under ASAN, the interceptor hard-errors on impossible sizes instead
// of returning null unless `ASAN_OPTIONS=allocator_may_return_null=1`
// is set (the crash-handler test sets it).
let v = Vec::<u8>::with_capacity(1usize << 61);
core::hint::black_box(&v);
Ok(JSValue::UNDEFINED)
}

#[bun_jsc::host_fn]
pub(crate) fn js_raise_ignoring_panic_handler(
_global: &JSGlobalObject,
Expand Down
3 changes: 2 additions & 1 deletion test/cli/run/fixture-crash.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ const approach = process.argv[2];
if (approach in crash_handler) {
crash_handler[approach]();
} else {
console.error("usage: bun fixture-crash.js <segfault|panic|rootError|outOfMemory|raiseIgnoringPanicHandler>");
console.error("usage: bun fixture-crash.js <segfault|panic|rootError|outOfMemory|infallibleOutOfMemory|raiseIgnoringPanicHandler>");
process.exit(1);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
25 changes: 19 additions & 6 deletions test/cli/run/run-crash-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ test("raise ignoring panic handler does not trigger the panic handler", async ()
});

describe("automatic crash reporter", () => {
for (const approach of ["panic", "segfault", "outOfMemory"]) {
// "outOfMemory" is the explicit `Result<_, AllocError>` path;
// "infallibleOutOfMemory" is ordinary `Vec`/`Box` growth failing inside the
// global allocator (`handle_alloc_error` → alloc-error hook). Both must
// produce the same OutOfMemory crash report.
for (const approach of ["panic", "segfault", "outOfMemory", "infallibleOutOfMemory"]) {
test(`${approach} should report`, async () => {
let sent = false;
const resolve_handler = Promise.withResolvers();
Expand All @@ -149,23 +153,32 @@ describe("automatic crash reporter", () => {
GITHUB_ACTIONS: undefined,
CI: undefined,
},
approach === "infallibleOutOfMemory"
? {
// ASAN's interceptor hard-errors on impossible allocation
// sizes; let it return null so the failure reaches Rust's
// `handle_alloc_error` like a real OOM would. No-op without ASAN.
ASAN_OPTIONS: [bunEnv.ASAN_OPTIONS, "allocator_may_return_null=1"].filter(Boolean).join(":"),
}
: {},
]),
stdio: ["ignore", "pipe", "pipe"],
});
const exitCode = await proc.exited;
const stderr = await proc.stderr.text();
console.log(stderr);

await resolve_handler.promise;

expect(exitCode).not.toBe(0);
expect(stderr).toContain(server.url.toString());
if (approach !== "outOfMemory") {
expect(stderr).toContain("oh no: Bun has crashed. This indicates a bug in Bun, not your code");
} else {
if (approach === "outOfMemory" || approach === "infallibleOutOfMemory") {
expect(stderr.toLowerCase()).toContain("out of memory");
expect(stderr.toLowerCase()).not.toContain("panic");
} else {
expect(stderr).toContain("oh no: Bun has crashed. This indicates a bug in Bun, not your code");
}

// Wait for the report to arrive (resolves the moment the POST is heard).
await resolve_handler.promise;
expect(sent).toBe(true);
});
}
Expand Down
Loading