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.
This commit is contained in:
Matthew Lugg 2026-01-12 11:41:53 +00:00 committed by mlugg
parent be84d7cb9b
commit 85cac9e5b6
7 changed files with 51 additions and 7 deletions

View file

@ -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"),
};
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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));
}

View file

@ -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,

View file

@ -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)];

View file

@ -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,