From 111165513156d5732d85e5ccb52b9d8bded41ffa Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Tue, 6 Jan 2026 10:50:45 +0000 Subject: [PATCH 1/3] std: block cancelation in default panic and segfault handlers It doesn't make any sense for a task to be canceled while it's panicking. As a happy accident, this also solves some cases where safety panics in `Io.Threaded` would cause stack traces not to print due to invalid thread-local state: when cancelation is blocked, `Io.Threaded` doesn't consult said thread-local state at all. For instance, try inserting a panic just after a call to `Syscall.start()` in `Io.Threaded`, and then call the `Io` function in question from a `concurrent` task. Before this PR, the stack trace fails to print, because the panic handler sees the thread-local cancelation state in an unexpected state, leading to a recursive panic. After this PR, the stack trace prints fine. --- lib/std/debug.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 0f151447fc56b4d7afdde22709b2bc0fe414d554..8b76e307a154654c132b83480f5abf19f5243fcd 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -532,6 +532,10 @@ pub fn defaultPanic(msg: []const u8, first_trace_addr: ?usize) noreturn { else => {}, } + // Don't try to cancel during a panic. No need to re-enable cancelation, + // because the panic handler doesn't return. + _ = std.Options.debug_io.swapCancelProtection(.blocked); + if (enable_segfault_handler) { // If a segfault happens while panicking, we want it to actually segfault, not trigger // the handler. @@ -1533,6 +1537,10 @@ fn handleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContextPtr) noret } pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContextPtr) noreturn { + // Don't try to cancel during a segfault. No need to re-enable cancelation, + // because the segfault handler doesn't return. + _ = std.Options.debug_io.swapCancelProtection(.blocked); + // There is very similar logic to the following in `defaultPanic`. switch (panic_stage) { 0 => { -- 2.54.0 From 073ef0f393ff4297e8d48a72f30a605c2d272289 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Tue, 6 Jan 2026 11:05:50 +0000 Subject: [PATCH 2/3] std.Thread: mask all signals before unmapping stack As the comment explains, if a signal were to arrive between a detached thread's `munmap` and `exit` calls, the signal handler would immediately trigger SIGSEGV due to the stack being unmapped. To solve this, we need to block all signals before entering this logic. The musl implementation which this logic was ported from does this exact thing; that logic was just lost when porting. Notably, this would lead to a crash with no stack trace, because the SIGSEGV handler would itself crash due to the missing stack. --- lib/std/Thread.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 126338e77f0a82eae15042c49a20c3155d677faf..1c361506b72e3eb1a8c6125642b68ba1e6fba51a 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -1224,6 +1224,10 @@ const LinuxThreadImpl = struct { /// Ported over from musl libc's pthread detached implementation: /// https://github.com/ifduyue/musl/search?q=__unmapself fn freeAndExit(self: *ThreadCompletion) noreturn { + // If a signal were delivered between SYS_munmap and SYS_exit, any installed signal + // handler would immediately segfault due to the stack being unmapped. To avoid this, + // we need to mask all signals before entering the inline asm. + posix.sigprocmask(std.posix.SIG.BLOCK, &std.os.linux.sigfillset(), null); switch (target.cpu.arch) { .x86 => asm volatile ( \\ movl $91, %%eax # SYS_munmap -- 2.54.0 From be0a77efd2aad5108d3357914cbc71f560ea0161 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Tue, 6 Jan 2026 11:13:13 +0000 Subject: [PATCH 3/3] std: re-enable some disabled tests I believe these tests may have been flaky as a result of the bug fixed in the previous commit. A big hint is that they were all crashing with SIGSEGV with no stack trace. I suspect that some lingering SIGIOs from cancelations were being delivered to a thread after its `munmap` call, which was happening because the test runner called `Io.Threaded.deinit` to cause all of the (detached) worker threads to exit. If this passes, I'll re-run the x86_64-linux CI jobs on this commit a few times before merge to try and be sure there are no lingering failures. Resolves: https://codeberg.org/ziglang/zig/issues/30096 Resolves: https://codeberg.org/ziglang/zig/issues/30592 Resolves: https://codeberg.org/ziglang/zig/issues/30682 --- lib/std/Io/test.zig | 4 ---- lib/std/crypto/argon2.zig | 2 -- 2 files changed, 6 deletions(-) diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 6af9a5bad98e3570e281ca6b346628411e567480..a467ee1a93992a32c460198e9de539fb1366df28 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -254,8 +254,6 @@ test "Group.cancel" { } test "Group.concurrent" { - if (builtin.os.tag == .linux and !builtin.link_libc) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30096 - const io = testing.io; var group: Io.Group = .init; @@ -421,8 +419,6 @@ test "Event" { } test "recancel" { - if (builtin.os.tag == .linux and !builtin.link_libc) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30592 - const global = struct { fn worker(io: Io) Io.Cancelable!void { var dummy_event: Io.Event = .unset; diff --git a/lib/std/crypto/argon2.zig b/lib/std/crypto/argon2.zig index 2bb5a49a45feb4a0eeb59affaa612f94db6177e7..0fd76e1282ddf29f9426ee03696faede747769a6 100644 --- a/lib/std/crypto/argon2.zig +++ b/lib/std/crypto/argon2.zig @@ -907,8 +907,6 @@ test "kdf" { } test "phc format hasher" { - if (true) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30682 - const allocator = std.testing.allocator; const password = "testpass"; const io = std.testing.io; -- 2.54.0