authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-01-12 11:41:53+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-01-13 07:24:49+01:00
log85cac9e5b6c3039537e87247f8640f7182df8540
tree378eaba88621711544e645f6c5287c788f3ce222
parentbe84d7cb9bdbf8a1a4212f10c1dfdc437af0532c

std: use sigaltstack for default segfault handler

This allows stack overflows to print stack traces. The size of the sigaltstack (and whether it is actually set) can be configured by setting `std.Options.signal_stack_size`. The default value for the signal stack size was chosen experimentally by doubling the value required to get stack traces on stack overflow with the self-hosted x86_64 backend. While some targets may typically use more stack space than x86_64-linux, the self-hosted x86_64 backend is quite wasteful with stack at the moment, making it a fair benchmark. Executables produced by the LLVM backend should have lower stack usage.

7 files changed, 51 insertions(+), 7 deletions(-)

lib/std/Thread.zig+28-1
...@@ -540,13 +540,16 @@ const Completion = std.atomic.Value(enum(if (builtin.zig_backend == .stage2_risc...@@ -540,13 +540,16 @@ const Completion = std.atomic.Value(enum(if (builtin.zig_backend == .stage2_risc
540 completed,540 completed,
541});541});
542542
543/// Used by the Thread implementations to call the spawned function with the arguments.543/// Performs implementation-agnostic thread setup (`maybeAttachSignalStack`), then calls the given
544/// thread entry point `f` with `args` and handles the result.
544fn callFn(comptime f: anytype, args: anytype) switch (Impl) {545fn callFn(comptime f: anytype, args: anytype) switch (Impl) {
545 WindowsThreadImpl => windows.DWORD,546 WindowsThreadImpl => windows.DWORD,
546 LinuxThreadImpl => u8,547 LinuxThreadImpl => u8,
547 PosixThreadImpl => ?*anyopaque,548 PosixThreadImpl => ?*anyopaque,
548 else => unreachable,549 else => unreachable,
549} {550} {
551 maybeAttachSignalStack();
552
550 const default_value = if (Impl == PosixThreadImpl) null else 0;553 const default_value = if (Impl == PosixThreadImpl) null else 0;
551 const bad_fn_ret = "expected return type of startFn to be 'u8', 'noreturn', '!noreturn', 'void', or '!void'";554 const bad_fn_ret = "expected return type of startFn to be 'u8', 'noreturn', '!noreturn', 'void', or '!void'";
552555
...@@ -1917,3 +1920,27 @@ test "ResetEvent broadcast" {...@@ -1917,3 +1920,27 @@ test "ResetEvent broadcast" {
19171920
1918 ctx.run();1921 ctx.run();
1919}1922}
1923
1924/// Configures the per-thread alternative signal stack requested by `std.options.signal_stack_size`.
1925pub fn maybeAttachSignalStack() void {
1926 const size = std.options.signal_stack_size orelse return;
1927 switch (builtin.target.os.tag) {
1928 // TODO: Windows vectored exception handlers always run on the main stack, but we could use
1929 // some target-specific inline assembly to swap the stack pointer.
1930 .windows => return,
1931 .wasi => return,
1932 else => {},
1933 }
1934 const global = struct {
1935 threadlocal var signal_stack: [size]u8 = undefined;
1936 };
1937 std.posix.sigaltstack(&.{
1938 .sp = &global.signal_stack,
1939 .flags = 0,
1940 .size = size,
1941 }, null) catch |err| switch (err) {
1942 error.SizeTooSmall => unreachable, // `std.options.signal_stack_size` must be sufficient for the target
1943 error.PermissionDenied => unreachable, // called `maybeAttachSignalStack` from a signal handler
1944 error.Unexpected => @panic("unexpected error attaching signal stack"),
1945 };
1946}
lib/std/c.zig+2-2
...@@ -11490,7 +11490,7 @@ const private = struct {...@@ -11490,7 +11490,7 @@ const private = struct {
11490 extern "c" fn sigprocmask(how: c_int, noalias set: ?*const sigset_t, noalias oset: ?*sigset_t) c_int;11490 extern "c" fn sigprocmask(how: c_int, noalias set: ?*const sigset_t, noalias oset: ?*sigset_t) c_int;
11491 extern "c" fn socket(domain: c_uint, sock_type: c_uint, protocol: c_uint) c_int;11491 extern "c" fn socket(domain: c_uint, sock_type: c_uint, protocol: c_uint) c_int;
11492 extern "c" fn socketpair(domain: c_uint, sock_type: c_uint, protocol: c_uint, sv: *[2]fd_t) c_int;11492 extern "c" fn socketpair(domain: c_uint, sock_type: c_uint, protocol: c_uint, sv: *[2]fd_t) c_int;
11493 extern "c" fn sigaltstack(ss: ?*stack_t, old_ss: ?*stack_t) c_int;11493 extern "c" fn sigaltstack(ss: ?*const stack_t, old_ss: ?*stack_t) c_int;
11494 extern "c" fn sysconf(sc: c_int) c_long;11494 extern "c" fn sysconf(sc: c_int) c_long;
11495 extern "c" fn shm_open(name: [*:0]const u8, flag: c_int, mode: mode_t) c_int;11495 extern "c" fn shm_open(name: [*:0]const u8, flag: c_int, mode: mode_t) c_int;
11496 extern "c" fn wait4(pid: pid_t, status: ?*c_int, options: c_int, ru: ?*rusage) pid_t;11496 extern "c" fn wait4(pid: pid_t, status: ?*c_int, options: c_int, ru: ?*rusage) pid_t;
...@@ -11545,7 +11545,7 @@ const private = struct {...@@ -11545,7 +11545,7 @@ const private = struct {
11545 extern "c" fn __socket30(domain: c_uint, sock_type: c_uint, protocol: c_uint) c_int;11545 extern "c" fn __socket30(domain: c_uint, sock_type: c_uint, protocol: c_uint) c_int;
11546 extern "c" fn __stat50(path: [*:0]const u8, buf: *Stat) c_int;11546 extern "c" fn __stat50(path: [*:0]const u8, buf: *Stat) c_int;
11547 extern "c" fn __getdents30(fd: c_int, buf_ptr: [*]u8, nbytes: usize) c_int;11547 extern "c" fn __getdents30(fd: c_int, buf_ptr: [*]u8, nbytes: usize) c_int;
11548 extern "c" fn __sigaltstack14(ss: ?*stack_t, old_ss: ?*stack_t) c_int;11548 extern "c" fn __sigaltstack14(ss: ?*const stack_t, old_ss: ?*stack_t) c_int;
11549 extern "c" fn __wait450(pid: pid_t, status: ?*c_int, options: c_int, ru: ?*rusage) pid_t;11549 extern "c" fn __wait450(pid: pid_t, status: ?*c_int, options: c_int, ru: ?*rusage) pid_t;
1155011550
11551 extern "c" fn __libc_current_sigrtmin() c_int;11551 extern "c" fn __libc_current_sigrtmin() c_int;
lib/std/debug.zig+5-2
...@@ -1411,6 +1411,9 @@ pub fn updateSegfaultHandler(act: ?*const posix.Sigaction) void {...@@ -1411,6 +1411,9 @@ pub fn updateSegfaultHandler(act: ?*const posix.Sigaction) void {
1411/// trace if possible. This implementation does not just call the panic handler, because unwinding1411/// trace if possible. This implementation does not just call the panic handler, because unwinding
1412/// the stack (for a stack trace) when a signal is received requires special target-specific logic.1412/// the stack (for a stack trace) when a signal is received requires special target-specific logic.
1413///1413///
1414/// On POSIX targets, the signal handler is configured to use the alternative signal stack. Such a
1415/// stack is configured by the Zig Standard Library if `std.options.signal_stack_size` is set.
1416///
1414/// The signals for which a handler is installed are:1417/// The signals for which a handler is installed are:
1415/// * SIGSEGV (segmentation fault)1418/// * SIGSEGV (segmentation fault)
1416/// * SIGILL (illegal instruction)1419/// * SIGILL (illegal instruction)
...@@ -1424,10 +1427,10 @@ pub fn attachSegfaultHandler() void {...@@ -1424,10 +1427,10 @@ pub fn attachSegfaultHandler() void {
1424 windows_segfault_handle = windows.ntdll.RtlAddVectoredExceptionHandler(0, handleSegfaultWindows);1427 windows_segfault_handle = windows.ntdll.RtlAddVectoredExceptionHandler(0, handleSegfaultWindows);
1425 return;1428 return;
1426 }1429 }
1427 const act = posix.Sigaction{1430 const act: posix.Sigaction = .{
1428 .handler = .{ .sigaction = handleSegfaultPosix },1431 .handler = .{ .sigaction = handleSegfaultPosix },
1429 .mask = posix.sigemptyset(),1432 .mask = posix.sigemptyset(),
1430 .flags = (posix.SA.SIGINFO | posix.SA.RESTART | posix.SA.RESETHAND),1433 .flags = (posix.SA.SIGINFO | posix.SA.RESTART | posix.SA.RESETHAND | posix.SA.ONSTACK),
1431 };1434 };
1432 updateSegfaultHandler(&act);1435 updateSegfaultHandler(&act);
1433}1436}
lib/std/os/linux.zig+1-1
...@@ -2488,7 +2488,7 @@ pub fn capset(hdrp: *cap_user_header_t, datap: *const cap_user_data_t) usize {...@@ -2488,7 +2488,7 @@ pub fn capset(hdrp: *cap_user_header_t, datap: *const cap_user_data_t) usize {
2488 return syscall2(.capset, @intFromPtr(hdrp), @intFromPtr(datap));2488 return syscall2(.capset, @intFromPtr(hdrp), @intFromPtr(datap));
2489}2489}
24902490
2491pub fn sigaltstack(ss: ?*stack_t, old_ss: ?*stack_t) usize {2491pub fn sigaltstack(ss: ?*const stack_t, old_ss: ?*stack_t) usize {
2492 return syscall2(.sigaltstack, @intFromPtr(ss), @intFromPtr(old_ss));2492 return syscall2(.sigaltstack, @intFromPtr(ss), @intFromPtr(old_ss));
2493}2493}
24942494
lib/std/posix.zig+1-1
...@@ -1310,7 +1310,7 @@ pub const SigaltstackError = error{...@@ -1310,7 +1310,7 @@ pub const SigaltstackError = error{
1310 PermissionDenied,1310 PermissionDenied,
1311} || UnexpectedError;1311} || UnexpectedError;
13121312
1313pub fn sigaltstack(ss: ?*stack_t, old_ss: ?*stack_t) SigaltstackError!void {1313pub fn sigaltstack(ss: ?*const stack_t, old_ss: ?*stack_t) SigaltstackError!void {
1314 switch (errno(system.sigaltstack(ss, old_ss))) {1314 switch (errno(system.sigaltstack(ss, old_ss))) {
1315 .SUCCESS => return,1315 .SUCCESS => return,
1316 .FAULT => unreachable,1316 .FAULT => unreachable,
lib/std/start.zig+4
...@@ -470,6 +470,7 @@ fn WinStartup() callconv(.withStackAlign(.c, 1)) noreturn {...@@ -470,6 +470,7 @@ fn WinStartup() callconv(.withStackAlign(.c, 1)) noreturn {
470 _ = @import("os/windows/tls.zig");470 _ = @import("os/windows/tls.zig");
471 }471 }
472472
473 std.Thread.maybeAttachSignalStack();
473 std.debug.maybeEnableSegfaultHandler();474 std.debug.maybeEnableSegfaultHandler();
474475
475 const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine;476 const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine;
...@@ -486,6 +487,7 @@ fn wWinMainCRTStartup() callconv(.withStackAlign(.c, 1)) noreturn {...@@ -486,6 +487,7 @@ fn wWinMainCRTStartup() callconv(.withStackAlign(.c, 1)) noreturn {
486 _ = @import("os/windows/tls.zig");487 _ = @import("os/windows/tls.zig");
487 }488 }
488489
490 std.Thread.maybeAttachSignalStack();
489 std.debug.maybeEnableSegfaultHandler();491 std.debug.maybeEnableSegfaultHandler();
490492
491 const result: std.os.windows.INT = call_wWinMain();493 const result: std.os.windows.INT = call_wWinMain();
...@@ -622,6 +624,7 @@ inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [:null]?[*:0]u8)...@@ -622,6 +624,7 @@ inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [:null]?[*:0]u8)
622 if (@sizeOf(std.Io.Threaded.Argv0) != 0) t.argv0.value = argv[0];624 if (@sizeOf(std.Io.Threaded.Argv0) != 0) t.argv0.value = argv[0];
623 t.environ = .{ .process_environ = .{ .block = envp } };625 t.environ = .{ .process_environ = .{ .block = envp } };
624 }626 }
627 std.Thread.maybeAttachSignalStack();
625 std.debug.maybeEnableSegfaultHandler();628 std.debug.maybeEnableSegfaultHandler();
626 return callMain(argv[0..argc], envp);629 return callMain(argv[0..argc], envp);
627}630}
...@@ -641,6 +644,7 @@ fn main(c_argc: c_int, c_argv: [*][*:0]c_char, c_envp: [*:null]?[*:0]c_char) cal...@@ -641,6 +644,7 @@ fn main(c_argc: c_int, c_argv: [*][*:0]c_char, c_envp: [*:null]?[*:0]c_char) cal
641 .windows => {644 .windows => {
642 // On Windows, we ignore libc environment and argv and get those645 // On Windows, we ignore libc environment and argv and get those
643 // values in their intended encoding from the PEB instead.646 // values in their intended encoding from the PEB instead.
647 std.Thread.maybeAttachSignalStack();
644 std.debug.maybeEnableSegfaultHandler();648 std.debug.maybeEnableSegfaultHandler();
645 const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine;649 const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine;
646 const cmd_line_w = cmd_line.Buffer.?[0..@divExact(cmd_line.Length, 2)];650 const cmd_line_w = cmd_line.Buffer.?[0..@divExact(cmd_line.Length, 2)];
lib/std/std.zig+10
...@@ -114,6 +114,16 @@ pub const options: Options = if (@hasDecl(root, "std_options")) root.std_options...@@ -114,6 +114,16 @@ pub const options: Options = if (@hasDecl(root, "std_options")) root.std_options
114pub const Options = struct {114pub const Options = struct {
115 enable_segfault_handler: bool = debug.default_enable_segfault_handler,115 enable_segfault_handler: bool = debug.default_enable_segfault_handler,
116116
117 /// If set, `std.start` and `std.Thread` will configure an per-thread alternative signal stack
118 /// of this size. Importantly, if `enable_segfault_handler` is set, the segfault handler will
119 /// use this alternative stack, meaning it can still print stack traces even if a segmentation
120 /// fault is caused by a stack overflow.
121 ///
122 /// On POSIX targets, the signal stack is configured using 'sigaltstack(2)'.
123 ///
124 /// On Windows, this value is currently ignored.
125 signal_stack_size: ?u64 = 1 << 18, // 1<<17 observed to be sufficient for stack tracing with self-hosted x86_64 backend
126
117 /// The current log level.127 /// The current log level.
118 log_level: log.Level = log.default_level,128 log_level: log.Level = log.default_level,
119129