Commit d16079d79a

Pat Tullmann <pat.github@tullmann.org>
2025-04-14 01:15:16
posix.zig: export sigset_t and matching operations from system
Unify the C library sigset_t and Linux native sigset_t and the accessor operations. Add tests that the various sigset_t operations are working. And clean up existing tests a bit.
1 parent 298b188
Changed files (2)
lib
lib/std/posix/test.zig
@@ -859,12 +859,64 @@ test "shutdown socket" {
     std.net.Stream.close(.{ .handle = sock });
 }
 
-test "sigaction" {
+test "sigset empty/full" {
+    if (native_os == .wasi or native_os == .windows)
+        return error.SkipZigTest;
+
+    var set: posix.sigset_t = undefined;
+
+    posix.sigemptyset(&set);
+    for (1..posix.NSIG) |i| {
+        try expectEqual(false, posix.sigismember(&set, @truncate(i)));
+    }
+
+    // The C library can reserve some (unnamed) signals, so can't check the full
+    // NSIG set is defined, but just test a couple:
+    posix.sigfillset(&set);
+    try expectEqual(true, posix.sigismember(&set, @truncate(posix.SIG.USR1)));
+    try expectEqual(true, posix.sigismember(&set, @truncate(posix.SIG.INT)));
+}
+
+// Some signals (32 - 34 on glibc/musl) are not allowed to be added to a
+// sigset by the C library, so avoid testing them.
+fn reserved_signo(i: usize) bool {
+    return builtin.link_libc and (i >= 32 and i <= 34);
+}
+
+test "sigset add/del" {
     if (native_os == .wasi or native_os == .windows)
         return error.SkipZigTest;
 
-    // https://github.com/ziglang/zig/issues/7427
-    if (native_os == .linux and builtin.target.cpu.arch == .x86)
+    var sigset = posix.empty_sigset;
+
+    // See that none are set, then set each one, see that they're all set, then
+    // remove them all, and then see that none are set.
+    for (1..posix.NSIG) |i| {
+        try expectEqual(false, posix.sigismember(&sigset, @truncate(i)));
+    }
+    for (1..posix.NSIG) |i| {
+        if (!reserved_signo(i)) {
+            posix.sigaddset(&sigset, @truncate(i));
+        }
+    }
+    for (1..posix.NSIG) |i| {
+        if (!reserved_signo(i)) {
+            try expectEqual(true, posix.sigismember(&sigset, @truncate(i)));
+        }
+        try expectEqual(false, posix.sigismember(&posix.empty_sigset, @truncate(i)));
+    }
+    for (1..posix.NSIG) |i| {
+        if (!reserved_signo(i)) {
+            posix.sigdelset(&sigset, @truncate(i));
+        }
+    }
+    for (1..posix.NSIG) |i| {
+        try expectEqual(false, posix.sigismember(&sigset, @truncate(i)));
+    }
+}
+
+test "sigaction" {
+    if (native_os == .wasi or native_os == .windows)
         return error.SkipZigTest;
 
     // https://github.com/ziglang/zig/issues/15381
@@ -872,21 +924,20 @@ test "sigaction" {
         return error.SkipZigTest;
     }
 
+    const test_signo = posix.SIG.USR1;
+
     const S = struct {
         var handler_called_count: u32 = 0;
 
         fn handler(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void {
             _ = ctx_ptr;
             // Check that we received the correct signal.
-            switch (native_os) {
-                .netbsd => {
-                    if (sig == posix.SIG.USR1 and sig == info.info.signo)
-                        handler_called_count += 1;
-                },
-                else => {
-                    if (sig == posix.SIG.USR1 and sig == info.signo)
-                        handler_called_count += 1;
-                },
+            const info_sig = switch (native_os) {
+                .netbsd => info.info.signo,
+                else => info.signo,
+            };
+            if (sig == test_signo and sig == info_sig) {
+                handler_called_count += 1;
             }
         }
     };
@@ -899,39 +950,109 @@ test "sigaction" {
     var old_sa: posix.Sigaction = undefined;
 
     // Install the new signal handler.
-    posix.sigaction(posix.SIG.USR1, &sa, null);
+    posix.sigaction(test_signo, &sa, null);
 
     // Check that we can read it back correctly.
-    posix.sigaction(posix.SIG.USR1, null, &old_sa);
+    posix.sigaction(test_signo, null, &old_sa);
     try testing.expectEqual(&S.handler, old_sa.handler.sigaction.?);
     try testing.expect((old_sa.flags & posix.SA.SIGINFO) != 0);
 
     // Invoke the handler.
-    try posix.raise(posix.SIG.USR1);
-    try testing.expect(S.handler_called_count == 1);
+    try posix.raise(test_signo);
+    try testing.expectEqual(1, S.handler_called_count);
 
     // Check if passing RESETHAND correctly reset the handler to SIG_DFL
-    posix.sigaction(posix.SIG.USR1, null, &old_sa);
+    posix.sigaction(test_signo, null, &old_sa);
     try testing.expectEqual(posix.SIG.DFL, old_sa.handler.handler);
 
     // Reinstall the signal w/o RESETHAND and re-raise
     sa.flags = posix.SA.SIGINFO;
-    posix.sigaction(posix.SIG.USR1, &sa, null);
-    try posix.raise(posix.SIG.USR1);
-    try testing.expect(S.handler_called_count == 2);
+    posix.sigaction(test_signo, &sa, null);
+    try posix.raise(test_signo);
+    try testing.expectEqual(2, S.handler_called_count);
 
     // Now set the signal to ignored
     sa.handler = .{ .handler = posix.SIG.IGN };
     sa.flags = 0;
-    posix.sigaction(posix.SIG.USR1, &sa, null);
+    posix.sigaction(test_signo, &sa, null);
 
     // Re-raise to ensure handler is actually ignored
-    try posix.raise(posix.SIG.USR1);
-    try testing.expect(S.handler_called_count == 2);
+    try posix.raise(test_signo);
+    try testing.expectEqual(2, S.handler_called_count);
 
     // Ensure that ignored state is returned when querying
-    posix.sigaction(posix.SIG.USR1, null, &old_sa);
-    try testing.expectEqual(posix.SIG.IGN, old_sa.handler.handler.?);
+    posix.sigaction(test_signo, null, &old_sa);
+    try testing.expectEqual(posix.SIG.IGN, old_sa.handler.handler);
+}
+
+test "sigset_t bits" {
+    if (native_os == .wasi or native_os == .windows)
+        return error.SkipZigTest;
+
+    const S = struct {
+        var expected_sig: i32 = undefined;
+        var handler_called_count: u32 = 0;
+
+        fn handler(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void {
+            _ = ctx_ptr;
+
+            const info_sig = switch (native_os) {
+                .netbsd => info.info.signo,
+                else => info.signo,
+            };
+            if (sig == expected_sig and sig == info_sig) {
+                handler_called_count += 1;
+            }
+        }
+    };
+
+    const self_pid = posix.system.getpid();
+
+    // To check that sigset_t mapping matches kernel (think u32/u64
+    // mismatches on big-endian), try sending a blocked signal to make
+    // sure the mask matches the signal.
+    inline for ([_]usize{ posix.SIG.INT, posix.SIG.USR1, 62, 94, 126 }) |test_signo| {
+        if (test_signo >= posix.NSIG) continue;
+
+        S.expected_sig = test_signo;
+        S.handler_called_count = 0;
+
+        var sa: posix.Sigaction = .{
+            .handler = .{ .sigaction = &S.handler },
+            .mask = posix.empty_sigset,
+            .flags = posix.SA.SIGINFO | posix.SA.RESETHAND,
+        };
+        var old_sa: posix.Sigaction = undefined;
+
+        // Install the new signal handler.
+        posix.sigaction(test_signo, &sa, &old_sa);
+
+        // block the signal and see that its delayed until unblocked
+        var block_one = posix.empty_sigset;
+        _ = posix.sigaddset(&block_one, test_signo);
+        posix.sigprocmask(posix.SIG.BLOCK, &block_one, null);
+
+        // qemu maps target signals to host signals 1-to-1, so targets
+        // with more signals than the host will fail to send the signal.
+        const rc = posix.system.kill(self_pid, test_signo);
+        switch (posix.errno(rc)) {
+            .SUCCESS => {
+                // See that the signal is blocked, then unblocked
+                try testing.expectEqual(0, S.handler_called_count);
+                posix.sigprocmask(posix.SIG.UNBLOCK, &block_one, null);
+                try testing.expectEqual(1, S.handler_called_count);
+            },
+            .INVAL => {
+                // Signal won't get delviered.  Just clean up.
+                posix.sigprocmask(posix.SIG.UNBLOCK, &block_one, null);
+                try testing.expectEqual(0, S.handler_called_count);
+            },
+            else => |errno| return posix.unexpectedErrno(errno),
+        }
+
+        // Restore original handler
+        posix.sigaction(test_signo, &old_sa, null);
+    }
 }
 
 test "dup & dup2" {
lib/std/posix.zig
@@ -86,6 +86,7 @@ pub const MREMAP = system.MREMAP;
 pub const MSF = system.MSF;
 pub const MSG = system.MSG;
 pub const NAME_MAX = system.NAME_MAX;
+pub const NSIG = system.NSIG;
 pub const O = system.O;
 pub const PATH_MAX = system.PATH_MAX;
 pub const POLL = system.POLL;
@@ -129,7 +130,6 @@ pub const dl_phdr_info = system.dl_phdr_info;
 pub const empty_sigset = system.empty_sigset;
 pub const fd_t = system.fd_t;
 pub const file_obj = system.file_obj;
-pub const filled_sigset = system.filled_sigset;
 pub const gid_t = system.gid_t;
 pub const ifreq = system.ifreq;
 pub const ino_t = system.ino_t;
@@ -678,7 +678,7 @@ pub fn abort() noreturn {
         raise(SIG.ABRT) catch {};
 
         // Disable all signal handlers.
-        sigprocmask(SIG.BLOCK, &linux.all_mask, null);
+        sigprocmask(SIG.BLOCK, &linux.filled_sigset, null);
 
         // Only one thread may proceed to the rest of abort().
         if (!builtin.single_threaded) {
@@ -698,7 +698,8 @@ pub fn abort() noreturn {
 
         _ = linux.tkill(linux.gettid(), SIG.ABRT);
 
-        const sigabrtmask: linux.sigset_t = [_]u32{0} ** 31 ++ [_]u32{1 << (SIG.ABRT - 1)};
+        var sigabrtmask = empty_sigset;
+        sigaddset(&sigabrtmask, SIG.ABRT);
         sigprocmask(SIG.UNBLOCK, &sigabrtmask, null);
 
         // Beyond this point should be unreachable.
@@ -723,18 +724,13 @@ pub fn raise(sig: u8) RaiseError!void {
     }
 
     if (native_os == .linux) {
-        // https://git.musl-libc.org/cgit/musl/commit/?id=0bed7e0acfd34e3fb63ca0e4d99b7592571355a9
-        //
-        // Unlike musl, libc-less Zig std does not have any internal signals for implementation purposes, so we
-        // need to block all signals on the assumption that any of them could potentially fork() in a handler.
-        var set: sigset_t = undefined;
-        sigprocmask(SIG.BLOCK, &linux.all_mask, &set);
-
-        const tid = linux.gettid();
-        const rc = linux.tkill(tid, sig);
-
-        // restore signal mask
-        sigprocmask(SIG.SETMASK, &set, null);
+        // Block all signals so a `fork` (from a signal handler) between the gettid() and kill() syscalls
+        // cannot trigger an extra, unexpected, inter-process signal.  Signal paranoia inherited from Musl.
+        const filled = linux.sigfillset();
+        var orig: sigset_t = undefined;
+        sigprocmask(SIG.BLOCK, &linux.filled_sigset, &orig);
+        const rc = linux.tkill(linux.gettid(), sig);
+        sigprocmask(SIG.SETMASK, &orig, null);
 
         switch (errno(rc)) {
             .SUCCESS => return,
@@ -5818,8 +5814,59 @@ pub fn sigaltstack(ss: ?*stack_t, old_ss: ?*stack_t) SigaltstackError!void {
     }
 }
 
+pub fn sigfillset(set: *sigset_t) void {
+    if (builtin.link_libc) {
+        switch (errno(system.sigfillset(set))) {
+            .SUCCESS => return,
+            else => unreachable,
+        }
+    }
+    set.* = system.filled_sigset;
+}
+
+pub fn sigemptyset(set: *sigset_t) void {
+    if (builtin.link_libc) {
+        switch (errno(system.sigemptyset(set))) {
+            .SUCCESS => return,
+            else => unreachable,
+        }
+    }
+    set.* = system.empty_sigset;
+}
+
+pub fn sigaddset(set: *sigset_t, sig: u8) void {
+    if (builtin.link_libc) {
+        switch (errno(system.sigaddset(set, sig))) {
+            .SUCCESS => return,
+            else => unreachable,
+        }
+    }
+    system.sigaddset(set, sig);
+}
+
+pub fn sigdelset(set: *sigset_t, sig: u8) void {
+    if (builtin.link_libc) {
+        switch (errno(system.sigdelset(set, sig))) {
+            .SUCCESS => return,
+            else => unreachable,
+        }
+    }
+    system.sigdelset(set, sig);
+}
+
+pub fn sigismember(set: *const sigset_t, sig: u8) bool {
+    if (builtin.link_libc) {
+        const rc = system.sigismember(set, sig);
+        switch (errno(rc)) {
+            .SUCCESS => return rc == 1,
+            else => unreachable,
+        }
+    }
+    return system.sigismember(set, sig);
+}
+
 /// Examine and change a signal action.
-pub fn sigaction(sig: u6, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) void {
+pub fn sigaction(sig: u8, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) void {
     switch (errno(system.sigaction(sig, act, oact))) {
         .SUCCESS => return,
         // EINVAL means the signal is either invalid or some signal that cannot have its action