a7a02f36
refactor: the pty forks, seals and resizes through server_os
a73x 2026-09-03 15:10
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -130,7 +130,7 @@ const mod_table = [_]ModSpec{ | |||
| 130 | // pty, and an app that links the engine and a client must not either. | 130 | // pty, and an app that links the engine and a client must not either. |
| 131 | .{ .name = "server_os", .path = "src/os/server_os.zig", .link_libc = true }, | 131 | .{ .name = "server_os", .path = "src/os/server_os.zig", .link_libc = true }, |
| 132 | .{ .name = "client_os", .path = "src/os/client_os.zig", .link_libc = true }, | 132 | .{ .name = "client_os", .path = "src/os/client_os.zig", .link_libc = true }, |
| 133 | .{ .name = "pty", .path = "src/server/pty.zig", .link_libc = true }, | 133 | .{ .name = "pty", .path = "src/server/pty.zig", .link_libc = true, .imports = &.{"server_os"} }, |
| 134 | // The QUIC vocabulary both ends share: the one @cImport of the vendored | 134 | // The QUIC vocabulary both ends share: the one @cImport of the vendored |
| 135 | // stack, the key, the wire constants, the egress ring. It has to be ONE | 135 | // stack, the key, the wire constants, the egress ring. It has to be ONE |
| 136 | // module — two @cImport blocks over the same headers are two distinct | 136 | // module — two @cImport blocks over the same headers are two distinct |
src/os/server_os.zig
| Old | New | ||
|---|---|---|---|
| @@ -21,10 +21,106 @@ pub fn getpid() std.posix.pid_t { | |||
| 21 | return impl.getpid(); | 21 | return impl.getpid(); |
| 22 | } | 22 | } |
| 23 | 23 | ||
| 24 | pub const Winsize = std.posix.winsize; | ||
| 25 | pub const ForkedPty = struct { pid: std.posix.pid_t, master: std.posix.fd_t }; | ||
| 26 | |||
| 27 | /// Fork with a fresh pty as the child's controlling terminal, sized before | ||
| 28 | /// the shell's first read so no program sees a 0x0 grid. Returns pid 0 in | ||
| 29 | /// the child, exactly as forkpty(3) does, so the child code that resets | ||
| 30 | /// signals and injects env stays where the fork is visible (pty.zig). | ||
| 31 | pub fn forkPty(ws: Winsize) error{ForkPtyFailed}!ForkedPty { | ||
| 32 | return impl.forkPty(ws); | ||
| 33 | } | ||
| 34 | |||
| 35 | /// A child's bail-out. Never `std.process.exit`: under link_libc that is | ||
| 36 | /// exit(3), which runs atexit and flushes stdio buffers the child inherited | ||
| 37 | /// from the parent — so the parent's pending bytes would be written twice. | ||
| 38 | pub fn exitNow(code: u8) noreturn { | ||
| 39 | impl.exitNow(code); | ||
| 40 | } | ||
| 41 | |||
| 42 | /// The fd barrier: every descriptor at or above `first` is closed in the | ||
| 43 | /// child before exec. CLOEXEC is set fd by fd, and an upgrade clears every | ||
| 44 | /// one and must seal them again — two hand-kept lists that would have to | ||
| 45 | /// agree, or the manifest carrier with the QUIC key bytes rides into the | ||
| 46 | /// shell. This needs no list. | ||
| 47 | pub fn closeFrom(first: std.posix.fd_t) void { | ||
| 48 | impl.closeFrom(first); | ||
| 49 | } | ||
| 50 | |||
| 51 | /// The two line-discipline bits that decide who echoes a keystroke, read | ||
| 52 | /// off the MASTER. Polled — the kernel notifies nobody when a mode changes. | ||
| 53 | pub const PtyMode = struct { icanon: bool, echo: bool }; | ||
| 54 | pub fn ptyMode(master: std.posix.fd_t) std.posix.TermiosGetError!PtyMode { | ||
| 55 | return impl.ptyMode(master); | ||
| 56 | } | ||
| 57 | |||
| 58 | /// Foreground process group of the pty. Equal to the session's child pid | ||
| 59 | /// means no foreground job: the kernel's "command returned" with zero shell | ||
| 60 | /// cooperation, which is `mux a`'s `pgid` mechanism. | ||
| 61 | pub fn ptyFgPgid(master: std.posix.fd_t) error{IoctlFailed}!std.posix.pid_t { | ||
| 62 | return impl.ptyFgPgid(master); | ||
| 63 | } | ||
| 64 | |||
| 65 | /// Resize the pty; the kernel raises SIGWINCH in the session. | ||
| 66 | pub fn setWinsize(master: std.posix.fd_t, ws: Winsize) error{IoctlFailed}!void { | ||
| 67 | return impl.setWinsize(master, ws); | ||
| 68 | } | ||
| 69 | |||
| 24 | test "server_os: the arm compiles and answers for the process it is in" { | 70 | test "server_os: the arm compiles and answers for the process it is in" { |
| 25 | try std.testing.expect(getpid() > 0); | 71 | try std.testing.expect(getpid() > 0); |
| 26 | } | 72 | } |
| 27 | 73 | ||
| 74 | test "server_os.closeFrom: a fd below the floor survives and one above does not" { | ||
| 75 | // pipe(2) sets no CLOEXEC, so a child that did not close would still | ||
| 76 | // hold pipe[1]. Asked through /dev/fd, which both OSes have. | ||
| 77 | const pipe = try std.posix.pipe(); | ||
| 78 | defer std.posix.close(pipe[0]); | ||
| 79 | defer std.posix.close(pipe[1]); | ||
| 80 | var cmd_buf: [96]u8 = undefined; | ||
| 81 | const cmd = try std.fmt.bufPrintZ(&cmd_buf, "test -e /dev/fd/{d} && exit 3; exit 0", .{pipe[1]}); | ||
| 82 | const argv = [_:null]?[*:0]const u8{ "/bin/sh", "-c", cmd.ptr }; | ||
| 83 | const ws: Winsize = .{ .row = 24, .col = 80, .xpixel = 0, .ypixel = 0 }; | ||
| 84 | const f = try forkPty(ws); | ||
| 85 | if (f.pid == 0) { | ||
| 86 | closeFrom(3); | ||
| 87 | std.posix.execveZ(argv[0].?, &argv, std.c.environ) catch {}; | ||
| 88 | exitNow(127); | ||
| 89 | } | ||
| 90 | defer std.posix.close(f.master); | ||
| 91 | const r = std.posix.waitpid(f.pid, 0); | ||
| 92 | try std.testing.expect(std.posix.W.IFEXITED(r.status)); | ||
| 93 | try std.testing.expectEqual(@as(u32, 0), std.posix.W.EXITSTATUS(r.status)); | ||
| 94 | } | ||
| 95 | |||
| 96 | test "server_os.setWinsize then ptyMode: the master answers about the line discipline" { | ||
| 97 | const ws: Winsize = .{ .row = 31, .col = 101, .xpixel = 0, .ypixel = 0 }; | ||
| 98 | const argv = [_:null]?[*:0]const u8{ "/bin/sh", "-c", "stty -echo; sleep 5" }; | ||
| 99 | const f = try forkPty(ws); | ||
| 100 | if (f.pid == 0) { | ||
| 101 | std.posix.execveZ(argv[0].?, &argv, std.c.environ) catch {}; | ||
| 102 | exitNow(127); | ||
| 103 | } | ||
| 104 | defer { | ||
| 105 | std.posix.kill(f.pid, std.posix.SIG.KILL) catch {}; | ||
| 106 | _ = std.posix.waitpid(f.pid, 0); | ||
| 107 | std.posix.close(f.master); | ||
| 108 | } | ||
| 109 | // A fresh pty echoes; the shell turns it off. Polled, because nothing | ||
| 110 | // notifies a mode change. | ||
| 111 | var waited: usize = 0; | ||
| 112 | while (waited < 100) : (waited += 1) { | ||
| 113 | const m = try ptyMode(f.master); | ||
| 114 | if (!m.echo) break; | ||
| 115 | std.Thread.sleep(50 * std.time.ns_per_ms); | ||
| 116 | } | ||
| 117 | try std.testing.expect(!(try ptyMode(f.master)).echo); | ||
| 118 | // The foreground group is the shell itself while `sleep` is its child | ||
| 119 | // in the same group: fgPgid equals the pid forkPty returned. | ||
| 120 | try std.testing.expectEqual(f.pid, try ptyFgPgid(f.master)); | ||
| 121 | try setWinsize(f.master, .{ .row = 10, .col = 40, .xpixel = 0, .ypixel = 0 }); | ||
| 122 | } | ||
| 123 | |||
| 28 | // Forces semantic analysis of every pub decl under `zig build test`, so an | 124 | // Forces semantic analysis of every pub decl under `zig build test`, so an |
| 29 | // unreferenced operation must at least compile for this OS. | 125 | // unreferenced operation must at least compile for this OS. |
| 30 | test { | 126 | test { |
src/os/server_os_linux.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,6 +1,46 @@ | |||
| 1 | //! Linux arm of `server_os`. Spellings only; the contract is in the root. | 1 | //! Linux arm of `server_os`. Spellings only; the contract is in the root. |
| 2 | const std = @import("std"); | 2 | const std = @import("std"); |
| 3 | const root = @import("server_os.zig"); | ||
| 4 | const c = @cImport({ | ||
| 5 | @cInclude("pty.h"); | ||
| 6 | @cInclude("sys/ioctl.h"); | ||
| 7 | }); | ||
| 3 | 8 | ||
| 4 | pub fn getpid() std.posix.pid_t { | 9 | pub fn getpid() std.posix.pid_t { |
| 5 | return std.os.linux.getpid(); | 10 | return std.os.linux.getpid(); |
| 6 | } | 11 | } |
| 12 | |||
| 13 | pub fn forkPty(ws: root.Winsize) error{ForkPtyFailed}!root.ForkedPty { | ||
| 14 | var master: c_int = undefined; | ||
| 15 | var cws: c.struct_winsize = .{ .ws_row = ws.row, .ws_col = ws.col, .ws_xpixel = 0, .ws_ypixel = 0 }; | ||
| 16 | const pid = c.forkpty(&master, null, null, &cws); | ||
| 17 | if (pid < 0) return error.ForkPtyFailed; | ||
| 18 | return .{ .pid = pid, .master = master }; | ||
| 19 | } | ||
| 20 | |||
| 21 | pub fn exitNow(code: u8) noreturn { | ||
| 22 | std.os.linux.exit_group(code); | ||
| 23 | } | ||
| 24 | |||
| 25 | pub fn closeFrom(first: std.posix.fd_t) void { | ||
| 26 | // ENOSYS (pre-5.9 kernel) leaves the CLOEXEC flags to do the work alone. | ||
| 27 | _ = std.os.linux.syscall3(.close_range, @intCast(first), std.math.maxInt(u32), 0); | ||
| 28 | } | ||
| 29 | |||
| 30 | pub fn ptyMode(master: std.posix.fd_t) std.posix.TermiosGetError!root.PtyMode { | ||
| 31 | // On Linux the master shares one termios with the slave, so what the | ||
| 32 | // session did with tcsetattr is one syscall away. | ||
| 33 | const t = try std.posix.tcgetattr(master); | ||
| 34 | return .{ .icanon = t.lflag.ICANON, .echo = t.lflag.ECHO }; | ||
| 35 | } | ||
| 36 | |||
| 37 | pub fn ptyFgPgid(master: std.posix.fd_t) error{IoctlFailed}!std.posix.pid_t { | ||
| 38 | var pgid: c.pid_t = 0; | ||
| 39 | if (c.ioctl(master, c.TIOCGPGRP, &pgid) < 0) return error.IoctlFailed; | ||
| 40 | return @intCast(pgid); | ||
| 41 | } | ||
| 42 | |||
| 43 | pub fn setWinsize(master: std.posix.fd_t, ws: root.Winsize) error{IoctlFailed}!void { | ||
| 44 | var cws: c.struct_winsize = .{ .ws_row = ws.row, .ws_col = ws.col, .ws_xpixel = 0, .ws_ypixel = 0 }; | ||
| 45 | if (c.ioctl(master, c.TIOCSWINSZ, &cws) < 0) return error.IoctlFailed; | ||
| 46 | } | ||
src/server/pty.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,12 +1,17 @@ | |||
| 1 | //! PTY lifecycle for a session: forkpty with the user's shell — or with any | 1 | //! PTY lifecycle for a session: a pty forked through `server_os.forkPty` with |
| 2 | //! argv, which is how the e2e fixture drives a real client — blocking master | 2 | //! the user's shell — or with any argv, which is how the e2e fixture drives a |
| 3 | //! fd (the daemon's poll loop drives readiness), exit detection. | 3 | //! real client — blocking master fd (the daemon's poll loop drives readiness), |
| 4 | //! exit detection. | ||
| 4 | const std = @import("std"); | 5 | const std = @import("std"); |
| 5 | const c = @cImport({ | 6 | const server_os = @import("server_os"); |
| 6 | @cInclude("pty.h"); | 7 | |
| 7 | @cInclude("stdlib.h"); | 8 | // Declared rather than @cInclude'd, and not `server_os`'s business either: |
| 8 | @cInclude("sys/ioctl.h"); | 9 | // setenv(3) and unsetenv(3) are POSIX, spelled the same on every OS mux runs |
| 9 | }); | 10 | // on, so there is nothing for a platform arm to choose between. Two externs |
| 11 | // also keep this file's whole C surface visible on two lines, instead of a | ||
| 12 | // header's entire namespace. | ||
| 13 | extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int; | ||
| 14 | extern "c" fn unsetenv(name: [*:0]const u8) c_int; | ||
| 10 | 15 | ||
| 11 | pub const Pty = struct { | 16 | pub const Pty = struct { |
| 12 | master: std.posix.fd_t, | 17 | master: std.posix.fd_t, |
| @@ -38,34 +43,29 @@ pub const Pty = struct { | |||
| 38 | /// on the far side of a pty is done here, so `spawn` and the e2e fixture | 43 | /// on the far side of a pty is done here, so `spawn` and the e2e fixture |
| 39 | /// cannot drift apart in what they hand the child. | 44 | /// cannot drift apart in what they hand the child. |
| 40 | pub fn spawnArgv(opts: SpawnArgvOptions) !Pty { | 45 | pub fn spawnArgv(opts: SpawnArgvOptions) !Pty { |
| 41 | var master: c_int = undefined; | 46 | const ws: server_os.Winsize = .{ .row = opts.rows, .col = opts.cols, .xpixel = 0, .ypixel = 0 }; |
| 42 | var ws: c.struct_winsize = .{ | ||
| 43 | .ws_row = opts.rows, | ||
| 44 | .ws_col = opts.cols, | ||
| 45 | .ws_xpixel = 0, | ||
| 46 | .ws_ypixel = 0, | ||
| 47 | }; | ||
| 48 | 47 | ||
| 49 | // Diagnosed in the parent, where it can still be an error: an empty | 48 | // Diagnosed in the parent, where it can still be an error: an empty |
| 50 | // argv exec'd in the child is indistinguishable from a real exec | 49 | // argv exec'd in the child is indistinguishable from a real exec |
| 51 | // failure, and costs a fork to say so. | 50 | // failure, and costs a fork to say so. |
| 52 | if (opts.argv[0] == null) return error.EmptyArgv; | 51 | if (opts.argv[0] == null) return error.EmptyArgv; |
| 53 | 52 | ||
| 54 | const pid = c.forkpty(&master, null, null, &ws); | 53 | const f = try server_os.forkPty(ws); |
| 55 | if (pid < 0) return error.ForkPtyFailed; | 54 | const pid = f.pid; |
| 55 | const master = f.master; | ||
| 56 | 56 | ||
| 57 | if (pid == 0) { | 57 | if (pid == 0) { |
| 58 | // Child. xterm-256color: ghostty-vt understands more, but this | 58 | // Child. xterm-256color: ghostty-vt understands more, but this |
| 59 | // terminfo exists everywhere the shell will look. | 59 | // terminfo exists everywhere the shell will look. |
| 60 | _ = c.setenv("TERM", "xterm-256color", 1); | 60 | _ = setenv("TERM", "xterm-256color", 1); |
| 61 | // Overwrite (1), and a CONTRACT rather than a detail: this is a loop | 61 | // Overwrite (1), and a CONTRACT rather than a detail: this is a loop |
| 62 | // over an ordered slice, so a LATER pair beats an earlier one for the | 62 | // over an ordered slice, so a LATER pair beats an earlier one for the |
| 63 | // same key. That is what lets `extra_env` override a variable the | 63 | // same key. That is what lets `extra_env` override a variable the |
| 64 | // shell-integration injection set, and a reorder would invert it. | 64 | // shell-integration injection set, and a reorder would invert it. |
| 65 | for (opts.env) |kv| _ = if (kv.value) |v| | 65 | for (opts.env) |kv| _ = if (kv.value) |v| |
| 66 | c.setenv(kv.key.ptr, v.ptr, 1) | 66 | setenv(kv.key.ptr, v.ptr, 1) |
| 67 | else | 67 | else |
| 68 | c.unsetenv(kv.key.ptr); | 68 | unsetenv(kv.key.ptr); |
| 69 | 69 | ||
| 70 | // Ctrl-C must work in the session, and without this it does not: a | 70 | // Ctrl-C must work in the session, and without this it does not: a |
| 71 | // non-interactive shell sets SIGINT to SIG_IGN for anything it | 71 | // non-interactive shell sets SIGINT to SIG_IGN for anything it |
| @@ -85,24 +85,19 @@ pub const Pty = struct { | |||
| 85 | // session shell. Resetting here makes it order-independent. | 85 | // session shell. Resetting here makes it order-independent. |
| 86 | std.posix.sigaction(std.posix.SIG.PIPE, &dfl, null); | 86 | std.posix.sigaction(std.posix.SIG.PIPE, &dfl, null); |
| 87 | 87 | ||
| 88 | // exit_group, never std.process.exit — see spawn.zig's fork child | 88 | // `exitNow`, never `std.process.exit` — see `server_os.exitNow` |
| 89 | // for the full reason: under link_libc that is exit(3), which | 89 | // for why. |
| 90 | // flushes stdio buffers inherited from the parent. | ||
| 91 | if (opts.stderr_fd) |fd| { | 90 | if (opts.stderr_fd) |fd| { |
| 92 | std.posix.dup2(fd, 2) catch std.os.linux.exit_group(126); | 91 | std.posix.dup2(fd, 2) catch server_os.exitNow(126); |
| 93 | // The dup left a spare copy at the caller's fd number and | 92 | // The dup left a spare copy at the caller's fd number and |
| 94 | // `pipe()` sets no CLOEXEC, so it would ride through exec into | 93 | // `pipe()` sets no CLOEXEC, so it would ride through exec into |
| 95 | // everything the client spawns. One handle, so the write end dies | 94 | // everything the client spawns. One handle, so the write end dies |
| 96 | // with the child's stderr and not later. | 95 | // with the child's stderr and not later. |
| 97 | if (fd > 2) std.posix.close(fd); | 96 | if (fd > 2) std.posix.close(fd); |
| 98 | } | 97 | } |
| 99 | // The barrier that needs no list: CLOEXEC is set fd by fd, and an | 98 | server_os.closeFrom(3); |
| 100 | // upgrade clears every one and must seal them again — two hand-kept | ||
| 101 | // lists that have to agree, or a key-carrying memfd rides into the | ||
| 102 | // shell. ENOSYS leaves the flags to do the work alone. | ||
| 103 | _ = std.os.linux.syscall3(.close_range, 3, std.math.maxInt(u32), 0); | ||
| 104 | std.posix.execveZ(opts.argv[0].?, opts.argv, std.c.environ) catch {}; | 99 | std.posix.execveZ(opts.argv[0].?, opts.argv, std.c.environ) catch {}; |
| 105 | std.os.linux.exit_group(127); | 100 | server_os.exitNow(127); |
| 106 | } | 101 | } |
| 107 | 102 | ||
| 108 | // Parent. The master is THIS session's private handle and must never | 103 | // Parent. The master is THIS session's private handle and must never |
| @@ -127,34 +122,21 @@ pub const Pty = struct { | |||
| 127 | return .{ .master = master, .child = pid }; | 122 | return .{ .master = master, .child = pid }; |
| 128 | } | 123 | } |
| 129 | 124 | ||
| 130 | /// The two line-discipline bits that decide who echoes a keystroke. Read off | 125 | pub const Mode = server_os.PtyMode; |
| 131 | /// the MASTER, which on Linux shares one termios with the slave, so what the | ||
| 132 | /// session did with tcsetattr is one syscall away. Polled, which is the only | ||
| 133 | /// option — the kernel notifies nobody when a mode changes. | ||
| 134 | pub const Mode = struct { icanon: bool, echo: bool }; | ||
| 135 | 126 | ||
| 136 | pub fn mode(self: *const Pty) !Mode { | 127 | pub fn mode(self: *const Pty) !Mode { |
| 137 | const t = try std.posix.tcgetattr(self.master); | 128 | return server_os.ptyMode(self.master); |
| 138 | return .{ .icanon = t.lflag.ICANON, .echo = t.lflag.ECHO }; | ||
| 139 | } | 129 | } |
| 140 | 130 | ||
| 141 | /// Equal to `child` means no foreground job: the kernel's "command | 131 | /// Equal to `child` means no foreground job: the kernel's "command |
| 142 | /// returned", with zero shell cooperation. No exit code and no output | 132 | /// returned", with zero shell cooperation. No exit code and no output |
| 143 | /// span; marks are for that. | 133 | /// span; marks are for that. |
| 144 | pub fn fgPgid(self: *const Pty) !std.posix.pid_t { | 134 | pub fn fgPgid(self: *const Pty) !std.posix.pid_t { |
| 145 | var pgid: c.pid_t = 0; | 135 | return server_os.ptyFgPgid(self.master); |
| 146 | if (c.ioctl(self.master, c.TIOCGPGRP, &pgid) < 0) return error.IoctlFailed; | ||
| 147 | return @intCast(pgid); | ||
| 148 | } | 136 | } |
| 149 | 137 | ||
| 150 | pub fn resize(self: *Pty, cols: u16, rows: u16) !void { | 138 | pub fn resize(self: *Pty, cols: u16, rows: u16) !void { |
| 151 | var ws: c.struct_winsize = .{ | 139 | return server_os.setWinsize(self.master, .{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 }); |
| 152 | .ws_row = rows, | ||
| 153 | .ws_col = cols, | ||
| 154 | .ws_xpixel = 0, | ||
| 155 | .ws_ypixel = 0, | ||
| 156 | }; | ||
| 157 | if (c.ioctl(self.master, c.TIOCSWINSZ, &ws) < 0) return error.IoctlFailed; | ||
| 158 | } | 140 | } |
| 159 | 141 | ||
| 160 | // Build a Pty from an fd and pid that already belong to this process. The | 142 | // Build a Pty from an fd and pid that already belong to this process. The |
| @@ -367,13 +349,16 @@ test "Pty: resize is visible via TIOCGWINSZ" { | |||
| 367 | 349 | ||
| 368 | try pty.resize(120, 40); | 350 | try pty.resize(120, 40); |
| 369 | 351 | ||
| 370 | var ws: c.struct_winsize = undefined; | 352 | // Asked of the kernel, not of `server_os`: the ioctl that reads the size |
| 353 | // back has to be a different call from the one that set it, or the test | ||
| 354 | // grades the platform arm against itself. | ||
| 355 | var ws: std.posix.winsize = undefined; | ||
| 371 | try std.testing.expectEqual( | 356 | try std.testing.expectEqual( |
| 372 | @as(c_int, 0), | 357 | @as(c_int, 0), |
| 373 | c.ioctl(pty.master, c.TIOCGWINSZ, &ws), | 358 | std.c.ioctl(pty.master, @intCast(std.c.T.IOCGWINSZ), &ws), |
| 374 | ); | 359 | ); |
| 375 | try std.testing.expectEqual(@as(c_ushort, 120), ws.ws_col); | 360 | try std.testing.expectEqual(@as(u16, 120), ws.col); |
| 376 | try std.testing.expectEqual(@as(c_ushort, 40), ws.ws_row); | 361 | try std.testing.expectEqual(@as(u16, 40), ws.row); |
| 377 | } | 362 | } |
| 378 | 363 | ||
| 379 | test "Pty: mode reads the line discipline off the master" { | 364 | test "Pty: mode reads the line discipline off the master" { |
| @@ -482,12 +467,13 @@ test "Pty: spawnArgv runs an argv and propagates exit status" { | |||
| 482 | 467 | ||
| 483 | test "Pty: a daemon fd without CLOEXEC still does not reach the shell" { | 468 | test "Pty: a daemon fd without CLOEXEC still does not reach the shell" { |
| 484 | // pipe(2) sets no CLOEXEC — exactly the state an upgrade exec leaves | 469 | // pipe(2) sets no CLOEXEC — exactly the state an upgrade exec leaves |
| 485 | // the adopted fds in. The child looks for its own copy. | 470 | // the adopted fds in. The child looks for its own copy: through /dev/fd, |
| 471 | // which every OS mux runs on has. | ||
| 486 | const pipe = try std.posix.pipe(); | 472 | const pipe = try std.posix.pipe(); |
| 487 | defer std.posix.close(pipe[0]); | 473 | defer std.posix.close(pipe[0]); |
| 488 | defer std.posix.close(pipe[1]); | 474 | defer std.posix.close(pipe[1]); |
| 489 | var cmd_buf: [96]u8 = undefined; | 475 | var cmd_buf: [96]u8 = undefined; |
| 490 | const cmd = try std.fmt.bufPrintZ(&cmd_buf, "test -e /proc/self/fd/{d} && exit 3; exit 0", .{pipe[1]}); | 476 | const cmd = try std.fmt.bufPrintZ(&cmd_buf, "test -e /dev/fd/{d} && exit 3; exit 0", .{pipe[1]}); |
| 491 | var argv = [_:null]?[*:0]const u8{ "/bin/sh", "-c", cmd.ptr }; | 477 | var argv = [_:null]?[*:0]const u8{ "/bin/sh", "-c", cmd.ptr }; |
| 492 | var pty = try Pty.spawnArgv(.{ .cols = 80, .rows = 24, .argv = &argv }); | 478 | var pty = try Pty.spawnArgv(.{ .cols = 80, .rows = 24, .argv = &argv }); |
| 493 | defer pty.deinit(); | 479 | defer pty.deinit(); |