Use std.debug.captureStackTrace on all platforms (#24456)

In the crash reporter, we currently use glibc's `backtrace()` function
on glibc Linux targets. However, this has resulted in poor stack traces
in many scenarios, particularly when a JSC signal handlers is involved,
in which case the stack trace tends to have only one frame—the signal
handler itself. Considering that JSC installs a signal handler for SEGV,
this is particularly bad.

Zig's `std.debug.captureStackTrace` generates considerably more complete
stack traces, but it has an issue where the top frame is missing when a
signal handler is involved. This is unfortunate, but it's still the
better option for now. Note that our stack traces on macOS also have
this missing frame issue.

In the future, we will investigate backporting the changes to stack
trace capturing that were recently made in Zig's `master` branch, since
that seems to have fixed the missing frame issue.

This PR still uses the stack trace provided by `backtrace()` if it
returns more frames than `captureStackTrace`. In particular, ARM may
need this behavior.

(For internal tracking: fixes ENG-21406)
This commit is contained in:
taylor.fish
2025-11-07 04:07:53 -08:00
committed by GitHub
parent 8ec856124c
commit 23a2b2129c

View File

@@ -163,6 +163,37 @@ pub const Action = union(enum) {
}
};
fn captureLibcBacktrace(begin_addr: usize, stack_trace: *std.builtin.StackTrace) void {
const backtrace = struct {
extern "c" fn backtrace(buffer: [*]*anyopaque, size: c_int) c_int;
}.backtrace;
const addrs = stack_trace.instruction_addresses;
const count = backtrace(@ptrCast(addrs), @intCast(addrs.len));
stack_trace.index = @intCast(count);
// Skip frames until we find begin_addr (or close to it)
// backtrace() captures everything including crash handler frames
const tolerance: usize = 128;
const skip: usize = for (addrs[0..stack_trace.index], 0..) |addr, i| {
// Check if this address is close to begin_addr (within tolerance)
const delta = if (addr >= begin_addr)
addr - begin_addr
else
begin_addr - addr;
if (delta <= tolerance) break i;
// Give up searching after 8 frames
if (i >= 8) break 0;
} else 0;
// Shift the addresses to skip crash handler frames
// If begin_addr was not found, use the complete backtrace
if (skip > 0) {
std.mem.copyForwards(usize, addrs, addrs[skip..stack_trace.index]);
stack_trace.index -= skip;
}
}
/// This function is invoked when a crash happens. A crash is classified in `CrashReason`.
pub fn crashHandler(
reason: CrashReason,
@@ -308,67 +339,31 @@ pub fn crashHandler(
var trace_buf: std.builtin.StackTrace = undefined;
// If a trace was not provided, compute one now
const trace = @as(?*std.builtin.StackTrace, if (error_return_trace) |ert|
if (ert.index > 0)
ert
else
null
else
null) orelse get_backtrace: {
const trace = blk: {
if (error_return_trace) |ert| {
if (ert.index > 0) break :blk ert;
}
trace_buf = std.builtin.StackTrace{
.index = 0,
.instruction_addresses = &addr_buf,
};
const desired_begin_addr = begin_addr orelse @returnAddress();
std.debug.captureStackTrace(desired_begin_addr, &trace_buf);
// On Linux with glibc, always use backtrace() instead of Zig's StackIterator
// because Zig's frame pointer-based unwinding doesn't work reliably,
// especially on aarch64. glibc's backtrace() uses DWARF unwinding.
if (bun.Environment.isLinux and !bun.Environment.isMusl) {
const backtrace_fn = struct {
extern "c" fn backtrace(buffer: [*]?*anyopaque, size: c_int) c_int;
}.backtrace;
const count = backtrace_fn(@ptrCast(&addr_buf), addr_buf.len);
if (count > 0) {
trace_buf.index = @intCast(count);
// Skip frames until we find begin_addr (or close to it)
// backtrace() captures everything including crash handler frames
var skip: usize = 0;
var found_begin = false;
const tolerance: usize = 128;
for (addr_buf[0..trace_buf.index], 0..) |addr, i| {
// Check if this address is close to begin_addr (within tolerance)
const delta = if (addr >= desired_begin_addr)
addr - desired_begin_addr
else
desired_begin_addr - addr;
if (delta <= tolerance) {
skip = i;
found_begin = true;
break;
}
// Give up searching after 8 frames
if (i >= 8) break;
}
// Shift the addresses to skip crash handler frames
// If begin_addr was not found, use the complete backtrace
if (found_begin and skip > 0 and skip < trace_buf.index) {
const remaining = trace_buf.index - skip;
var j: usize = 0;
while (j < remaining) : (j += 1) {
addr_buf[j] = addr_buf[skip + j];
}
trace_buf.index = remaining;
}
if (comptime bun.Environment.isLinux and !bun.Environment.isMusl) {
var addr_buf_libc: [20]usize = undefined;
var trace_buf_libc: std.builtin.StackTrace = .{
.index = 0,
.instruction_addresses = &addr_buf_libc,
};
captureLibcBacktrace(desired_begin_addr, &trace_buf_libc);
// Use stack trace from glibc's backtrace() if it has more frames
if (trace_buf_libc.index > trace_buf.index) {
addr_buf = addr_buf_libc;
trace_buf.index = trace_buf_libc.index;
}
} else {
// Fall back to Zig's stack capture on other platforms
std.debug.captureStackTrace(desired_begin_addr, &trace_buf);
}
break :get_backtrace &trace_buf;
break :blk &trace_buf;
};
if (debug_trace) {