- commit
- 76cec40
- parent
- 11a12cc
- author
- Ian Tay
- date
- 2026-03-08 12:55:37 -0400 EDT
fix(pty): isolate forked child from parent code path and heap-alloc argv Two issues in spawnPty's child (pid==0) branch: 1. `try` on allocPrint/bufPrintZ could propagate an error past the if-block, causing the forked child to fall through to the parent code path — running a second daemon on the same socket, or hitting errdefers that delete the parent's socket file. The bufPrintZ case is triggerable via a long SHELL basename. 2. The fixed-size argv_buf[64] overflowed with >63 CLI arguments. Both are fixed by extracting child setup into execChild() which returns !noreturn (exec or error), with the caller exiting on any error. The argv array is now heap-allocated to the exact size needed.
1 files changed,
+50,
-25
+50,
-25
1@@ -294,6 +294,48 @@ const Daemon = struct {
2 return false;
3 }
4
5+ /// Runs in the forked child. Either execs or returns an error (caller
6+ /// must exit on error -- returning would fall through to parent code).
7+ fn execChild(self: *Daemon) !noreturn {
8+ const alloc = std.heap.c_allocator;
9+
10+ // main() set SIGPIPE to SIG_IGN, which (unlike handlers) survives
11+ // exec. Restore the default so the shell and its children behave
12+ // normally (e.g. `yes | head` should exit 141 via SIGPIPE).
13+ const dfl: posix.Sigaction = .{
14+ .handler = .{ .handler = posix.SIG.DFL },
15+ .mask = posix.sigemptyset(),
16+ .flags = 0,
17+ };
18+ posix.sigaction(posix.SIG.PIPE, &dfl, null);
19+
20+ const session_env = try std.fmt.allocPrintSentinel(
21+ alloc,
22+ "ZMX_SESSION={s}",
23+ .{self.session_name},
24+ 0,
25+ );
26+ _ = cross.c.putenv(session_env.ptr);
27+
28+ if (self.command) |cmd_args| {
29+ const argv = try alloc.allocSentinel(?[*:0]const u8, cmd_args.len, null);
30+ for (cmd_args, 0..) |arg, i| {
31+ argv[i] = try alloc.dupeZ(u8, arg);
32+ }
33+ const err = std.posix.execvpeZ(argv[0].?, argv.ptr, std.c.environ);
34+ std.log.err("execvpe failed: cmd={s} err={s}", .{ cmd_args[0], @errorName(err) });
35+ std.posix.exit(1);
36+ }
37+
38+ const shell = util.detectShell();
39+ // Use "-shellname" as argv[0] to signal login shell (traditional method)
40+ const login_shell = try std.fmt.allocPrintSentinel(alloc, "-{s}", .{std.fs.path.basename(shell)}, 0);
41+ const argv = [_:null]?[*:0]const u8{ login_shell, null };
42+ const err = std.posix.execveZ(shell, &argv, std.c.environ);
43+ std.log.err("execve failed: err={s}", .{@errorName(err)});
44+ std.posix.exit(1);
45+ }
46+
47 fn spawnPty(self: *Daemon) !c_int {
48 const size = ipc.getTerminalSize(posix.STDOUT_FILENO);
49 var ws: cross.c.struct_winsize = .{
50@@ -310,32 +352,15 @@ const Daemon = struct {
51 }
52
53 if (pid == 0) { // child pid code path
54- const session_env = try std.fmt.allocPrint(self.alloc, "ZMX_SESSION={s}\x00", .{self.session_name});
55- _ = cross.c.putenv(@ptrCast(session_env.ptr));
56-
57- if (self.command) |cmd_args| {
58- const alloc = std.heap.c_allocator;
59- var argv_buf: [64:null]?[*:0]const u8 = undefined;
60- for (cmd_args, 0..) |arg, i| {
61- argv_buf[i] = alloc.dupeZ(u8, arg) catch {
62- std.posix.exit(1);
63- };
64- }
65- argv_buf[cmd_args.len] = null;
66- const argv: [*:null]const ?[*:0]const u8 = &argv_buf;
67- const err = std.posix.execvpeZ(argv_buf[0].?, argv, std.c.environ);
68- std.log.err("execvpe failed: cmd={s} err={s}", .{ cmd_args[0], @errorName(err) });
69- std.posix.exit(1);
70- } else {
71- const shell = util.detectShell();
72- // Use "-shellname" as argv[0] to signal login shell (traditional method)
73- var buf: [64]u8 = undefined;
74- const login_shell = try std.fmt.bufPrintZ(&buf, "-{s}", .{std.fs.path.basename(shell)});
75- const argv = [_:null]?[*:0]const u8{ login_shell, null };
76- const err = std.posix.execveZ(shell, &argv, std.c.environ);
77- std.log.err("execve failed: err={s}", .{@errorName(err)});
78+ // In the forked child, ANY error must exit rather than propagate:
79+ // a returned error falls through to the parent code path below,
80+ // running a second daemon on the same socket (or worse, hitting
81+ // errdefers that delete the parent's socket file).
82+ execChild(self) catch |err| {
83+ std.log.err("child setup failed: {s}", .{@errorName(err)});
84 std.posix.exit(1);
85- }
86+ };
87+ unreachable; // execChild either execs or exits, never returns ok
88 }
89 // master pid code path
90 self.pid = pid;