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