authorgravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2026-02-12 13:10:07-05:00
committergravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2026-02-12 13:16:26-05:00
log645ffe21cf77400c88ae9356e86f266cb1b5839f
tree395fa67976e6f4c31a822c6117901d713cab036e
parent251f54d1d7d0f871f551c54d77f1a8238a041983

std.debug: rewrite panic


3 files changed, 80 insertions(+), 141 deletions(-)

lib/std/debug.zig+72-105
......@@ -285,7 +285,7 @@ pub fn lockStderr(buffer: []u8) Io.LockedStderr {
285285 const prev = io.swapCancelProtection(.blocked);
286286 defer _ = io.swapCancelProtection(prev);
287287 return io.lockStderr(buffer, null) catch |err| switch (err) {
288 error.Canceled => unreachable, // Cancel protection enabled above.
288 error.Canceled => unreachable, // blocked
289289 };
290290}
291291
......@@ -463,13 +463,9 @@ pub fn panicExtra(
463463 std.builtin.panic.call(msg, ret_addr);
464464}
465465
466/// Non-zero whenever the program triggered a panic.
467/// The counter is incremented/decremented atomically.
468var panicking = std.atomic.Value(u8).init(0);
469
470466/// Counts how many times the panic handler is invoked by this thread.
471467/// This is used to catch and handle panics triggered by the panic handler.
472threadlocal var panic_stage: usize = 0;
468threadlocal var recursive_panic_writer: ?*Io.Writer = null;
473469
474470/// For backends that cannot handle the language features depended on by the
475471/// default panic handler, we will use a simpler implementation.
......@@ -533,74 +529,51 @@ pub fn defaultPanic(msg: []const u8, first_trace_addr: ?usize) noreturn {
533529 else => {},
534530 }
535531
536 // Don't try to cancel during a panic. No need to re-enable cancelation,
537 // because the panic handler doesn't return.
538 _ = std.Options.debug_io.swapCancelProtection(.blocked);
539
540 if (enable_segfault_handler) {
541 // If a segfault happens while panicking, we want it to actually segfault, not trigger
542 // the handler.
543 resetSegfaultHandler();
544 }
545
546532 // There is very similar logic to the following in `handleSegfault`.
547 switch (panic_stage) {
548 0 => {
549 panic_stage = 1;
550 _ = panicking.fetchAdd(1, .seq_cst);
551
552 trace: {
553 const stderr = lockStderr(&.{}).terminal();
554 defer unlockStderr();
555 const writer = stderr.writer;
556
557 if (builtin.single_threaded) {
558 writer.print("panic: ", .{}) catch break :trace;
559 } else {
560 const current_thread_id = std.Thread.getCurrentId();
561 writer.print("thread {d} panic: ", .{current_thread_id}) catch break :trace;
562 }
563 writer.print("{s}\n", .{msg}) catch break :trace;
564
565 if (@errorReturnTrace()) |t| if (t.index > 0) {
566 writer.writeAll("error return context:\n") catch break :trace;
567 writeStackTrace(t, stderr) catch break :trace;
568 writer.writeAll("\nstack trace:\n") catch break :trace;
569 };
570 writeCurrentStackTrace(.{
571 .first_address = first_trace_addr orelse @returnAddress(),
572 .allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
573 }, stderr) catch break :trace;
574 }
533 var discarding: Io.Writer.Discarding = .init(&.{});
534 const current_recursive_panic_writer = recursive_panic_writer;
535 recursive_panic_writer = &discarding.writer;
536 if (current_recursive_panic_writer) |writer| {
537 // A panic happened while trying to print a previous panic message.
538 writer.writeAll("aborting due to recursive panic\n") catch {};
539 } else trace: {
540 // Don't try to cancel during a panic. No need to re-enable cancelation,
541 // because the panic handler doesn't return.
542 _ = std.Options.debug_io.swapCancelProtection(.blocked);
543
544 const stderr = lockStderr(&.{}).terminal();
545 const writer = stderr.writer;
546 recursive_panic_writer = writer;
547
548 if (enable_segfault_handler) {
549 // If a segfault happens while panicking, we want it to actually segfault, not trigger
550 // the handler.
551 resetSegfaultHandler();
552 }
575553
576 waitForOtherThreadToFinishPanicking();
577 },
578 1 => {
579 panic_stage = 2;
580 // A panic happened while trying to print a previous panic message.
581 // We're still holding the mutex but that's fine as we're going to
582 // call abort().
583 const stderr = lockStderr(&.{}).terminal();
584 stderr.writer.writeAll("aborting due to recursive panic\n") catch {};
585 },
586 else => {}, // Panicked while printing the recursive panic message.
587 }
554 if (@hasDecl(root, "debug") and @hasDecl(root.debug, "printCrashContext")) {
555 root.debug.printCrashContext(stderr);
556 }
588557
589 std.process.abort();
590}
558 if (builtin.single_threaded) {
559 writer.print("panic: ", .{}) catch break :trace;
560 } else {
561 const current_thread_id = std.Thread.getCurrentId();
562 writer.print("thread {d} panic: ", .{current_thread_id}) catch break :trace;
563 }
564 writer.print("{s}\n", .{msg}) catch break :trace;
591565
592/// Must be called only after adding 1 to `panicking`. There are three callsites.
593fn waitForOtherThreadToFinishPanicking() void {
594 if (panicking.fetchSub(1, .seq_cst) != 1) {
595 // Another thread is panicking, wait for the last one to finish
596 // and call abort()
597 if (builtin.single_threaded) unreachable;
598
599 // Sleep forever without hammering the CPU
600 var futex: u32 = 0;
601 while (true) std.Options.debug_io.futexWaitUncancelable(u32, &futex, 0);
602 unreachable;
566 if (@errorReturnTrace()) |t| if (t.index > 0) {
567 writer.writeAll("error return context:\n") catch break :trace;
568 writeStackTrace(t, stderr) catch break :trace;
569 writer.writeAll("\nstack trace:\n") catch break :trace;
570 };
571 writeCurrentStackTrace(.{
572 .first_address = first_trace_addr orelse @returnAddress(),
573 .allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
574 }, stderr) catch break :trace;
603575 }
576 std.process.abort();
604577}
605578
606579pub const StackUnwindOptions = struct {
......@@ -1535,44 +1508,38 @@ fn handleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContextPtr) noret
15351508}
15361509
15371510pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContextPtr) noreturn {
1538 // Don't try to cancel during a segfault. No need to re-enable cancelation,
1539 // because the segfault handler doesn't return.
1540 _ = std.Options.debug_io.swapCancelProtection(.blocked);
1541
15421511 // There is very similar logic to the following in `defaultPanic`.
1543 switch (panic_stage) {
1544 0 => {
1545 panic_stage = 1;
1546 _ = panicking.fetchAdd(1, .seq_cst);
1547
1548 trace: {
1549 const stderr = lockStderr(&.{}).terminal();
1550 defer unlockStderr();
1551
1552 if (addr) |a| {
1553 stderr.writer.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace;
1554 } else {
1555 stderr.writer.print("{s} (no address available)\n", .{name}) catch break :trace;
1556 }
1557 if (opt_ctx) |context| {
1558 writeCurrentStackTrace(.{
1559 .context = context,
1560 .allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
1561 }, stderr) catch break :trace;
1562 }
1563 }
1564 },
1565 1 => {
1566 panic_stage = 2;
1567 // A segfault happened while trying to print a previous panic message.
1568 // We're still holding the mutex but that's fine as we're going to
1569 // call abort().
1570 const stderr = lockStderr(&.{}).terminal();
1571 stderr.writer.writeAll("aborting due to recursive panic\n") catch {};
1572 },
1573 else => {}, // Panicked while printing the recursive panic message.
1574 }
1512 var discarding: Io.Writer.Discarding = .init(&.{});
1513 const current_recursive_panic_writer = recursive_panic_writer;
1514 recursive_panic_writer = &discarding.writer;
1515 if (current_recursive_panic_writer) |writer| {
1516 // A segfault happened while trying to print a previous panic message.
1517 writer.writeAll("aborting due to recursive panic\n") catch {};
1518 } else trace: {
1519 // Don't try to cancel during a segfault. No need to re-enable cancelation,
1520 // because the segfault handler doesn't return.
1521 _ = std.Options.debug_io.swapCancelProtection(.blocked);
1522
1523 const stderr = lockStderr(&.{}).terminal();
1524 const writer = stderr.writer;
1525 recursive_panic_writer = writer;
1526
1527 if (@hasDecl(root, "debug") and @hasDecl(root.debug, "printCrashContext")) {
1528 root.debug.printCrashContext(stderr);
1529 }
15751530
1531 if (addr) |a| {
1532 writer.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace;
1533 } else {
1534 writer.print("{s} (no address available)\n", .{name}) catch break :trace;
1535 }
1536 if (opt_ctx) |context| {
1537 writeCurrentStackTrace(.{
1538 .context = context,
1539 .allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
1540 }, stderr) catch break :trace;
1541 }
1542 }
15761543 // We cannot allow the signal handler to return because when it runs the original instruction
15771544 // again, the memory may be mapped and undefined behavior would occur rather than repeating
15781545 // the segfault. So we simply abort here.
src/crash_report.zig+3-34
......@@ -1,30 +1,8 @@
1/// We override the panic implementation to our own one, so we can print our own information before
2/// calling the default panic handler. This declaration must be re-exposed from `@import("root")`.
3pub const panic = std.debug.FullPanic(panicImpl);
4
5/// We let std install its segfault handler, but we override the target-agnostic handler it calls,
6/// so we can print our own information before calling the default segfault logic. This declaration
7/// must be re-exposed from `@import("root")`.
8pub const debug = struct {
9 pub const handleSegfault = handleSegfaultImpl;
10};
11
121/// Printed in panic messages when suggesting a command to run, allowing copy-pasting the command.
132/// Set by `main` as soon as arguments are known. The value here is a default in case we somehow
143/// crash earlier than that.
154pub var zig_argv0: []const u8 = "zig";
165
17fn handleSegfaultImpl(addr: ?usize, name: []const u8, opt_ctx: ?std.debug.CpuContextPtr) noreturn {
18 @branchHint(.cold);
19 dumpCrashContext() catch {};
20 std.debug.defaultHandleSegfault(addr, name, opt_ctx);
21}
22fn panicImpl(msg: []const u8, first_trace_addr: ?usize) noreturn {
23 @branchHint(.cold);
24 dumpCrashContext() catch {};
25 std.debug.defaultPanic(msg, first_trace_addr orelse @returnAddress());
26}
27
286pub const AnalyzeBody = struct {
297 parent: ?*AnalyzeBody,
308 sema: *Sema,
......@@ -68,23 +46,14 @@ pub const CodegenFunc = struct {
6846 }
6947};
7048
71fn dumpCrashContext() Io.Writer.Error!void {
49pub fn dumpCrashContext(terminal: Io.Terminal) Io.Writer.Error!void {
7250 const S = struct {
73 /// In the case of recursive panics or segfaults, don't print the context for a second time.
74 threadlocal var already_dumped = false;
7551 /// TODO: make this unnecessary. It exists because `print_zir` currently needs an allocator,
7652 /// but that shouldn't be necessary---it's already only used in one place.
77 threadlocal var crash_heap: [64 * 1024]u8 = undefined;
53 var crash_heap: [64 * 1024]u8 = undefined;
7854 };
79 if (S.already_dumped) return;
80 S.already_dumped = true;
81
82 // TODO: this does mean that a different thread could grab the stderr mutex between the context
83 // and the actual panic printing, which would be quite confusing.
84 const stderr = std.debug.lockStderr(&.{});
85 defer std.debug.unlockStderr();
86 const w = &stderr.file_writer.interface;
8755
56 const w = terminal.writer;
8857 try w.writeAll("Compiler crash context:\n");
8958
9059 if (CodegenFunc.current) |*cg| {
src/main.zig+5-2
......@@ -56,8 +56,11 @@ const crash_report_enabled = switch (build_options.io_mode) {
5656 .threaded => build_options.enable_debug_extensions,
5757 .evented => false, // would use threadlocals in a way incompatible with evented
5858};
59pub const panic = if (crash_report_enabled) crash_report.panic else std.debug.FullPanic(std.debug.defaultPanic);
60pub const debug = if (crash_report_enabled) crash_report.debug else struct {};
59pub const debug = if (crash_report_enabled) struct {
60 pub fn printCrashContext(terminal: Io.Terminal) void {
61 crash_report.dumpCrashContext(terminal) catch {};
62 }
63} else struct {};
6164
6265var preopens: std.process.Preopens = .empty;
6366pub fn wasi_cwd() Io.Dir {