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
540540 completed,
541541});
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.
544545fn callFn(comptime f: anytype, args: anytype) switch (Impl) {
545546 WindowsThreadImpl => windows.DWORD,
546547 LinuxThreadImpl => u8,
547548 PosixThreadImpl => ?*anyopaque,
548549 else => unreachable,
549550} {
551 maybeAttachSignalStack();
552
550553 const default_value = if (Impl == PosixThreadImpl) null else 0;
551554 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" {
19171920
19181921 ctx.run();
19191922}
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 {
1149011490 extern "c" fn sigprocmask(how: c_int, noalias set: ?*const sigset_t, noalias oset: ?*sigset_t) c_int;
1149111491 extern "c" fn socket(domain: c_uint, sock_type: c_uint, protocol: c_uint) c_int;
1149211492 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;
1149411494 extern "c" fn sysconf(sc: c_int) c_long;
1149511495 extern "c" fn shm_open(name: [*:0]const u8, flag: c_int, mode: mode_t) c_int;
1149611496 extern "c" fn wait4(pid: pid_t, status: ?*c_int, options: c_int, ru: ?*rusage) pid_t;
......@@ -11545,7 +11545,7 @@ const private = struct {
1154511545 extern "c" fn __socket30(domain: c_uint, sock_type: c_uint, protocol: c_uint) c_int;
1154611546 extern "c" fn __stat50(path: [*:0]const u8, buf: *Stat) c_int;
1154711547 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;
1154911549 extern "c" fn __wait450(pid: pid_t, status: ?*c_int, options: c_int, ru: ?*rusage) pid_t;
1155011550
1155111551 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 {
14111411/// trace if possible. This implementation does not just call the panic handler, because unwinding
14121412/// the stack (for a stack trace) when a signal is received requires special target-specific logic.
14131413///
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///
14141417/// The signals for which a handler is installed are:
14151418/// * SIGSEGV (segmentation fault)
14161419/// * SIGILL (illegal instruction)
......@@ -1424,10 +1427,10 @@ pub fn attachSegfaultHandler() void {
14241427 windows_segfault_handle = windows.ntdll.RtlAddVectoredExceptionHandler(0, handleSegfaultWindows);
14251428 return;
14261429 }
1427 const act = posix.Sigaction{
1430 const act: posix.Sigaction = .{
14281431 .handler = .{ .sigaction = handleSegfaultPosix },
14291432 .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),
14311434 };
14321435 updateSegfaultHandler(&act);
14331436}
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 {
24882488 return syscall2(.capset, @intFromPtr(hdrp), @intFromPtr(datap));
24892489}
24902490
2491pub fn sigaltstack(ss: ?*stack_t, old_ss: ?*stack_t) usize {
2491pub fn sigaltstack(ss: ?*const stack_t, old_ss: ?*stack_t) usize {
24922492 return syscall2(.sigaltstack, @intFromPtr(ss), @intFromPtr(old_ss));
24932493}
24942494
lib/std/posix.zig+1-1
......@@ -1310,7 +1310,7 @@ pub const SigaltstackError = error{
13101310 PermissionDenied,
13111311} || 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 {
13141314 switch (errno(system.sigaltstack(ss, old_ss))) {
13151315 .SUCCESS => return,
13161316 .FAULT => unreachable,
lib/std/start.zig+4
......@@ -470,6 +470,7 @@ fn WinStartup() callconv(.withStackAlign(.c, 1)) noreturn {
470470 _ = @import("os/windows/tls.zig");
471471 }
472472
473 std.Thread.maybeAttachSignalStack();
473474 std.debug.maybeEnableSegfaultHandler();
474475
475476 const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine;
......@@ -486,6 +487,7 @@ fn wWinMainCRTStartup() callconv(.withStackAlign(.c, 1)) noreturn {
486487 _ = @import("os/windows/tls.zig");
487488 }
488489
490 std.Thread.maybeAttachSignalStack();
489491 std.debug.maybeEnableSegfaultHandler();
490492
491493 const result: std.os.windows.INT = call_wWinMain();
......@@ -622,6 +624,7 @@ inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [:null]?[*:0]u8)
622624 if (@sizeOf(std.Io.Threaded.Argv0) != 0) t.argv0.value = argv[0];
623625 t.environ = .{ .process_environ = .{ .block = envp } };
624626 }
627 std.Thread.maybeAttachSignalStack();
625628 std.debug.maybeEnableSegfaultHandler();
626629 return callMain(argv[0..argc], envp);
627630}
......@@ -641,6 +644,7 @@ fn main(c_argc: c_int, c_argv: [*][*:0]c_char, c_envp: [*:null]?[*:0]c_char) cal
641644 .windows => {
642645 // On Windows, we ignore libc environment and argv and get those
643646 // values in their intended encoding from the PEB instead.
647 std.Thread.maybeAttachSignalStack();
644648 std.debug.maybeEnableSegfaultHandler();
645649 const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine;
646650 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
114114pub const Options = struct {
115115 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
117127 /// The current log level.
118128 log_level: log.Level = log.default_level,
119129