zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit 85cac9e5b6c3039537e87247f8640f7182df8540 (tree)
parent be84d7cb9bdbf8a1a4212f10c1dfdc437af0532c
Author: Matthew Lugg <mlugg@mlugg.co.uk>
Date:   Mon, 12 Jan 2026 11:41:53 +0000

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.

Diffstat:
Mlib/std/Thread.zig | 29++++++++++++++++++++++++++++-
Mlib/std/c.zig | 4++--
Mlib/std/debug.zig | 7+++++--
Mlib/std/os/linux.zig | 2+-
Mlib/std/posix.zig | 2+-
Mlib/std/start.zig | 4++++
Mlib/std/std.zig | 10++++++++++
7 files changed, 51 insertions(+), 7 deletions(-)

diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig @@ -540,13 +540,16 @@ const Completion = std.atomic.Value(enum(if (builtin.zig_backend == .stage2_risc completed, }); -/// Used by the Thread implementations to call the spawned function with the arguments. +/// Performs implementation-agnostic thread setup (`maybeAttachSignalStack`), then calls the given +/// thread entry point `f` with `args` and handles the result. fn callFn(comptime f: anytype, args: anytype) switch (Impl) { WindowsThreadImpl => windows.DWORD, LinuxThreadImpl => u8, PosixThreadImpl => ?*anyopaque, else => unreachable, } { + maybeAttachSignalStack(); + const default_value = if (Impl == PosixThreadImpl) null else 0; const bad_fn_ret = "expected return type of startFn to be 'u8', 'noreturn', '!noreturn', 'void', or '!void'"; @@ -1917,3 +1920,27 @@ test "ResetEvent broadcast" { ctx.run(); } + +/// Configures the per-thread alternative signal stack requested by `std.options.signal_stack_size`. +pub fn maybeAttachSignalStack() void { + const size = std.options.signal_stack_size orelse return; + switch (builtin.target.os.tag) { + // TODO: Windows vectored exception handlers always run on the main stack, but we could use + // some target-specific inline assembly to swap the stack pointer. + .windows => return, + .wasi => return, + else => {}, + } + const global = struct { + threadlocal var signal_stack: [size]u8 = undefined; + }; + std.posix.sigaltstack(&.{ + .sp = &global.signal_stack, + .flags = 0, + .size = size, + }, null) catch |err| switch (err) { + error.SizeTooSmall => unreachable, // `std.options.signal_stack_size` must be sufficient for the target + error.PermissionDenied => unreachable, // called `maybeAttachSignalStack` from a signal handler + error.Unexpected => @panic("unexpected error attaching signal stack"), + }; +} diff --git a/lib/std/c.zig b/lib/std/c.zig @@ -11490,7 +11490,7 @@ const private = struct { extern "c" fn sigprocmask(how: c_int, noalias set: ?*const sigset_t, noalias oset: ?*sigset_t) c_int; extern "c" fn socket(domain: c_uint, sock_type: c_uint, protocol: c_uint) c_int; extern "c" fn socketpair(domain: c_uint, sock_type: c_uint, protocol: c_uint, sv: *[2]fd_t) c_int; - extern "c" fn sigaltstack(ss: ?*stack_t, old_ss: ?*stack_t) c_int; + extern "c" fn sigaltstack(ss: ?*const stack_t, old_ss: ?*stack_t) c_int; extern "c" fn sysconf(sc: c_int) c_long; extern "c" fn shm_open(name: [*:0]const u8, flag: c_int, mode: mode_t) c_int; extern "c" fn wait4(pid: pid_t, status: ?*c_int, options: c_int, ru: ?*rusage) pid_t; @@ -11545,7 +11545,7 @@ const private = struct { extern "c" fn __socket30(domain: c_uint, sock_type: c_uint, protocol: c_uint) c_int; extern "c" fn __stat50(path: [*:0]const u8, buf: *Stat) c_int; extern "c" fn __getdents30(fd: c_int, buf_ptr: [*]u8, nbytes: usize) c_int; - extern "c" fn __sigaltstack14(ss: ?*stack_t, old_ss: ?*stack_t) c_int; + extern "c" fn __sigaltstack14(ss: ?*const stack_t, old_ss: ?*stack_t) c_int; extern "c" fn __wait450(pid: pid_t, status: ?*c_int, options: c_int, ru: ?*rusage) pid_t; extern "c" fn __libc_current_sigrtmin() c_int; diff --git a/lib/std/debug.zig b/lib/std/debug.zig @@ -1411,6 +1411,9 @@ pub fn updateSegfaultHandler(act: ?*const posix.Sigaction) void { /// trace if possible. This implementation does not just call the panic handler, because unwinding /// the stack (for a stack trace) when a signal is received requires special target-specific logic. /// +/// On POSIX targets, the signal handler is configured to use the alternative signal stack. Such a +/// stack is configured by the Zig Standard Library if `std.options.signal_stack_size` is set. +/// /// The signals for which a handler is installed are: /// * SIGSEGV (segmentation fault) /// * SIGILL (illegal instruction) @@ -1424,10 +1427,10 @@ pub fn attachSegfaultHandler() void { windows_segfault_handle = windows.ntdll.RtlAddVectoredExceptionHandler(0, handleSegfaultWindows); return; } - const act = posix.Sigaction{ + const act: posix.Sigaction = .{ .handler = .{ .sigaction = handleSegfaultPosix }, .mask = posix.sigemptyset(), - .flags = (posix.SA.SIGINFO | posix.SA.RESTART | posix.SA.RESETHAND), + .flags = (posix.SA.SIGINFO | posix.SA.RESTART | posix.SA.RESETHAND | posix.SA.ONSTACK), }; updateSegfaultHandler(&act); } diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig @@ -2488,7 +2488,7 @@ pub fn capset(hdrp: *cap_user_header_t, datap: *const cap_user_data_t) usize { return syscall2(.capset, @intFromPtr(hdrp), @intFromPtr(datap)); } -pub fn sigaltstack(ss: ?*stack_t, old_ss: ?*stack_t) usize { +pub fn sigaltstack(ss: ?*const stack_t, old_ss: ?*stack_t) usize { return syscall2(.sigaltstack, @intFromPtr(ss), @intFromPtr(old_ss)); } diff --git a/lib/std/posix.zig b/lib/std/posix.zig @@ -1310,7 +1310,7 @@ pub const SigaltstackError = error{ PermissionDenied, } || UnexpectedError; -pub fn sigaltstack(ss: ?*stack_t, old_ss: ?*stack_t) SigaltstackError!void { +pub fn sigaltstack(ss: ?*const stack_t, old_ss: ?*stack_t) SigaltstackError!void { switch (errno(system.sigaltstack(ss, old_ss))) { .SUCCESS => return, .FAULT => unreachable, diff --git a/lib/std/start.zig b/lib/std/start.zig @@ -470,6 +470,7 @@ fn WinStartup() callconv(.withStackAlign(.c, 1)) noreturn { _ = @import("os/windows/tls.zig"); } + std.Thread.maybeAttachSignalStack(); std.debug.maybeEnableSegfaultHandler(); const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine; @@ -486,6 +487,7 @@ fn wWinMainCRTStartup() callconv(.withStackAlign(.c, 1)) noreturn { _ = @import("os/windows/tls.zig"); } + std.Thread.maybeAttachSignalStack(); std.debug.maybeEnableSegfaultHandler(); const result: std.os.windows.INT = call_wWinMain(); @@ -622,6 +624,7 @@ inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [:null]?[*:0]u8) if (@sizeOf(std.Io.Threaded.Argv0) != 0) t.argv0.value = argv[0]; t.environ = .{ .process_environ = .{ .block = envp } }; } + std.Thread.maybeAttachSignalStack(); std.debug.maybeEnableSegfaultHandler(); return callMain(argv[0..argc], envp); } @@ -641,6 +644,7 @@ fn main(c_argc: c_int, c_argv: [*][*:0]c_char, c_envp: [*:null]?[*:0]c_char) cal .windows => { // On Windows, we ignore libc environment and argv and get those // values in their intended encoding from the PEB instead. + std.Thread.maybeAttachSignalStack(); std.debug.maybeEnableSegfaultHandler(); const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine; const cmd_line_w = cmd_line.Buffer.?[0..@divExact(cmd_line.Length, 2)]; diff --git a/lib/std/std.zig b/lib/std/std.zig @@ -114,6 +114,16 @@ pub const options: Options = if (@hasDecl(root, "std_options")) root.std_options pub const Options = struct { enable_segfault_handler: bool = debug.default_enable_segfault_handler, + /// If set, `std.start` and `std.Thread` will configure an per-thread alternative signal stack + /// of this size. Importantly, if `enable_segfault_handler` is set, the segfault handler will + /// use this alternative stack, meaning it can still print stack traces even if a segmentation + /// fault is caused by a stack overflow. + /// + /// On POSIX targets, the signal stack is configured using 'sigaltstack(2)'. + /// + /// On Windows, this value is currently ignored. + signal_stack_size: ?u64 = 1 << 18, // 1<<17 observed to be sufficient for stack tracing with self-hosted x86_64 backend + /// The current log level. log_level: log.Level = log.default_level,