std.debug: rewrite panic

This commit is contained in:
Jacob Young
2026-02-12 13:10:07 -05:00
parent 251f54d1d7
commit 645ffe21cf
3 changed files with 76 additions and 137 deletions
+68 -101
View File
@@ -285,7 +285,7 @@ pub fn lockStderr(buffer: []u8) Io.LockedStderr {
const prev = io.swapCancelProtection(.blocked);
defer _ = io.swapCancelProtection(prev);
return io.lockStderr(buffer, null) catch |err| switch (err) {
error.Canceled => unreachable, // Cancel protection enabled above.
error.Canceled => unreachable, // blocked
};
}
@@ -463,13 +463,9 @@ pub fn panicExtra(
std.builtin.panic.call(msg, ret_addr);
}
/// Non-zero whenever the program triggered a panic.
/// The counter is incremented/decremented atomically.
var panicking = std.atomic.Value(u8).init(0);
/// Counts how many times the panic handler is invoked by this thread.
/// This is used to catch and handle panics triggered by the panic handler.
threadlocal var panic_stage: usize = 0;
threadlocal var recursive_panic_writer: ?*Io.Writer = null;
/// For backends that cannot handle the language features depended on by the
/// default panic handler, we will use a simpler implementation.
@@ -533,76 +529,53 @@ pub fn defaultPanic(msg: []const u8, first_trace_addr: ?usize) noreturn {
else => {},
}
// Don't try to cancel during a panic. No need to re-enable cancelation,
// because the panic handler doesn't return.
_ = std.Options.debug_io.swapCancelProtection(.blocked);
if (enable_segfault_handler) {
// If a segfault happens while panicking, we want it to actually segfault, not trigger
// the handler.
resetSegfaultHandler();
}
// There is very similar logic to the following in `handleSegfault`.
switch (panic_stage) {
0 => {
panic_stage = 1;
_ = panicking.fetchAdd(1, .seq_cst);
var discarding: Io.Writer.Discarding = .init(&.{});
const current_recursive_panic_writer = recursive_panic_writer;
recursive_panic_writer = &discarding.writer;
if (current_recursive_panic_writer) |writer| {
// A panic happened while trying to print a previous panic message.
writer.writeAll("aborting due to recursive panic\n") catch {};
} else trace: {
// Don't try to cancel during a panic. No need to re-enable cancelation,
// because the panic handler doesn't return.
_ = std.Options.debug_io.swapCancelProtection(.blocked);
trace: {
const stderr = lockStderr(&.{}).terminal();
defer unlockStderr();
const writer = stderr.writer;
const stderr = lockStderr(&.{}).terminal();
const writer = stderr.writer;
recursive_panic_writer = writer;
if (builtin.single_threaded) {
writer.print("panic: ", .{}) catch break :trace;
} else {
const current_thread_id = std.Thread.getCurrentId();
writer.print("thread {d} panic: ", .{current_thread_id}) catch break :trace;
}
writer.print("{s}\n", .{msg}) catch break :trace;
if (enable_segfault_handler) {
// If a segfault happens while panicking, we want it to actually segfault, not trigger
// the handler.
resetSegfaultHandler();
}
if (@errorReturnTrace()) |t| if (t.index > 0) {
writer.writeAll("error return context:\n") catch break :trace;
writeStackTrace(t, stderr) catch break :trace;
writer.writeAll("\nstack trace:\n") catch break :trace;
};
writeCurrentStackTrace(.{
.first_address = first_trace_addr orelse @returnAddress(),
.allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
}, stderr) catch break :trace;
}
if (@hasDecl(root, "debug") and @hasDecl(root.debug, "printCrashContext")) {
root.debug.printCrashContext(stderr);
}
waitForOtherThreadToFinishPanicking();
},
1 => {
panic_stage = 2;
// A panic happened while trying to print a previous panic message.
// We're still holding the mutex but that's fine as we're going to
// call abort().
const stderr = lockStderr(&.{}).terminal();
stderr.writer.writeAll("aborting due to recursive panic\n") catch {};
},
else => {}, // Panicked while printing the recursive panic message.
if (builtin.single_threaded) {
writer.print("panic: ", .{}) catch break :trace;
} else {
const current_thread_id = std.Thread.getCurrentId();
writer.print("thread {d} panic: ", .{current_thread_id}) catch break :trace;
}
writer.print("{s}\n", .{msg}) catch break :trace;
if (@errorReturnTrace()) |t| if (t.index > 0) {
writer.writeAll("error return context:\n") catch break :trace;
writeStackTrace(t, stderr) catch break :trace;
writer.writeAll("\nstack trace:\n") catch break :trace;
};
writeCurrentStackTrace(.{
.first_address = first_trace_addr orelse @returnAddress(),
.allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
}, stderr) catch break :trace;
}
std.process.abort();
}
/// Must be called only after adding 1 to `panicking`. There are three callsites.
fn waitForOtherThreadToFinishPanicking() void {
if (panicking.fetchSub(1, .seq_cst) != 1) {
// Another thread is panicking, wait for the last one to finish
// and call abort()
if (builtin.single_threaded) unreachable;
// Sleep forever without hammering the CPU
var futex: u32 = 0;
while (true) std.Options.debug_io.futexWaitUncancelable(u32, &futex, 0);
unreachable;
}
}
pub const StackUnwindOptions = struct {
/// If not `null`, we will ignore all frames up until this return address. This is typically
/// used to omit intermediate handling code (for instance, a panic handler and its machinery)
@@ -1535,44 +1508,38 @@ fn handleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContextPtr) noret
}
pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContextPtr) noreturn {
// Don't try to cancel during a segfault. No need to re-enable cancelation,
// because the segfault handler doesn't return.
_ = std.Options.debug_io.swapCancelProtection(.blocked);
// There is very similar logic to the following in `defaultPanic`.
switch (panic_stage) {
0 => {
panic_stage = 1;
_ = panicking.fetchAdd(1, .seq_cst);
var discarding: Io.Writer.Discarding = .init(&.{});
const current_recursive_panic_writer = recursive_panic_writer;
recursive_panic_writer = &discarding.writer;
if (current_recursive_panic_writer) |writer| {
// A segfault happened while trying to print a previous panic message.
writer.writeAll("aborting due to recursive panic\n") catch {};
} else trace: {
// Don't try to cancel during a segfault. No need to re-enable cancelation,
// because the segfault handler doesn't return.
_ = std.Options.debug_io.swapCancelProtection(.blocked);
trace: {
const stderr = lockStderr(&.{}).terminal();
defer unlockStderr();
const stderr = lockStderr(&.{}).terminal();
const writer = stderr.writer;
recursive_panic_writer = writer;
if (addr) |a| {
stderr.writer.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace;
} else {
stderr.writer.print("{s} (no address available)\n", .{name}) catch break :trace;
}
if (opt_ctx) |context| {
writeCurrentStackTrace(.{
.context = context,
.allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
}, stderr) catch break :trace;
}
}
},
1 => {
panic_stage = 2;
// A segfault happened while trying to print a previous panic message.
// We're still holding the mutex but that's fine as we're going to
// call abort().
const stderr = lockStderr(&.{}).terminal();
stderr.writer.writeAll("aborting due to recursive panic\n") catch {};
},
else => {}, // Panicked while printing the recursive panic message.
if (@hasDecl(root, "debug") and @hasDecl(root.debug, "printCrashContext")) {
root.debug.printCrashContext(stderr);
}
if (addr) |a| {
writer.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace;
} else {
writer.print("{s} (no address available)\n", .{name}) catch break :trace;
}
if (opt_ctx) |context| {
writeCurrentStackTrace(.{
.context = context,
.allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
}, stderr) catch break :trace;
}
}
// We cannot allow the signal handler to return because when it runs the original instruction
// again, the memory may be mapped and undefined behavior would occur rather than repeating
// the segfault. So we simply abort here.
+3 -34
View File
@@ -1,30 +1,8 @@
/// We override the panic implementation to our own one, so we can print our own information before
/// calling the default panic handler. This declaration must be re-exposed from `@import("root")`.
pub const panic = std.debug.FullPanic(panicImpl);
/// We let std install its segfault handler, but we override the target-agnostic handler it calls,
/// so we can print our own information before calling the default segfault logic. This declaration
/// must be re-exposed from `@import("root")`.
pub const debug = struct {
pub const handleSegfault = handleSegfaultImpl;
};
/// Printed in panic messages when suggesting a command to run, allowing copy-pasting the command.
/// Set by `main` as soon as arguments are known. The value here is a default in case we somehow
/// crash earlier than that.
pub var zig_argv0: []const u8 = "zig";
fn handleSegfaultImpl(addr: ?usize, name: []const u8, opt_ctx: ?std.debug.CpuContextPtr) noreturn {
@branchHint(.cold);
dumpCrashContext() catch {};
std.debug.defaultHandleSegfault(addr, name, opt_ctx);
}
fn panicImpl(msg: []const u8, first_trace_addr: ?usize) noreturn {
@branchHint(.cold);
dumpCrashContext() catch {};
std.debug.defaultPanic(msg, first_trace_addr orelse @returnAddress());
}
pub const AnalyzeBody = struct {
parent: ?*AnalyzeBody,
sema: *Sema,
@@ -68,23 +46,14 @@ pub const CodegenFunc = struct {
}
};
fn dumpCrashContext() Io.Writer.Error!void {
pub fn dumpCrashContext(terminal: Io.Terminal) Io.Writer.Error!void {
const S = struct {
/// In the case of recursive panics or segfaults, don't print the context for a second time.
threadlocal var already_dumped = false;
/// TODO: make this unnecessary. It exists because `print_zir` currently needs an allocator,
/// but that shouldn't be necessary---it's already only used in one place.
threadlocal var crash_heap: [64 * 1024]u8 = undefined;
var crash_heap: [64 * 1024]u8 = undefined;
};
if (S.already_dumped) return;
S.already_dumped = true;
// TODO: this does mean that a different thread could grab the stderr mutex between the context
// and the actual panic printing, which would be quite confusing.
const stderr = std.debug.lockStderr(&.{});
defer std.debug.unlockStderr();
const w = &stderr.file_writer.interface;
const w = terminal.writer;
try w.writeAll("Compiler crash context:\n");
if (CodegenFunc.current) |*cg| {
+5 -2
View File
@@ -56,8 +56,11 @@ const crash_report_enabled = switch (build_options.io_mode) {
.threaded => build_options.enable_debug_extensions,
.evented => false, // would use threadlocals in a way incompatible with evented
};
pub const panic = if (crash_report_enabled) crash_report.panic else std.debug.FullPanic(std.debug.defaultPanic);
pub const debug = if (crash_report_enabled) crash_report.debug else struct {};
pub const debug = if (crash_report_enabled) struct {
pub fn printCrashContext(terminal: Io.Terminal) void {
crash_report.dumpCrashContext(terminal) catch {};
}
} else struct {};
var preopens: std.process.Preopens = .empty;
pub fn wasi_cwd() Io.Dir {