diff --git a/src/main.zig b/src/main.zig index 4c2f74b..577572d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -148,8 +148,10 @@ pub fn main() anyerror!void { return list_languages.list(a, stdout.writer(), tty_config); } - if (builtin.os.tag != .windows) - if (std.posix.getenv("JITDEBUG")) |_| thespian.install_debugger(); + if (std.posix.getenv("JITDEBUG")) |_| + thespian.install_debugger() + else if (@hasDecl(renderer, "install_crash_handler")) + renderer.install_crash_handler(); if (args.debug_wait) { std.debug.print("press return to start", .{}); diff --git a/src/renderer/vaxis/renderer.zig b/src/renderer/vaxis/renderer.zig index 016738b..8d3cd1f 100644 --- a/src/renderer/vaxis/renderer.zig +++ b/src/renderer/vaxis/renderer.zig @@ -118,6 +118,34 @@ pub fn panic_in_progress() bool { return in_panic.load(.acquire); } +pub fn install_crash_handler() void { + if (!std.debug.have_segfault_handling_support) { + @compileError("segfault handler not supported for this target"); + } + const act = std.posix.Sigaction{ + .handler = .{ .sigaction = handle_crash }, + .mask = std.posix.empty_sigset, + .flags = (std.posix.SA.SIGINFO | std.posix.SA.RESTART), + }; + + std.posix.sigaction(std.posix.SIG.BUS, &act, null); + std.posix.sigaction(std.posix.SIG.SEGV, &act, null); + std.posix.sigaction(std.posix.SIG.ABRT, &act, null); + std.posix.sigaction(std.posix.SIG.FPE, &act, null); + std.posix.sigaction(std.posix.SIG.ILL, &act, null); +} + +fn handle_crash(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) noreturn { + in_panic.store(true, .release); + const cleanup = panic_cleanup; + panic_cleanup = null; + if (cleanup) |self| { + self.vx.deinit(self.allocator, self.tty.anyWriter()); + self.tty.deinit(); + } + @import("std/debug.zig").handleSegfaultPosix(sig, info, ctx_ptr); +} + pub fn run(self: *Self) Error!void { self.vx.sgr = .legacy; self.vx.conpty_hacks = true; diff --git a/src/renderer/vaxis/std/debug.zig b/src/renderer/vaxis/std/debug.zig new file mode 100644 index 0000000..d155d5a --- /dev/null +++ b/src/renderer/vaxis/std/debug.zig @@ -0,0 +1,1732 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const math = std.math; +const mem = std.mem; +const io = std.io; +const posix = std.posix; +const fs = std.fs; +const testing = std.testing; +const root = @import("root"); +const File = std.fs.File; +const windows = std.os.windows; +const native_arch = builtin.cpu.arch; +const native_os = builtin.os.tag; +const native_endian = native_arch.endian(); + +pub const MemoryAccessor = std.debug.MemoryAccessor; +pub const FixedBufferReader = std.debug.FixedBufferReader.zig; +pub const Dwarf = std.debug.Dwarf; +pub const Pdb = std.debug.Pdb; +pub const SelfInfo = std.debug.SelfInfo; +pub const Info = std.debug.Info; +pub const Coverage = std.debug.Coverage; + +pub const simple_panic = std.debug.simple_panic; +pub const no_panic = std.debug.no_panic; + +/// A fully-featured panic handler namespace which lowers all panics to calls to `panicFn`. +/// Safety panics will use formatted printing to provide a meaningful error message. +/// The signature of `panicFn` should match that of `defaultPanic`. +pub fn FullPanic(comptime panicFn: fn ([]const u8, ?usize) noreturn) type { + return struct { + pub const call = panicFn; + pub fn sentinelMismatch(expected: anytype, found: @TypeOf(expected)) noreturn { + @branchHint(.cold); + std.debug.panicExtra(@returnAddress(), "sentinel mismatch: expected {any}, found {any}", .{ + expected, found, + }); + } + pub fn unwrapError(err: anyerror) noreturn { + @branchHint(.cold); + std.debug.panicExtra(@returnAddress(), "attempt to unwrap error: {s}", .{@errorName(err)}); + } + pub fn outOfBounds(index: usize, len: usize) noreturn { + @branchHint(.cold); + std.debug.panicExtra(@returnAddress(), "index out of bounds: index {d}, len {d}", .{ index, len }); + } + pub fn startGreaterThanEnd(start: usize, end: usize) noreturn { + @branchHint(.cold); + std.debug.panicExtra(@returnAddress(), "start index {d} is larger than end index {d}", .{ start, end }); + } + pub fn inactiveUnionField(active: anytype, accessed: @TypeOf(active)) noreturn { + @branchHint(.cold); + std.debug.panicExtra(@returnAddress(), "access of union field '{s}' while field '{s}' is active", .{ + @tagName(accessed), @tagName(active), + }); + } + pub fn sliceCastLenRemainder(src_len: usize) noreturn { + @branchHint(.cold); + std.debug.panicExtra(@returnAddress(), "slice length '{d}' does not divide exactly into destination elements", .{src_len}); + } + pub fn reachedUnreachable() noreturn { + @branchHint(.cold); + call("reached unreachable code", @returnAddress()); + } + pub fn unwrapNull() noreturn { + @branchHint(.cold); + call("attempt to use null value", @returnAddress()); + } + pub fn castToNull() noreturn { + @branchHint(.cold); + call("cast causes pointer to be null", @returnAddress()); + } + pub fn incorrectAlignment() noreturn { + @branchHint(.cold); + call("incorrect alignment", @returnAddress()); + } + pub fn invalidErrorCode() noreturn { + @branchHint(.cold); + call("invalid error code", @returnAddress()); + } + pub fn castTruncatedData() noreturn { + @branchHint(.cold); + call("integer cast truncated bits", @returnAddress()); + } + pub fn negativeToUnsigned() noreturn { + @branchHint(.cold); + call("attempt to cast negative value to unsigned integer", @returnAddress()); + } + pub fn integerOverflow() noreturn { + @branchHint(.cold); + call("integer overflow", @returnAddress()); + } + pub fn shlOverflow() noreturn { + @branchHint(.cold); + call("left shift overflowed bits", @returnAddress()); + } + pub fn shrOverflow() noreturn { + @branchHint(.cold); + call("right shift overflowed bits", @returnAddress()); + } + pub fn divideByZero() noreturn { + @branchHint(.cold); + call("division by zero", @returnAddress()); + } + pub fn exactDivisionRemainder() noreturn { + @branchHint(.cold); + call("exact division produced remainder", @returnAddress()); + } + pub fn integerPartOutOfBounds() noreturn { + @branchHint(.cold); + call("integer part of floating point value out of bounds", @returnAddress()); + } + pub fn corruptSwitch() noreturn { + @branchHint(.cold); + call("switch on corrupt value", @returnAddress()); + } + pub fn shiftRhsTooBig() noreturn { + @branchHint(.cold); + call("shift amount is greater than the type size", @returnAddress()); + } + pub fn invalidEnumValue() noreturn { + @branchHint(.cold); + call("invalid enum value", @returnAddress()); + } + pub fn forLenMismatch() noreturn { + @branchHint(.cold); + call("for loop over objects with non-equal lengths", @returnAddress()); + } + pub fn memcpyLenMismatch() noreturn { + @branchHint(.cold); + call("@memcpy arguments have non-equal lengths", @returnAddress()); + } + pub fn memcpyAlias() noreturn { + @branchHint(.cold); + call("@memcpy arguments alias", @returnAddress()); + } + pub fn noreturnReturned() noreturn { + @branchHint(.cold); + call("'noreturn' function returned", @returnAddress()); + } + }; +} + +pub const SourceLocation = std.debug.SourceLocation; + +pub const Symbol = std.debug.Symbol; + +/// Deprecated because it returns the optimization mode of the standard +/// library, when the caller probably wants to use the optimization mode of +/// their own module. +pub const runtime_safety = switch (builtin.mode) { + .Debug, .ReleaseSafe => true, + .ReleaseFast, .ReleaseSmall => false, +}; + +pub const sys_can_stack_trace = switch (builtin.cpu.arch) { + // Observed to go into an infinite loop. + // TODO: Make this work. + .mips, + .mipsel, + .mips64, + .mips64el, + .s390x, + => false, + + // `@returnAddress()` in LLVM 10 gives + // "Non-Emscripten WebAssembly hasn't implemented __builtin_return_address". + .wasm32, + .wasm64, + => native_os == .emscripten, + + // `@returnAddress()` is unsupported in LLVM 13. + .bpfel, + .bpfeb, + => false, + + else => true, +}; + +/// Allows the caller to freely write to stderr until `unlockStdErr` is called. +/// +/// During the lock, any `std.Progress` information is cleared from the terminal. +pub fn lockStdErr() void { + std.Progress.lockStdErr(); +} + +pub fn unlockStdErr() void { + std.Progress.unlockStdErr(); +} + +/// Print to stderr, unbuffered, and silently returning on failure. Intended +/// for use in "printf debugging." Use `std.log` functions for proper logging. +pub fn print(comptime fmt: []const u8, args: anytype) void { + lockStdErr(); + defer unlockStdErr(); + const stderr = io.getStdErr().writer(); + nosuspend stderr.print(fmt, args) catch return; +} + +pub fn getStderrMutex() *std.Thread.Mutex { + @compileError("deprecated. call std.debug.lockStdErr() and std.debug.unlockStdErr() instead which will integrate properly with std.Progress"); +} + +/// TODO multithreaded awareness +var self_debug_info: ?SelfInfo = null; + +pub fn getSelfDebugInfo() !*SelfInfo { + if (self_debug_info) |*info| { + return info; + } else { + self_debug_info = try SelfInfo.open(getDebugInfoAllocator()); + return &self_debug_info.?; + } +} + +/// Tries to print a hexadecimal view of the bytes, unbuffered, and ignores any error returned. +/// Obtains the stderr mutex while dumping. +pub fn dumpHex(bytes: []const u8) void { + lockStdErr(); + defer unlockStdErr(); + dumpHexFallible(bytes) catch {}; +} + +/// Prints a hexadecimal view of the bytes, unbuffered, returning any error that occurs. +pub fn dumpHexFallible(bytes: []const u8) !void { + const stderr = std.io.getStdErr(); + const ttyconf = std.io.tty.detectConfig(stderr); + const writer = stderr.writer(); + try dumpHexInternal(bytes, ttyconf, writer); +} + +fn dumpHexInternal(bytes: []const u8, ttyconf: std.io.tty.Config, writer: anytype) !void { + var chunks = mem.window(u8, bytes, 16, 16); + while (chunks.next()) |window| { + // 1. Print the address. + const address = (@intFromPtr(bytes.ptr) + 0x10 * (std.math.divCeil(usize, chunks.index orelse bytes.len, 16) catch unreachable)) - 0x10; + try ttyconf.setColor(writer, .dim); + // We print the address in lowercase and the bytes in uppercase hexadecimal to distinguish them more. + // Also, make sure all lines are aligned by padding the address. + try writer.print("{x:0>[1]} ", .{ address, @sizeOf(usize) * 2 }); + try ttyconf.setColor(writer, .reset); + + // 2. Print the bytes. + for (window, 0..) |byte, index| { + try writer.print("{X:0>2} ", .{byte}); + if (index == 7) try writer.writeByte(' '); + } + try writer.writeByte(' '); + if (window.len < 16) { + var missing_columns = (16 - window.len) * 3; + if (window.len < 8) missing_columns += 1; + try writer.writeByteNTimes(' ', missing_columns); + } + + // 3. Print the characters. + for (window) |byte| { + if (std.ascii.isPrint(byte)) { + try writer.writeByte(byte); + } else { + // Related: https://github.com/ziglang/zig/issues/7600 + if (ttyconf == .windows_api) { + try writer.writeByte('.'); + continue; + } + + // Let's print some common control codes as graphical Unicode symbols. + // We don't want to do this for all control codes because most control codes apart from + // the ones that Zig has escape sequences for are likely not very useful to print as symbols. + switch (byte) { + '\n' => try writer.writeAll("␊"), + '\r' => try writer.writeAll("␍"), + '\t' => try writer.writeAll("␉"), + else => try writer.writeByte('.'), + } + } + } + try writer.writeByte('\n'); + } +} + +test dumpHexInternal { + const bytes: []const u8 = &.{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x01, 0x12, 0x13 }; + var output = std.ArrayList(u8).init(std.testing.allocator); + defer output.deinit(); + try dumpHexInternal(bytes, .no_color, output.writer()); + const expected = try std.fmt.allocPrint(std.testing.allocator, + \\{x:0>[2]} 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF .."3DUfw........ + \\{x:0>[2]} 01 12 13 ... + \\ + , .{ + @intFromPtr(bytes.ptr), + @intFromPtr(bytes.ptr) + 16, + @sizeOf(usize) * 2, + }); + defer std.testing.allocator.free(expected); + try std.testing.expectEqualStrings(expected, output.items); +} + +/// Tries to print the current stack trace to stderr, unbuffered, and ignores any error returned. +/// TODO multithreaded awareness +pub fn dumpCurrentStackTrace(start_addr: ?usize) void { + nosuspend { + if (builtin.target.cpu.arch.isWasm()) { + if (native_os == .wasi) { + const stderr = io.getStdErr().writer(); + stderr.print("Unable to dump stack trace: not implemented for Wasm\n", .{}) catch return; + } + return; + } + const stderr = io.getStdErr().writer(); + if (builtin.strip_debug_info) { + stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; + return; + } + const debug_info = getSelfDebugInfo() catch |err| { + stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; + return; + }; + writeCurrentStackTrace(stderr, debug_info, io.tty.detectConfig(io.getStdErr()), start_addr) catch |err| { + stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; + return; + }; + } +} + +pub const have_ucontext = posix.ucontext_t != void; + +/// Platform-specific thread state. This contains register state, and on some platforms +/// information about the stack. This is not safe to trivially copy, because some platforms +/// use internal pointers within this structure. To make a copy, use `copyContext`. +pub const ThreadContext = blk: { + if (native_os == .windows) { + break :blk windows.CONTEXT; + } else if (have_ucontext) { + break :blk posix.ucontext_t; + } else { + break :blk void; + } +}; + +/// Copies one context to another, updating any internal pointers +pub fn copyContext(source: *const ThreadContext, dest: *ThreadContext) void { + if (!have_ucontext) return {}; + dest.* = source.*; + relocateContext(dest); +} + +/// Updates any internal pointers in the context to reflect its current location +pub fn relocateContext(context: *ThreadContext) void { + return switch (native_os) { + .macos => { + context.mcontext = &context.__mcontext_data; + }, + else => {}, + }; +} + +pub const have_getcontext = @TypeOf(posix.system.getcontext) != void; + +/// Capture the current context. The register values in the context will reflect the +/// state after the platform `getcontext` function returns. +/// +/// It is valid to call this if the platform doesn't have context capturing support, +/// in that case false will be returned. +pub inline fn getContext(context: *ThreadContext) bool { + if (native_os == .windows) { + context.* = std.mem.zeroes(windows.CONTEXT); + windows.ntdll.RtlCaptureContext(context); + return true; + } + + const result = have_getcontext and posix.system.getcontext(context) == 0; + if (native_os == .macos) { + assert(context.mcsize == @sizeOf(std.c.mcontext_t)); + + // On aarch64-macos, the system getcontext doesn't write anything into the pc + // register slot, it only writes lr. This makes the context consistent with + // other aarch64 getcontext implementations which write the current lr + // (where getcontext will return to) into both the lr and pc slot of the context. + if (native_arch == .aarch64) context.mcontext.ss.pc = context.mcontext.ss.lr; + } + + return result; +} + +/// Tries to print the stack trace starting from the supplied base pointer to stderr, +/// unbuffered, and ignores any error returned. +/// TODO multithreaded awareness +pub fn dumpStackTraceFromBase(context: *ThreadContext) void { + nosuspend { + if (builtin.target.cpu.arch.isWasm()) { + if (native_os == .wasi) { + const stderr = io.getStdErr().writer(); + stderr.print("Unable to dump stack trace: not implemented for Wasm\n", .{}) catch return; + } + return; + } + const stderr = io.getStdErr().writer(); + if (builtin.strip_debug_info) { + stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; + return; + } + const debug_info = getSelfDebugInfo() catch |err| { + stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; + return; + }; + const tty_config = io.tty.detectConfig(io.getStdErr()); + if (native_os == .windows) { + // On x86_64 and aarch64, the stack will be unwound using RtlVirtualUnwind using the context + // provided by the exception handler. On x86, RtlVirtualUnwind doesn't exist. Instead, a new backtrace + // will be captured and frames prior to the exception will be filtered. + // The caveat is that RtlCaptureStackBackTrace does not include the KiUserExceptionDispatcher frame, + // which is where the IP in `context` points to, so it can't be used as start_addr. + // Instead, start_addr is recovered from the stack. + const start_addr = if (builtin.cpu.arch == .x86) @as(*const usize, @ptrFromInt(context.getRegs().bp + 4)).* else null; + writeStackTraceWindows(stderr, debug_info, tty_config, context, start_addr) catch return; + return; + } + + var it = StackIterator.initWithContext(null, debug_info, context) catch return; + defer it.deinit(); + + // DWARF unwinding on aarch64-macos is not complete so we need to get pc address from mcontext + const pc_addr = if (builtin.target.os.tag.isDarwin() and native_arch == .aarch64) + context.mcontext.ss.pc + else + it.unwind_state.?.dwarf_context.pc; + printSourceAtAddress(debug_info, stderr, pc_addr, tty_config) catch return; + + while (it.next()) |return_address| { + printLastUnwindError(&it, debug_info, stderr, tty_config); + + // On arm64 macOS, the address of the last frame is 0x0 rather than 0x1 as on x86_64 macOS, + // therefore, we do a check for `return_address == 0` before subtracting 1 from it to avoid + // an overflow. We do not need to signal `StackIterator` as it will correctly detect this + // condition on the subsequent iteration and return `null` thus terminating the loop. + // same behaviour for x86-windows-msvc + const address = if (return_address == 0) return_address else return_address - 1; + printSourceAtAddress(debug_info, stderr, address, tty_config) catch return; + } else printLastUnwindError(&it, debug_info, stderr, tty_config); + } +} + +/// Returns a slice with the same pointer as addresses, with a potentially smaller len. +/// On Windows, when first_address is not null, we ask for at least 32 stack frames, +/// and then try to find the first address. If addresses.len is more than 32, we +/// capture that many stack frames exactly, and then look for the first address, +/// chopping off the irrelevant frames and shifting so that the returned addresses pointer +/// equals the passed in addresses pointer. +pub fn captureStackTrace(first_address: ?usize, stack_trace: *std.builtin.StackTrace) void { + if (native_os == .windows) { + const addrs = stack_trace.instruction_addresses; + const first_addr = first_address orelse { + stack_trace.index = walkStackWindows(addrs[0..], null); + return; + }; + var addr_buf_stack: [32]usize = undefined; + const addr_buf = if (addr_buf_stack.len > addrs.len) addr_buf_stack[0..] else addrs; + const n = walkStackWindows(addr_buf[0..], null); + const first_index = for (addr_buf[0..n], 0..) |addr, i| { + if (addr == first_addr) { + break i; + } + } else { + stack_trace.index = 0; + return; + }; + const end_index = @min(first_index + addrs.len, n); + const slice = addr_buf[first_index..end_index]; + // We use a for loop here because slice and addrs may alias. + for (slice, 0..) |addr, i| { + addrs[i] = addr; + } + stack_trace.index = slice.len; + } else { + // TODO: This should use the DWARF unwinder if .eh_frame_hdr is available (so that full debug info parsing isn't required). + // A new path for loading SelfInfo needs to be created which will only attempt to parse in-memory sections, because + // stopping to load other debug info (ie. source line info) from disk here is not required for unwinding. + var it = StackIterator.init(first_address, null); + defer it.deinit(); + for (stack_trace.instruction_addresses, 0..) |*addr, i| { + addr.* = it.next() orelse { + stack_trace.index = i; + return; + }; + } + stack_trace.index = stack_trace.instruction_addresses.len; + } +} + +/// Tries to print a stack trace to stderr, unbuffered, and ignores any error returned. +/// TODO multithreaded awareness +pub fn dumpStackTrace(stack_trace: std.builtin.StackTrace) void { + nosuspend { + if (builtin.target.cpu.arch.isWasm()) { + if (native_os == .wasi) { + const stderr = io.getStdErr().writer(); + stderr.print("Unable to dump stack trace: not implemented for Wasm\n", .{}) catch return; + } + return; + } + const stderr = io.getStdErr().writer(); + if (builtin.strip_debug_info) { + stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; + return; + } + const debug_info = getSelfDebugInfo() catch |err| { + stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; + return; + }; + writeStackTrace(stack_trace, stderr, debug_info, io.tty.detectConfig(io.getStdErr())) catch |err| { + stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; + return; + }; + } +} + +/// Invokes detectable illegal behavior when `ok` is `false`. +/// +/// In Debug and ReleaseSafe modes, calls to this function are always +/// generated, and the `unreachable` statement triggers a panic. +/// +/// In ReleaseFast and ReleaseSmall modes, calls to this function are optimized +/// away, and in fact the optimizer is able to use the assertion in its +/// heuristics. +/// +/// Inside a test block, it is best to use the `std.testing` module rather than +/// this function, because this function may not detect a test failure in +/// ReleaseFast and ReleaseSmall mode. Outside of a test block, this assert +/// function is the correct function to use. +pub fn assert(ok: bool) void { + if (!ok) unreachable; // assertion failure +} + +/// Invokes detectable illegal behavior when the provided slice is not mapped +/// or lacks read permissions. +pub fn assertReadable(slice: []const volatile u8) void { + if (!runtime_safety) return; + for (slice) |*byte| _ = byte.*; +} + +/// Equivalent to `@panic` but with a formatted message. +pub fn panic(comptime format: []const u8, args: anytype) noreturn { + @branchHint(.cold); + panicExtra(@returnAddress(), format, args); +} + +/// Equivalent to `@panic` but with a formatted message, and with an explicitly +/// provided return address. +pub fn panicExtra( + ret_addr: ?usize, + comptime format: []const u8, + args: anytype, +) noreturn { + @branchHint(.cold); + + const size = 0x1000; + const trunc_msg = "(msg truncated)"; + var buf: [size + trunc_msg.len]u8 = undefined; + // a minor annoyance with this is that it will result in the NoSpaceLeft + // error being part of the @panic stack trace (but that error should + // only happen rarely) + const msg = std.fmt.bufPrint(buf[0..size], format, args) catch |err| switch (err) { + error.NoSpaceLeft => blk: { + @memcpy(buf[size..], trunc_msg); + break :blk &buf; + }, + }; + 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; + +/// Dumps a stack trace to standard error, then aborts. +pub fn defaultPanic( + msg: []const u8, + first_trace_addr: ?usize, +) noreturn { + @branchHint(.cold); + + // For backends that cannot handle the language features depended on by the + // default panic handler, we have a simpler panic handler: + if (builtin.zig_backend == .stage2_wasm or + builtin.zig_backend == .stage2_arm or + builtin.zig_backend == .stage2_aarch64 or + builtin.zig_backend == .stage2_x86 or + (builtin.zig_backend == .stage2_x86_64 and (builtin.target.ofmt != .elf and builtin.target.ofmt != .macho)) or + builtin.zig_backend == .stage2_sparc64 or + builtin.zig_backend == .stage2_spirv64) + { + @trap(); + } + + switch (builtin.os.tag) { + .freestanding, .other => { + @trap(); + }, + .uefi => { + const uefi = std.os.uefi; + + var utf16_buffer: [1000]u16 = undefined; + const len_minus_3 = std.unicode.utf8ToUtf16Le(&utf16_buffer, msg) catch 0; + utf16_buffer[len_minus_3..][0..3].* = .{ '\r', '\n', 0 }; + const len = len_minus_3 + 3; + const exit_msg = utf16_buffer[0 .. len - 1 :0]; + + // Output to both std_err and con_out, as std_err is easier + // to read in stuff like QEMU at times, but, unlike con_out, + // isn't visible on actual hardware if directly booted into + inline for ([_]?*uefi.protocol.SimpleTextOutput{ uefi.system_table.std_err, uefi.system_table.con_out }) |o| { + if (o) |out| { + _ = out.setAttribute(uefi.protocol.SimpleTextOutput.red); + _ = out.outputString(exit_msg); + _ = out.setAttribute(uefi.protocol.SimpleTextOutput.white); + } + } + + if (uefi.system_table.boot_services) |bs| { + // ExitData buffer must be allocated using boot_services.allocatePool (spec: page 220) + const exit_data: []u16 = uefi.raw_pool_allocator.alloc(u16, exit_msg.len + 1) catch @trap(); + @memcpy(exit_data, exit_msg[0..exit_data.len]); // Includes null terminator. + _ = bs.exit(uefi.handle, .aborted, exit_data.len, exit_data.ptr); + } + @trap(); + }, + .cuda, .amdhsa => std.posix.abort(), + .plan9 => { + var status: [std.os.plan9.ERRMAX]u8 = undefined; + const len = @min(msg.len, status.len - 1); + @memcpy(status[0..len], msg[0..len]); + status[len] = 0; + std.os.plan9.exits(status[0..len :0]); + }, + else => {}, + } + + if (enable_segfault_handler) { + // If a segfault happens while panicking, we want it to actually segfault, not trigger + // the handler. + resetSegfaultHandler(); + } + + // Note there is similar logic in handleSegfaultPosix and handleSegfaultWindowsExtra. + nosuspend switch (panic_stage) { + 0 => { + panic_stage = 1; + + _ = panicking.fetchAdd(1, .seq_cst); + + { + lockStdErr(); + defer unlockStdErr(); + + const stderr = io.getStdErr().writer(); + if (builtin.single_threaded) { + stderr.print("panic: ", .{}) catch posix.abort(); + } else { + const current_thread_id = std.Thread.getCurrentId(); + stderr.print("thread {} panic: ", .{current_thread_id}) catch posix.abort(); + } + stderr.print("{s}\n", .{msg}) catch posix.abort(); + + if (@errorReturnTrace()) |t| dumpStackTrace(t.*); + dumpCurrentStackTrace(first_trace_addr orelse @returnAddress()); + } + + 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(). + io.getStdErr().writeAll("aborting due to recursive panic\n") catch {}; + }, + else => {}, // Panicked while printing the recursive panic message. + }; + + posix.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 = std.atomic.Value(u32).init(0); + while (true) std.Thread.Futex.wait(&futex, 0); + unreachable; + } +} + +pub fn writeStackTrace( + stack_trace: std.builtin.StackTrace, + out_stream: anytype, + debug_info: *SelfInfo, + tty_config: io.tty.Config, +) !void { + if (builtin.strip_debug_info) return error.MissingDebugInfo; + var frame_index: usize = 0; + var frames_left: usize = @min(stack_trace.index, stack_trace.instruction_addresses.len); + + while (frames_left != 0) : ({ + frames_left -= 1; + frame_index = (frame_index + 1) % stack_trace.instruction_addresses.len; + }) { + const return_address = stack_trace.instruction_addresses[frame_index]; + try printSourceAtAddress(debug_info, out_stream, return_address - 1, tty_config); + } + + if (stack_trace.index > stack_trace.instruction_addresses.len) { + const dropped_frames = stack_trace.index - stack_trace.instruction_addresses.len; + + tty_config.setColor(out_stream, .bold) catch {}; + try out_stream.print("({d} additional stack frames skipped...)\n", .{dropped_frames}); + tty_config.setColor(out_stream, .reset) catch {}; + } +} + +pub const UnwindError = if (have_ucontext) + @typeInfo(@typeInfo(@TypeOf(StackIterator.next_unwind)).@"fn".return_type.?).error_union.error_set +else + void; + +pub const StackIterator = struct { + // Skip every frame before this address is found. + first_address: ?usize, + // Last known value of the frame pointer register. + fp: usize, + ma: MemoryAccessor = MemoryAccessor.init, + + // When SelfInfo and a register context is available, this iterator can unwind + // stacks with frames that don't use a frame pointer (ie. -fomit-frame-pointer), + // using DWARF and MachO unwind info. + unwind_state: if (have_ucontext) ?struct { + debug_info: *SelfInfo, + dwarf_context: SelfInfo.UnwindContext, + last_error: ?UnwindError = null, + failed: bool = false, + } else void = if (have_ucontext) null else {}, + + pub fn init(first_address: ?usize, fp: ?usize) StackIterator { + if (native_arch.isSPARC()) { + // Flush all the register windows on stack. + asm volatile (if (std.Target.sparc.featureSetHas(builtin.cpu.features, .v9)) + "flushw" + else + "ta 3" // ST_FLUSH_WINDOWS + ::: "memory"); + } + + return StackIterator{ + .first_address = first_address, + // TODO: this is a workaround for #16876 + //.fp = fp orelse @frameAddress(), + .fp = fp orelse blk: { + const fa = @frameAddress(); + break :blk fa; + }, + }; + } + + pub fn initWithContext(first_address: ?usize, debug_info: *SelfInfo, context: *posix.ucontext_t) !StackIterator { + // The implementation of DWARF unwinding on aarch64-macos is not complete. However, Apple mandates that + // the frame pointer register is always used, so on this platform we can safely use the FP-based unwinder. + if (builtin.target.os.tag.isDarwin() and native_arch == .aarch64) + return init(first_address, @truncate(context.mcontext.ss.fp)); + + if (SelfInfo.supports_unwinding) { + var iterator = init(first_address, null); + iterator.unwind_state = .{ + .debug_info = debug_info, + .dwarf_context = try SelfInfo.UnwindContext.init(debug_info.allocator, context), + }; + return iterator; + } + + return init(first_address, null); + } + + pub fn deinit(it: *StackIterator) void { + it.ma.deinit(); + if (have_ucontext and it.unwind_state != null) it.unwind_state.?.dwarf_context.deinit(); + } + + pub fn getLastError(it: *StackIterator) ?struct { + err: UnwindError, + address: usize, + } { + if (!have_ucontext) return null; + if (it.unwind_state) |*unwind_state| { + if (unwind_state.last_error) |err| { + unwind_state.last_error = null; + return .{ + .err = err, + .address = unwind_state.dwarf_context.pc, + }; + } + } + + return null; + } + + // Offset of the saved BP wrt the frame pointer. + const fp_offset = if (native_arch.isRISCV()) + // On RISC-V the frame pointer points to the top of the saved register + // area, on pretty much every other architecture it points to the stack + // slot where the previous frame pointer is saved. + 2 * @sizeOf(usize) + else if (native_arch.isSPARC()) + // On SPARC the previous frame pointer is stored at 14 slots past %fp+BIAS. + 14 * @sizeOf(usize) + else + 0; + + const fp_bias = if (native_arch.isSPARC()) + // On SPARC frame pointers are biased by a constant. + 2047 + else + 0; + + // Positive offset of the saved PC wrt the frame pointer. + const pc_offset = if (native_arch == .powerpc64le) + 2 * @sizeOf(usize) + else + @sizeOf(usize); + + pub fn next(it: *StackIterator) ?usize { + var address = it.next_internal() orelse return null; + + if (it.first_address) |first_address| { + while (address != first_address) { + address = it.next_internal() orelse return null; + } + it.first_address = null; + } + + return address; + } + + fn next_unwind(it: *StackIterator) !usize { + const unwind_state = &it.unwind_state.?; + const module = try unwind_state.debug_info.getModuleForAddress(unwind_state.dwarf_context.pc); + switch (native_os) { + .macos, .ios, .watchos, .tvos, .visionos => { + // __unwind_info is a requirement for unwinding on Darwin. It may fall back to DWARF, but unwinding + // via DWARF before attempting to use the compact unwind info will produce incorrect results. + if (module.unwind_info) |unwind_info| { + if (SelfInfo.unwindFrameMachO( + unwind_state.debug_info.allocator, + module.base_address, + &unwind_state.dwarf_context, + &it.ma, + unwind_info, + module.eh_frame, + )) |return_address| { + return return_address; + } else |err| { + if (err != error.RequiresDWARFUnwind) return err; + } + } else return error.MissingUnwindInfo; + }, + else => {}, + } + + if (try module.getDwarfInfoForAddress(unwind_state.debug_info.allocator, unwind_state.dwarf_context.pc)) |di| { + return SelfInfo.unwindFrameDwarf( + unwind_state.debug_info.allocator, + di, + module.base_address, + &unwind_state.dwarf_context, + &it.ma, + null, + ); + } else return error.MissingDebugInfo; + } + + fn next_internal(it: *StackIterator) ?usize { + if (have_ucontext) { + if (it.unwind_state) |*unwind_state| { + if (!unwind_state.failed) { + if (unwind_state.dwarf_context.pc == 0) return null; + defer it.fp = unwind_state.dwarf_context.getFp() catch 0; + if (it.next_unwind()) |return_address| { + return return_address; + } else |err| { + unwind_state.last_error = err; + unwind_state.failed = true; + + // Fall back to fp-based unwinding on the first failure. + // We can't attempt it again for other modules higher in the + // stack because the full register state won't have been unwound. + } + } + } + } + + const fp = if (comptime native_arch.isSPARC()) + // On SPARC the offset is positive. (!) + math.add(usize, it.fp, fp_offset) catch return null + else + math.sub(usize, it.fp, fp_offset) catch return null; + + // Sanity check. + if (fp == 0 or !mem.isAligned(fp, @alignOf(usize))) return null; + const new_fp = math.add(usize, it.ma.load(usize, fp) orelse return null, fp_bias) catch + return null; + + // Sanity check: the stack grows down thus all the parent frames must be + // be at addresses that are greater (or equal) than the previous one. + // A zero frame pointer often signals this is the last frame, that case + // is gracefully handled by the next call to next_internal. + if (new_fp != 0 and new_fp < it.fp) return null; + const new_pc = it.ma.load(usize, math.add(usize, fp, pc_offset) catch return null) orelse + return null; + + it.fp = new_fp; + + return new_pc; + } +}; + +pub fn writeCurrentStackTrace( + out_stream: anytype, + debug_info: *SelfInfo, + tty_config: io.tty.Config, + start_addr: ?usize, +) !void { + if (native_os == .windows) { + var context: ThreadContext = undefined; + assert(getContext(&context)); + return writeStackTraceWindows(out_stream, debug_info, tty_config, &context, start_addr); + } + var context: ThreadContext = undefined; + const has_context = getContext(&context); + + var it = (if (has_context) blk: { + break :blk StackIterator.initWithContext(start_addr, debug_info, &context) catch null; + } else null) orelse StackIterator.init(start_addr, null); + defer it.deinit(); + + while (it.next()) |return_address| { + printLastUnwindError(&it, debug_info, out_stream, tty_config); + + // On arm64 macOS, the address of the last frame is 0x0 rather than 0x1 as on x86_64 macOS, + // therefore, we do a check for `return_address == 0` before subtracting 1 from it to avoid + // an overflow. We do not need to signal `StackIterator` as it will correctly detect this + // condition on the subsequent iteration and return `null` thus terminating the loop. + // same behaviour for x86-windows-msvc + const address = return_address -| 1; + try printSourceAtAddress(debug_info, out_stream, address, tty_config); + } else printLastUnwindError(&it, debug_info, out_stream, tty_config); +} + +pub noinline fn walkStackWindows(addresses: []usize, existing_context: ?*const windows.CONTEXT) usize { + if (builtin.cpu.arch == .x86) { + // RtlVirtualUnwind doesn't exist on x86 + return windows.ntdll.RtlCaptureStackBackTrace(0, addresses.len, @as(**anyopaque, @ptrCast(addresses.ptr)), null); + } + + const tib = &windows.teb().NtTib; + + var context: windows.CONTEXT = undefined; + if (existing_context) |context_ptr| { + context = context_ptr.*; + } else { + context = std.mem.zeroes(windows.CONTEXT); + windows.ntdll.RtlCaptureContext(&context); + } + + var i: usize = 0; + var image_base: windows.DWORD64 = undefined; + var history_table: windows.UNWIND_HISTORY_TABLE = std.mem.zeroes(windows.UNWIND_HISTORY_TABLE); + + while (i < addresses.len) : (i += 1) { + const current_regs = context.getRegs(); + if (windows.ntdll.RtlLookupFunctionEntry(current_regs.ip, &image_base, &history_table)) |runtime_function| { + var handler_data: ?*anyopaque = null; + var establisher_frame: u64 = undefined; + _ = windows.ntdll.RtlVirtualUnwind( + windows.UNW_FLAG_NHANDLER, + image_base, + current_regs.ip, + runtime_function, + &context, + &handler_data, + &establisher_frame, + null, + ); + } else { + // leaf function + context.setIp(@as(*usize, @ptrFromInt(current_regs.sp)).*); + context.setSp(current_regs.sp + @sizeOf(usize)); + } + + const next_regs = context.getRegs(); + if (next_regs.sp < @intFromPtr(tib.StackLimit) or next_regs.sp > @intFromPtr(tib.StackBase)) { + break; + } + + if (next_regs.ip == 0) { + break; + } + + addresses[i] = next_regs.ip; + } + + return i; +} + +pub fn writeStackTraceWindows( + out_stream: anytype, + debug_info: *SelfInfo, + tty_config: io.tty.Config, + context: *const windows.CONTEXT, + start_addr: ?usize, +) !void { + var addr_buf: [1024]usize = undefined; + const n = walkStackWindows(addr_buf[0..], context); + const addrs = addr_buf[0..n]; + const start_i: usize = if (start_addr) |saddr| blk: { + for (addrs, 0..) |addr, i| { + if (addr == saddr) break :blk i; + } + return; + } else 0; + for (addrs[start_i..]) |addr| { + try printSourceAtAddress(debug_info, out_stream, addr - 1, tty_config); + } +} + +fn printUnknownSource(debug_info: *SelfInfo, out_stream: anytype, address: usize, tty_config: io.tty.Config) !void { + const module_name = debug_info.getModuleNameForAddress(address); + return printLineInfo( + out_stream, + null, + address, + "???", + module_name orelse "???", + tty_config, + printLineFromFileAnyOs, + ); +} + +fn printLastUnwindError(it: *StackIterator, debug_info: *SelfInfo, out_stream: anytype, tty_config: io.tty.Config) void { + if (!have_ucontext) return; + if (it.getLastError()) |unwind_error| { + printUnwindError(debug_info, out_stream, unwind_error.address, unwind_error.err, tty_config) catch {}; + } +} + +fn printUnwindError(debug_info: *SelfInfo, out_stream: anytype, address: usize, err: UnwindError, tty_config: io.tty.Config) !void { + const module_name = debug_info.getModuleNameForAddress(address) orelse "???"; + try tty_config.setColor(out_stream, .dim); + if (err == error.MissingDebugInfo) { + try out_stream.print("Unwind information for `{s}:0x{x}` was not available, trace may be incomplete\n\n", .{ module_name, address }); + } else { + try out_stream.print("Unwind error at address `{s}:0x{x}` ({}), trace may be incomplete\n\n", .{ module_name, address, err }); + } + try tty_config.setColor(out_stream, .reset); +} + +pub fn printSourceAtAddress(debug_info: *SelfInfo, out_stream: anytype, address: usize, tty_config: io.tty.Config) !void { + const module = debug_info.getModuleForAddress(address) catch |err| switch (err) { + error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config), + else => return err, + }; + + const symbol_info = module.getSymbolAtAddress(debug_info.allocator, address) catch |err| switch (err) { + error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config), + else => return err, + }; + defer if (symbol_info.source_location) |sl| debug_info.allocator.free(sl.file_name); + + return printLineInfo( + out_stream, + symbol_info.source_location, + address, + symbol_info.name, + symbol_info.compile_unit_name, + tty_config, + printLineFromFileAnyOs, + ); +} + +fn printLineInfo( + out_stream: anytype, + source_location: ?SourceLocation, + address: usize, + symbol_name: []const u8, + compile_unit_name: []const u8, + tty_config: io.tty.Config, + comptime printLineFromFile: anytype, +) !void { + nosuspend { + try tty_config.setColor(out_stream, .bold); + + if (source_location) |*sl| { + try out_stream.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column }); + } else { + try out_stream.writeAll("???:?:?"); + } + + try tty_config.setColor(out_stream, .reset); + try out_stream.writeAll(": "); + try tty_config.setColor(out_stream, .dim); + try out_stream.print("0x{x} in {s} ({s})", .{ address, symbol_name, compile_unit_name }); + try tty_config.setColor(out_stream, .reset); + try out_stream.writeAll("\n"); + + // Show the matching source code line if possible + if (source_location) |sl| { + if (printLineFromFile(out_stream, sl)) { + if (sl.column > 0) { + // The caret already takes one char + const space_needed = @as(usize, @intCast(sl.column - 1)); + + try out_stream.writeByteNTimes(' ', space_needed); + try tty_config.setColor(out_stream, .green); + try out_stream.writeAll("^"); + try tty_config.setColor(out_stream, .reset); + } + try out_stream.writeAll("\n"); + } else |err| switch (err) { + error.EndOfFile, error.FileNotFound => {}, + error.BadPathName => {}, + error.AccessDenied => {}, + else => return err, + } + } + } +} + +fn printLineFromFileAnyOs(out_stream: anytype, source_location: SourceLocation) !void { + // Need this to always block even in async I/O mode, because this could potentially + // be called from e.g. the event loop code crashing. + var f = try fs.cwd().openFile(source_location.file_name, .{}); + defer f.close(); + // TODO fstat and make sure that the file has the correct size + + var buf: [4096]u8 = undefined; + var amt_read = try f.read(buf[0..]); + const line_start = seek: { + var current_line_start: usize = 0; + var next_line: usize = 1; + while (next_line != source_location.line) { + const slice = buf[current_line_start..amt_read]; + if (mem.indexOfScalar(u8, slice, '\n')) |pos| { + next_line += 1; + if (pos == slice.len - 1) { + amt_read = try f.read(buf[0..]); + current_line_start = 0; + } else current_line_start += pos + 1; + } else if (amt_read < buf.len) { + return error.EndOfFile; + } else { + amt_read = try f.read(buf[0..]); + current_line_start = 0; + } + } + break :seek current_line_start; + }; + const slice = buf[line_start..amt_read]; + if (mem.indexOfScalar(u8, slice, '\n')) |pos| { + const line = slice[0 .. pos + 1]; + mem.replaceScalar(u8, line, '\t', ' '); + return out_stream.writeAll(line); + } else { // Line is the last inside the buffer, and requires another read to find delimiter. Alternatively the file ends. + mem.replaceScalar(u8, slice, '\t', ' '); + try out_stream.writeAll(slice); + while (amt_read == buf.len) { + amt_read = try f.read(buf[0..]); + if (mem.indexOfScalar(u8, buf[0..amt_read], '\n')) |pos| { + const line = buf[0 .. pos + 1]; + mem.replaceScalar(u8, line, '\t', ' '); + return out_stream.writeAll(line); + } else { + const line = buf[0..amt_read]; + mem.replaceScalar(u8, line, '\t', ' '); + try out_stream.writeAll(line); + } + } + // Make sure printing last line of file inserts extra newline + try out_stream.writeByte('\n'); + } +} + +test printLineFromFileAnyOs { + var output = std.ArrayList(u8).init(std.testing.allocator); + defer output.deinit(); + const output_stream = output.writer(); + + const allocator = std.testing.allocator; + const join = std.fs.path.join; + const expectError = std.testing.expectError; + const expectEqualStrings = std.testing.expectEqualStrings; + + var test_dir = std.testing.tmpDir(.{}); + defer test_dir.cleanup(); + // Relies on testing.tmpDir internals which is not ideal, but SourceLocation requires paths. + const test_dir_path = try join(allocator, &.{ ".zig-cache", "tmp", test_dir.sub_path[0..] }); + defer allocator.free(test_dir_path); + + // Cases + { + const path = try join(allocator, &.{ test_dir_path, "one_line.zig" }); + defer allocator.free(path); + try test_dir.dir.writeFile(.{ .sub_path = "one_line.zig", .data = "no new lines in this file, but one is printed anyway" }); + + try expectError(error.EndOfFile, printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = 2, .column = 0 })); + + try printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = 1, .column = 0 }); + try expectEqualStrings("no new lines in this file, but one is printed anyway\n", output.items); + output.clearRetainingCapacity(); + } + { + const path = try fs.path.join(allocator, &.{ test_dir_path, "three_lines.zig" }); + defer allocator.free(path); + try test_dir.dir.writeFile(.{ + .sub_path = "three_lines.zig", + .data = + \\1 + \\2 + \\3 + , + }); + + try printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = 1, .column = 0 }); + try expectEqualStrings("1\n", output.items); + output.clearRetainingCapacity(); + + try printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = 3, .column = 0 }); + try expectEqualStrings("3\n", output.items); + output.clearRetainingCapacity(); + } + { + const file = try test_dir.dir.createFile("line_overlaps_page_boundary.zig", .{}); + defer file.close(); + const path = try fs.path.join(allocator, &.{ test_dir_path, "line_overlaps_page_boundary.zig" }); + defer allocator.free(path); + + const overlap = 10; + var writer = file.writer(); + try writer.writeByteNTimes('a', std.heap.page_size_min - overlap); + try writer.writeByte('\n'); + try writer.writeByteNTimes('a', overlap); + + try printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = 2, .column = 0 }); + try expectEqualStrings(("a" ** overlap) ++ "\n", output.items); + output.clearRetainingCapacity(); + } + { + const file = try test_dir.dir.createFile("file_ends_on_page_boundary.zig", .{}); + defer file.close(); + const path = try fs.path.join(allocator, &.{ test_dir_path, "file_ends_on_page_boundary.zig" }); + defer allocator.free(path); + + var writer = file.writer(); + try writer.writeByteNTimes('a', std.heap.page_size_max); + + try printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = 1, .column = 0 }); + try expectEqualStrings(("a" ** std.heap.page_size_max) ++ "\n", output.items); + output.clearRetainingCapacity(); + } + { + const file = try test_dir.dir.createFile("very_long_first_line_spanning_multiple_pages.zig", .{}); + defer file.close(); + const path = try fs.path.join(allocator, &.{ test_dir_path, "very_long_first_line_spanning_multiple_pages.zig" }); + defer allocator.free(path); + + var writer = file.writer(); + try writer.writeByteNTimes('a', 3 * std.heap.page_size_max); + + try expectError(error.EndOfFile, printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = 2, .column = 0 })); + + try printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = 1, .column = 0 }); + try expectEqualStrings(("a" ** (3 * std.heap.page_size_max)) ++ "\n", output.items); + output.clearRetainingCapacity(); + + try writer.writeAll("a\na"); + + try printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = 1, .column = 0 }); + try expectEqualStrings(("a" ** (3 * std.heap.page_size_max)) ++ "a\n", output.items); + output.clearRetainingCapacity(); + + try printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = 2, .column = 0 }); + try expectEqualStrings("a\n", output.items); + output.clearRetainingCapacity(); + } + { + const file = try test_dir.dir.createFile("file_of_newlines.zig", .{}); + defer file.close(); + const path = try fs.path.join(allocator, &.{ test_dir_path, "file_of_newlines.zig" }); + defer allocator.free(path); + + var writer = file.writer(); + const real_file_start = 3 * std.heap.page_size_min; + try writer.writeByteNTimes('\n', real_file_start); + try writer.writeAll("abc\ndef"); + + try printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = real_file_start + 1, .column = 0 }); + try expectEqualStrings("abc\n", output.items); + output.clearRetainingCapacity(); + + try printLineFromFileAnyOs(output_stream, .{ .file_name = path, .line = real_file_start + 2, .column = 0 }); + try expectEqualStrings("def\n", output.items); + output.clearRetainingCapacity(); + } +} + +/// TODO multithreaded awareness +var debug_info_allocator: ?mem.Allocator = null; +var debug_info_arena_allocator: std.heap.ArenaAllocator = undefined; +fn getDebugInfoAllocator() mem.Allocator { + if (debug_info_allocator) |a| return a; + + debug_info_arena_allocator = std.heap.ArenaAllocator.init(std.heap.page_allocator); + const allocator = debug_info_arena_allocator.allocator(); + debug_info_allocator = allocator; + return allocator; +} + +/// Whether or not the current target can print useful debug information when a segfault occurs. +pub const have_segfault_handling_support = switch (native_os) { + .linux, + .macos, + .netbsd, + .solaris, + .illumos, + .windows, + => true, + + .freebsd, .openbsd => have_ucontext, + else => false, +}; + +const enable_segfault_handler = std.options.enable_segfault_handler; +pub const default_enable_segfault_handler = runtime_safety and have_segfault_handling_support; + +pub fn maybeEnableSegfaultHandler() void { + if (enable_segfault_handler) { + attachSegfaultHandler(); + } +} + +var windows_segfault_handle: ?windows.HANDLE = null; + +pub fn updateSegfaultHandler(act: ?*const posix.Sigaction) void { + posix.sigaction(posix.SIG.SEGV, act, null); + posix.sigaction(posix.SIG.ILL, act, null); + posix.sigaction(posix.SIG.BUS, act, null); + posix.sigaction(posix.SIG.FPE, act, null); +} + +/// Attaches a global SIGSEGV handler which calls `@panic("segmentation fault");` +pub fn attachSegfaultHandler() void { + if (!have_segfault_handling_support) { + @compileError("segfault handler not supported for this target"); + } + if (native_os == .windows) { + windows_segfault_handle = windows.kernel32.AddVectoredExceptionHandler(0, handleSegfaultWindows); + return; + } + var act = posix.Sigaction{ + .handler = .{ .sigaction = handleSegfaultPosix }, + .mask = posix.empty_sigset, + .flags = (posix.SA.SIGINFO | posix.SA.RESTART | posix.SA.RESETHAND), + }; + + updateSegfaultHandler(&act); +} + +fn resetSegfaultHandler() void { + if (native_os == .windows) { + if (windows_segfault_handle) |handle| { + assert(windows.kernel32.RemoveVectoredExceptionHandler(handle) != 0); + windows_segfault_handle = null; + } + return; + } + var act = posix.Sigaction{ + .handler = .{ .handler = posix.SIG.DFL }, + .mask = posix.empty_sigset, + .flags = 0, + }; + updateSegfaultHandler(&act); +} + +pub fn handleSegfaultPosix(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) noreturn { + // Reset to the default handler so that if a segfault happens in this handler it will crash + // the process. Also when this handler returns, the original instruction will be repeated + // and the resulting segfault will crash the process rather than continually dump stack traces. + resetSegfaultHandler(); + + const addr = switch (native_os) { + .linux => @intFromPtr(info.fields.sigfault.addr), + .freebsd, .macos => @intFromPtr(info.addr), + .netbsd => @intFromPtr(info.info.reason.fault.addr), + .openbsd => @intFromPtr(info.data.fault.addr), + .solaris, .illumos => @intFromPtr(info.reason.fault.addr), + else => unreachable, + }; + + const code = if (native_os == .netbsd) info.info.code else info.code; + nosuspend switch (panic_stage) { + 0 => { + panic_stage = 1; + _ = panicking.fetchAdd(1, .seq_cst); + + { + lockStdErr(); + defer unlockStdErr(); + + dumpSegfaultInfoPosix(sig, code, addr, ctx_ptr); + } + + waitForOtherThreadToFinishPanicking(); + }, + else => { + // panic mutex already locked + dumpSegfaultInfoPosix(sig, code, addr, ctx_ptr); + }, + }; + + // 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. + posix.abort(); +} + +fn dumpSegfaultInfoPosix(sig: i32, code: i32, addr: usize, ctx_ptr: ?*anyopaque) void { + const stderr = io.getStdErr().writer(); + _ = switch (sig) { + posix.SIG.SEGV => if (native_arch == .x86_64 and native_os == .linux and code == 128) // SI_KERNEL + // x86_64 doesn't have a full 64-bit virtual address space. + // Addresses outside of that address space are non-canonical + // and the CPU won't provide the faulting address to us. + // This happens when accessing memory addresses such as 0xaaaaaaaaaaaaaaaa + // but can also happen when no addressable memory is involved; + // for example when reading/writing model-specific registers + // by executing `rdmsr` or `wrmsr` in user-space (unprivileged mode). + stderr.print("General protection exception (no address available)\n", .{}) + else + stderr.print("Segmentation fault at address 0x{x}\n", .{addr}), + posix.SIG.ILL => stderr.print("Illegal instruction at address 0x{x}\n", .{addr}), + posix.SIG.BUS => stderr.print("Bus error at address 0x{x}\n", .{addr}), + posix.SIG.FPE => stderr.print("Arithmetic exception at address 0x{x}\n", .{addr}), + else => unreachable, + } catch posix.abort(); + + switch (native_arch) { + .x86, + .x86_64, + .arm, + .armeb, + .thumb, + .thumbeb, + .aarch64, + .aarch64_be, + => { + // Some kernels don't align `ctx_ptr` properly. Handle this defensively. + const ctx: *align(1) posix.ucontext_t = @ptrCast(ctx_ptr); + var new_ctx: posix.ucontext_t = ctx.*; + if (builtin.os.tag.isDarwin() and builtin.cpu.arch == .aarch64) { + // The kernel incorrectly writes the contents of `__mcontext_data` right after `mcontext`, + // rather than after the 8 bytes of padding that are supposed to sit between the two. Copy the + // contents to the right place so that the `mcontext` pointer will be correct after the + // `relocateContext` call below. + new_ctx.__mcontext_data = @as(*align(1) extern struct { + onstack: c_int, + sigmask: std.c.sigset_t, + stack: std.c.stack_t, + link: ?*std.c.ucontext_t, + mcsize: u64, + mcontext: *std.c.mcontext_t, + __mcontext_data: std.c.mcontext_t align(@sizeOf(usize)), // Disable padding after `mcontext`. + }, @ptrCast(ctx)).__mcontext_data; + } + relocateContext(&new_ctx); + dumpStackTraceFromBase(&new_ctx); + }, + else => {}, + } +} + +fn handleSegfaultWindows(info: *windows.EXCEPTION_POINTERS) callconv(.winapi) c_long { + switch (info.ExceptionRecord.ExceptionCode) { + windows.EXCEPTION_DATATYPE_MISALIGNMENT => handleSegfaultWindowsExtra(info, 0, "Unaligned Memory Access"), + windows.EXCEPTION_ACCESS_VIOLATION => handleSegfaultWindowsExtra(info, 1, null), + windows.EXCEPTION_ILLEGAL_INSTRUCTION => handleSegfaultWindowsExtra(info, 2, null), + windows.EXCEPTION_STACK_OVERFLOW => handleSegfaultWindowsExtra(info, 0, "Stack Overflow"), + else => return windows.EXCEPTION_CONTINUE_SEARCH, + } +} + +fn handleSegfaultWindowsExtra(info: *windows.EXCEPTION_POINTERS, msg: u8, label: ?[]const u8) noreturn { + comptime assert(windows.CONTEXT != void); + nosuspend switch (panic_stage) { + 0 => { + panic_stage = 1; + _ = panicking.fetchAdd(1, .seq_cst); + + { + lockStdErr(); + defer unlockStdErr(); + + dumpSegfaultInfoWindows(info, msg, label); + } + + waitForOtherThreadToFinishPanicking(); + }, + 1 => { + panic_stage = 2; + io.getStdErr().writeAll("aborting due to recursive panic\n") catch {}; + }, + else => {}, + }; + posix.abort(); +} + +fn dumpSegfaultInfoWindows(info: *windows.EXCEPTION_POINTERS, msg: u8, label: ?[]const u8) void { + const stderr = io.getStdErr().writer(); + _ = switch (msg) { + 0 => stderr.print("{s}\n", .{label.?}), + 1 => stderr.print("Segmentation fault at address 0x{x}\n", .{info.ExceptionRecord.ExceptionInformation[1]}), + 2 => stderr.print("Illegal instruction at address 0x{x}\n", .{info.ContextRecord.getRegs().ip}), + else => unreachable, + } catch posix.abort(); + + dumpStackTraceFromBase(info.ContextRecord); +} + +pub fn dumpStackPointerAddr(prefix: []const u8) void { + const sp = asm ("" + : [argc] "={rsp}" (-> usize), + ); + print("{s} sp = 0x{x}\n", .{ prefix, sp }); +} + +test "manage resources correctly" { + if (builtin.strip_debug_info) return error.SkipZigTest; + + if (native_os == .wasi) return error.SkipZigTest; + + if (native_os == .windows) { + // https://github.com/ziglang/zig/issues/13963 + return error.SkipZigTest; + } + + // self-hosted debug info is still too buggy + if (builtin.zig_backend != .stage2_llvm) return error.SkipZigTest; + + const writer = std.io.null_writer; + var di = try SelfInfo.open(testing.allocator); + defer di.deinit(); + try printSourceAtAddress(&di, writer, showMyTrace(), io.tty.detectConfig(std.io.getStdErr())); +} + +noinline fn showMyTrace() usize { + return @returnAddress(); +} + +/// This API helps you track where a value originated and where it was mutated, +/// or any other points of interest. +/// In debug mode, it adds a small size penalty (104 bytes on 64-bit architectures) +/// to the aggregate that you add it to. +/// In release mode, it is size 0 and all methods are no-ops. +/// This is a pre-made type with default settings. +/// For more advanced usage, see `ConfigurableTrace`. +pub const Trace = ConfigurableTrace(2, 4, builtin.mode == .Debug); + +pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize, comptime is_enabled: bool) type { + return struct { + addrs: [actual_size][stack_frame_count]usize, + notes: [actual_size][]const u8, + index: Index, + + const actual_size = if (enabled) size else 0; + const Index = if (enabled) usize else u0; + + pub const init: @This() = .{ + .addrs = undefined, + .notes = undefined, + .index = 0, + }; + + pub const enabled = is_enabled; + + pub const add = if (enabled) addNoInline else addNoOp; + + pub noinline fn addNoInline(t: *@This(), note: []const u8) void { + comptime assert(enabled); + return addAddr(t, @returnAddress(), note); + } + + pub inline fn addNoOp(t: *@This(), note: []const u8) void { + _ = t; + _ = note; + comptime assert(!enabled); + } + + pub fn addAddr(t: *@This(), addr: usize, note: []const u8) void { + if (!enabled) return; + + if (t.index < size) { + t.notes[t.index] = note; + t.addrs[t.index] = [1]usize{0} ** stack_frame_count; + var stack_trace: std.builtin.StackTrace = .{ + .index = 0, + .instruction_addresses = &t.addrs[t.index], + }; + captureStackTrace(addr, &stack_trace); + } + // Keep counting even if the end is reached so that the + // user can find out how much more size they need. + t.index += 1; + } + + pub fn dump(t: @This()) void { + if (!enabled) return; + + const tty_config = io.tty.detectConfig(std.io.getStdErr()); + const stderr = io.getStdErr().writer(); + const end = @min(t.index, size); + const debug_info = getSelfDebugInfo() catch |err| { + stderr.print( + "Unable to dump stack trace: Unable to open debug info: {s}\n", + .{@errorName(err)}, + ) catch return; + return; + }; + for (t.addrs[0..end], 0..) |frames_array, i| { + stderr.print("{s}:\n", .{t.notes[i]}) catch return; + var frames_array_mutable = frames_array; + const frames = mem.sliceTo(frames_array_mutable[0..], 0); + const stack_trace: std.builtin.StackTrace = .{ + .index = frames.len, + .instruction_addresses = frames, + }; + writeStackTrace(stack_trace, stderr, debug_info, tty_config) catch continue; + } + if (t.index > end) { + stderr.print("{d} more traces not shown; consider increasing trace size\n", .{ + t.index - end, + }) catch return; + } + } + + pub fn format( + t: @This(), + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + if (fmt.len != 0) std.fmt.invalidFmtError(fmt, t); + _ = options; + if (enabled) { + try writer.writeAll("\n"); + t.dump(); + try writer.writeAll("\n"); + } else { + return writer.writeAll("(value tracing disabled)"); + } + } + }; +} + +pub const SafetyLock = struct { + state: State = if (runtime_safety) .unlocked else .unknown, + + pub const State = if (runtime_safety) enum { unlocked, locked } else enum { unknown }; + + pub fn lock(l: *SafetyLock) void { + if (!runtime_safety) return; + assert(l.state == .unlocked); + l.state = .locked; + } + + pub fn unlock(l: *SafetyLock) void { + if (!runtime_safety) return; + assert(l.state == .locked); + l.state = .unlocked; + } + + pub fn assertUnlocked(l: SafetyLock) void { + if (!runtime_safety) return; + assert(l.state == .unlocked); + } + + pub fn assertLocked(l: SafetyLock) void { + if (!runtime_safety) return; + assert(l.state == .locked); + } +}; + +test SafetyLock { + var safety_lock: SafetyLock = .{}; + safety_lock.assertUnlocked(); + safety_lock.lock(); + safety_lock.assertLocked(); + safety_lock.unlock(); + safety_lock.assertUnlocked(); +} + +/// Detect whether the program is being executed in the Valgrind virtual machine. +/// +/// When Valgrind integrations are disabled, this returns comptime-known false. +/// Otherwise, the result is runtime-known. +pub inline fn inValgrind() bool { + if (@inComptime()) return false; + if (!builtin.valgrind_support) return false; + return std.valgrind.runningOnValgrind() > 0; +} + +test { + _ = &Dwarf; + _ = &MemoryAccessor; + _ = &FixedBufferReader; + _ = &Pdb; + _ = &SelfInfo; + _ = &dumpHex; +}