summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 13:39:12 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit164da9fc0a7ab3c7ed39ea0a2dd6bbff3c36cdaf (patch)
treebba5867e52764c5d2f891b8e6861dfdeec068063 /src
parentfea3f2c5abf14aada5d9187b82985e51eb5b7a98 (diff)
downloadpardes-164da9fc0a7ab3c7ed39ea0a2dd6bbff3c36cdaf.tar.gz
pardes-164da9fc0a7ab3c7ed39ea0a2dd6bbff3c36cdaf.zip
Every child starts with its signals at their defaults and none blocked
Shells, command panes, language servers, the link opener and the v9fs mount inherited the editor's signal mask (the tty's blocked SIGWINCH) and anything it ignored (a SIGHUP nohup ignored), since only handlers reset at exec. Each fork now resets every disposition and clears the mask before its exec (resetChildSignals); std's spawn for selection pipes runs with the mask cleared across it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/host_io.zig43
-rw-r--r--src/linux/v9fs.zig10
-rw-r--r--src/look.zig1
-rw-r--r--src/lsp/lsp_client.zig1
-rw-r--r--src/selection_pipe.zig8
5 files changed, 60 insertions, 3 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index 9d4b27c2..bbd664a9 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -1075,6 +1075,45 @@ pub const Shell = struct {
}
};
+/// In a child between fork and exec: every signal back to its default and
+/// none blocked, whatever this process ignores or blocks (the tty's SIGWINCH,
+/// a SIGHUP nohup ignored). A handler resets at exec by itself; an ignored
+/// signal and the mask do not. Async-signal-safe: sigaction and sigprocmask.
+pub fn resetChildSignals() void {
+ const default: posix.Sigaction = .{ .handler = .{ .handler = posix.SIG.DFL }, .mask = posix.sigemptyset(), .flags = 0 };
+ var sig: u8 = 1;
+ while (sig < 65) : (sig += 1) {
+ if (sig == @intFromEnum(posix.SIG.KILL) or sig == @intFromEnum(posix.SIG.STOP)) continue;
+ _ = std.c.sigaction(@enumFromInt(sig), &default, null);
+ }
+ const none = posix.sigemptyset();
+ posix.sigprocmask(posix.SIG.SETMASK, &none, null);
+}
+
+test "a child starts with every signal at its default and none blocked" {
+ var blocked = posix.sigemptyset();
+ posix.sigaddset(&blocked, posix.SIG.WINCH);
+ posix.sigprocmask(posix.SIG.BLOCK, &blocked, null);
+ defer posix.sigprocmask(posix.SIG.UNBLOCK, &blocked, null);
+ const ignore: posix.Sigaction = .{ .handler = .{ .handler = posix.SIG.IGN }, .mask = posix.sigemptyset(), .flags = 0 };
+ var was: posix.Sigaction = undefined;
+ posix.sigaction(posix.SIG.HUP, &ignore, &was);
+ defer posix.sigaction(posix.SIG.HUP, &was, null);
+ const pid = libc.fork();
+ if (pid == 0) {
+ resetChildSignals();
+ var now: posix.Sigaction = undefined;
+ posix.sigaction(posix.SIG.HUP, null, &now);
+ var mask = posix.sigemptyset();
+ posix.sigprocmask(posix.SIG.BLOCK, null, &mask);
+ const ok = now.handler.handler == posix.SIG.DFL and !posix.sigismember(&mask, posix.SIG.WINCH);
+ libc._exit(if (ok) 0 else 1);
+ }
+ var status: c_int = 0;
+ _ = libc.waitpid(pid, &status, 0);
+ try std.testing.expectEqual(@as(c_int, 0), status);
+}
+
extern "c" fn forkpty(amaster: *c_int, name: ?[*:0]u8, termp: ?*const anyopaque, winp: ?*const posix.winsize) c_int;
extern "c" fn execv(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;
extern "c" fn execve(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8) c_int;
@@ -1278,9 +1317,7 @@ pub fn forkShell(
return error.ForkFailed;
}
if (pid == 0) {
- var set = posix.sigemptyset();
- posix.sigaddset(&set, posix.SIG.WINCH);
- posix.sigprocmask(posix.SIG.UNBLOCK, &set, null);
+ resetChildSignals();
if (cwd_z) |path| if (chdir(path.ptr) != 0) {
const why = [_]u8{ 'c', @truncate(@intFromEnum(libc.errno(@as(c_int, -1)))) };
_ = libc.write(told[1], &why, why.len);
diff --git a/src/linux/v9fs.zig b/src/linux/v9fs.zig
index 4e18660d..5e031bfe 100644
--- a/src/linux/v9fs.zig
+++ b/src/linux/v9fs.zig
@@ -118,6 +118,16 @@ fn launch(arena: std.mem.Allocator, args: []const [:0]const u8) !u8 {
const pid = fork();
if (pid < 0) return error.ForkFailed;
if (pid == 0) {
+ // host_io's resetChildSignals, here in its own module: every signal
+ // back to its default and none blocked before the exec.
+ const default: std.posix.Sigaction = .{ .handler = .{ .handler = std.posix.SIG.DFL }, .mask = std.posix.sigemptyset(), .flags = 0 };
+ var sig: u8 = 1;
+ while (sig < 65) : (sig += 1) {
+ if (sig == @intFromEnum(std.posix.SIG.KILL) or sig == @intFromEnum(std.posix.SIG.STOP)) continue;
+ _ = std.c.sigaction(@enumFromInt(sig), &default, null);
+ }
+ const none = std.posix.sigemptyset();
+ std.posix.sigprocmask(std.posix.SIG.SETMASK, &none, null);
_ = execvp("sudo", argv.ptr);
_exit(127);
}
diff --git a/src/look.zig b/src/look.zig
index 5cf26240..4e948fad 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -49,6 +49,7 @@ pub fn openLink(url: []const u8) void {
if (pid < 0) return;
if (pid == 0) {
if (fork() == 0) {
+ @import("host_io.zig").resetChildSignals();
const argv: [3:null]?[*:0]const u8 = .{ opener, url_z.ptr, null };
_ = execv(opener, &argv);
}
diff --git a/src/lsp/lsp_client.zig b/src/lsp/lsp_client.zig
index 55df1f91..91ea67d6 100644
--- a/src/lsp/lsp_client.zig
+++ b/src/lsp/lsp_client.zig
@@ -1148,6 +1148,7 @@ fn ensure(c: *Conn, si: usize, arena: std.mem.Allocator, req: lsp.Req, tr: *Trac
// signal-safe calls, no allocation, no locks. (Same rule as tty.zig's
// forkShell.)
_ = setsid(); // Ctrl-C in pardes's terminal is not the server's business
+ @import("../host_io.zig").resetChildSignals();
_ = libc.dup2(sv[1], 0);
_ = libc.dup2(sv[1], 1);
const devnull = libc.open("/dev/null", .{ .ACCMODE = .WRONLY });
diff --git a/src/selection_pipe.zig b/src/selection_pipe.zig
index e1a42962..44cc1709 100644
--- a/src/selection_pipe.zig
+++ b/src/selection_pipe.zig
@@ -202,6 +202,14 @@ pub fn runOne(
if (std.mem.indexOfScalar(u8, command, 0) != null or
std.mem.indexOfScalar(u8, cwd, 0) != null) return fail.k(.spawn);
+ // std's spawn runs no code in the child: this thread's mask, which the
+ // child inherits, is cleared across the fork (only the tty's SIGWINCH is
+ // ever blocked, and its default is to be ignored). A handler resets at
+ // exec by itself, and nothing here is ignored.
+ const none = std.posix.sigemptyset();
+ var kept: std.posix.sigset_t = undefined;
+ std.posix.sigprocmask(std.posix.SIG.SETMASK, &none, &kept);
+ defer std.posix.sigprocmask(std.posix.SIG.SETMASK, &kept, null);
var child = std.process.spawn(io, .{
.argv = &.{ "/bin/sh", "-c", command },
.cwd = if (cwd.len == 0) .inherit else .{ .path = cwd },