94838e6a
refactor: the upgrade manifest rides server_os.anonFd
a73x 2026-09-03 15:10
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -205,7 +205,7 @@ const mod_table = [_]ModSpec{ | |||
| 205 | // daemon itself when nobody handed it a --key — and for the shim | 205 | // daemon itself when nobody handed it a --key — and for the shim |
| 206 | // directory shell integration writes under the same 0700 policy. | 206 | // directory shell integration writes under the same 0700 policy. |
| 207 | // `pty` stays a row of its own: the ptyclient fixture consumes it. | 207 | // `pty` stays a row of its own: the ptyclient fixture consumes it. |
| 208 | .{ .name = "daemon", .path = "src/server/server.zig", .link_libc = true, .imports = &.{ "term", "pty", "sockpath", "serve", "quic", "xdg", "proxy" }, .test_imports = &.{ "testtmp", "dial", "link" }, .quic_tests = true }, | 208 | .{ .name = "daemon", .path = "src/server/server.zig", .link_libc = true, .imports = &.{ "term", "pty", "sockpath", "serve", "quic", "xdg", "proxy", "server_os" }, .test_imports = &.{ "testtmp", "dial", "link" }, .quic_tests = true }, |
| 209 | // The agent-facing client. It speaks frames and owns no terminal, which | 209 | // The agent-facing client. It speaks frames and owns no terminal, which |
| 210 | // is the whole point — it attaches at 0x0 and never claims the grid. | 210 | // is the whole point — it attaches at 0x0 and never claims the grid. |
| 211 | // The transport modules are the CLI client's, minus everything that | 211 | // The transport modules are the CLI client's, minus everything that |
src/cli/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -430,7 +430,7 @@ fn rollbackEnvp(alloc: std.mem.Allocator) ![*:null]const ?[*:0]const u8 { | |||
| 430 | 430 | ||
| 431 | /// Adopt a daemon from the manifest descriptor supplied by an upgrade exec. | 431 | /// Adopt a daemon from the manifest descriptor supplied by an upgrade exec. |
| 432 | /// The process keeps its pid, children, and descriptors. Reading directly from | 432 | /// The process keeps its pid, children, and descriptors. Reading directly from |
| 433 | /// the anonymous memfd also keeps the embedded QUIC key off disk. | 433 | /// the carrier, which no path names, also keeps the embedded QUIC key off disk. |
| 434 | fn resumeRun(alloc: std.mem.Allocator, o: DaemonArguments, resume_fd: std.posix.fd_t) !u8 { | 434 | fn resumeRun(alloc: std.mem.Allocator, o: DaemonArguments, resume_fd: std.posix.fd_t) !u8 { |
| 435 | // The writer left the offset at the end of what it wrote, and a child | 435 | // The writer left the offset at the end of what it wrote, and a child |
| 436 | // shares the file description with it, so the rewind is ours to do. | 436 | // shares the file description with it, so the rewind is ours to do. |
| @@ -1828,8 +1828,8 @@ test "parseArgs: upgrade is a command, and same-version is a flag it takes" { | |||
| 1828 | test "resumeRun: --check adopts nothing, so --resume-fail-at has nothing to abort" { | 1828 | test "resumeRun: --check adopts nothing, so --resume-fail-at has nothing to abort" { |
| 1829 | const alloc = std.testing.allocator; | 1829 | const alloc = std.testing.allocator; |
| 1830 | 1830 | ||
| 1831 | const memfd = try std.posix.memfd_create("mux-resume-check-test", 0); | 1831 | const carrier = try server_os.anonFd("mux-resume-check-test"); |
| 1832 | defer std.posix.close(memfd); | 1832 | defer std.posix.close(carrier); |
| 1833 | 1833 | ||
| 1834 | var buf: std.ArrayList(u8) = .empty; | 1834 | var buf: std.ArrayList(u8) = .empty; |
| 1835 | defer buf.deinit(alloc); | 1835 | defer buf.deinit(alloc); |
| @@ -1848,15 +1848,15 @@ test "resumeRun: --check adopts nothing, so --resume-fail-at has nothing to abor | |||
| 1848 | .quic = .{}, | 1848 | .quic = .{}, |
| 1849 | .counters = .{}, | 1849 | .counters = .{}, |
| 1850 | }, &.{}); | 1850 | }, &.{}); |
| 1851 | var file = std.fs.File{ .handle = memfd }; | 1851 | var file = std.fs.File{ .handle = carrier }; |
| 1852 | try file.writeAll(buf.items); | 1852 | try file.writeAll(buf.items); |
| 1853 | 1853 | ||
| 1854 | const code = try resumeRun(alloc, .{ | 1854 | const code = try resumeRun(alloc, .{ |
| 1855 | ._cmd = .start, | 1855 | ._cmd = .start, |
| 1856 | .check = true, | 1856 | .check = true, |
| 1857 | .resume_fd = memfd, | 1857 | .resume_fd = carrier, |
| 1858 | .resume_fail_at = "daemon", | 1858 | .resume_fail_at = "daemon", |
| 1859 | }, memfd); | 1859 | }, carrier); |
| 1860 | try std.testing.expectEqual(@as(u8, 0), code); | 1860 | try std.testing.expectEqual(@as(u8, 0), code); |
| 1861 | } | 1861 | } |
| 1862 | 1862 | ||
src/os/server_os.zig
| Old | New | ||
|---|---|---|---|
| @@ -96,6 +96,15 @@ pub fn setWinsize(master: std.posix.fd_t, ws: Winsize) error{IoctlFailed}!void { | |||
| 96 | return impl.setWinsize(master, ws); | 96 | return impl.setWinsize(master, ws); |
| 97 | } | 97 | } |
| 98 | 98 | ||
| 99 | /// The upgrade manifest's carrier across `mux d upgrade`'s exec: an fd that | ||
| 100 | /// no path names once this returns, readable only by this uid, and NOT | ||
| 101 | /// CLOEXEC because the candidate must inherit it. It carries the QUIC arm's | ||
| 102 | /// raw key bytes, which is why "no path" is the property and not a nicety — | ||
| 103 | /// and why `closeFrom` seals it away from every session shell. | ||
| 104 | pub fn anonFd(name: [*:0]const u8) error{CarrierFailed}!std.posix.fd_t { | ||
| 105 | return impl.anonFd(name); | ||
| 106 | } | ||
| 107 | |||
| 99 | test "server_os: the arm compiles and answers for the process it is in" { | 108 | test "server_os: the arm compiles and answers for the process it is in" { |
| 100 | try std.testing.expect(getpid() > 0); | 109 | try std.testing.expect(getpid() > 0); |
| 101 | } | 110 | } |
| @@ -185,6 +194,21 @@ test "server_os.forkDetached: the child is a session leader writing to the fd it | |||
| 185 | try std.testing.expectEqual(pid, try std.fmt.parseInt(std.posix.pid_t, shpid, 10)); | 194 | try std.testing.expectEqual(pid, try std.fmt.parseInt(std.posix.pid_t, shpid, 10)); |
| 186 | } | 195 | } |
| 187 | 196 | ||
| 197 | test "server_os.anonFd: no path names it, and it is not CLOEXEC" { | ||
| 198 | const fd = try anonFd("mux-test-carrier"); | ||
| 199 | defer std.posix.close(fd); | ||
| 200 | const st = try std.posix.fstat(fd); | ||
| 201 | try std.testing.expectEqual(@as(@TypeOf(st.nlink), 0), st.nlink); | ||
| 202 | const flags = try std.posix.fcntl(fd, std.posix.F.GETFD, 0); | ||
| 203 | try std.testing.expectEqual(@as(usize, 0), flags & std.posix.FD_CLOEXEC); | ||
| 204 | try std.posix.lseek_SET(fd, 0); | ||
| 205 | _ = try std.posix.write(fd, "abc"); | ||
| 206 | try std.posix.lseek_SET(fd, 0); | ||
| 207 | var buf: [3]u8 = undefined; | ||
| 208 | try std.testing.expectEqual(@as(usize, 3), try std.posix.read(fd, &buf)); | ||
| 209 | try std.testing.expectEqualStrings("abc", &buf); | ||
| 210 | } | ||
| 211 | |||
| 188 | // Forces semantic analysis of every pub decl under `zig build test`, so an | 212 | // Forces semantic analysis of every pub decl under `zig build test`, so an |
| 189 | // unreferenced operation must at least compile for this OS. | 213 | // unreferenced operation must at least compile for this OS. |
| 190 | test { | 214 | test { |
src/os/server_os_linux.zig
| Old | New | ||
|---|---|---|---|
| @@ -67,3 +67,7 @@ pub fn setWinsize(master: std.posix.fd_t, ws: root.Winsize) error{IoctlFailed}!v | |||
| 67 | var cws: c.struct_winsize = .{ .ws_row = ws.row, .ws_col = ws.col, .ws_xpixel = 0, .ws_ypixel = 0 }; | 67 | var cws: c.struct_winsize = .{ .ws_row = ws.row, .ws_col = ws.col, .ws_xpixel = 0, .ws_ypixel = 0 }; |
| 68 | if (c.ioctl(master, c.TIOCSWINSZ, &cws) < 0) return error.IoctlFailed; | 68 | if (c.ioctl(master, c.TIOCSWINSZ, &cws) < 0) return error.IoctlFailed; |
| 69 | } | 69 | } |
| 70 | |||
| 71 | pub fn anonFd(name: [*:0]const u8) error{CarrierFailed}!std.posix.fd_t { | ||
| 72 | return std.posix.memfd_create(std.mem.span(name), 0) catch error.CarrierFailed; | ||
| 73 | } | ||
src/server/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -15,6 +15,7 @@ const sockpath = @import("sockpath"); | |||
| 15 | const serve = @import("serve"); | 15 | const serve = @import("serve"); |
| 16 | const quic = @import("quic"); | 16 | const quic = @import("quic"); |
| 17 | const xdg = @import("xdg"); | 17 | const xdg = @import("xdg"); |
| 18 | const server_os = @import("server_os"); | ||
| 18 | // Re-exported for the daemon's own main (src/cli/main.zig) — the only | 19 | // Re-exported for the daemon's own main (src/cli/main.zig) — the only |
| 19 | // consumer outside this folder; nobody else may know these exist. | 20 | // consumer outside this folder; nobody else may know these exist. |
| 20 | pub const quic_server = @import("quic_server.zig"); | 21 | pub const quic_server = @import("quic_server.zig"); |
| @@ -515,11 +516,12 @@ pub const Server = struct { | |||
| 515 | // importing build_options (which would conflict with exe's own import). | 516 | // importing build_options (which would conflict with exe's own import). |
| 516 | version: []const u8 = "", | 517 | version: []const u8 = "", |
| 517 | 518 | ||
| 518 | // What the run loop needs to exec: the candidate's path and the memfd | 519 | // What the run loop needs to exec: the candidate's path and the carrier |
| 519 | // holding the manifest. Set by validateUpgrade + writeManifestTo. | 520 | // holding the manifest (`server_os.anonFd`). Set by validateUpgrade + |
| 521 | // writeManifestTo. | ||
| 520 | const PendingUpgrade = struct { | 522 | const PendingUpgrade = struct { |
| 521 | path: []const u8, | 523 | path: []const u8, |
| 522 | memfd: std.posix.fd_t, | 524 | carrier: std.posix.fd_t, |
| 523 | }; | 525 | }; |
| 524 | 526 | ||
| 525 | pub const Options = struct { | 527 | pub const Options = struct { |
| @@ -595,7 +597,7 @@ pub const Server = struct { | |||
| 595 | return srv; | 597 | return srv; |
| 596 | } | 598 | } |
| 597 | 599 | ||
| 598 | /// Adopt the manifest an exec-ing daemon left in a memfd. | 600 | /// Adopt the manifest an exec-ing daemon left in its carrier. |
| 599 | pub fn initFromManifest( | 601 | pub fn initFromManifest( |
| 600 | alloc: std.mem.Allocator, | 602 | alloc: std.mem.Allocator, |
| 601 | parsed: *const upgrade.Parsed, | 603 | parsed: *const upgrade.Parsed, |
| @@ -1082,9 +1084,9 @@ pub const Server = struct { | |||
| 1082 | // An upgrade was accepted: the reply has drained (pumpOnce | 1084 | // An upgrade was accepted: the reply has drained (pumpOnce |
| 1083 | // ran the observer handler), so exec now. Shaped like | 1085 | // ran the observer handler), so exec now. Shaped like |
| 1084 | // shutdown_flag but per-instance because the exec carries the | 1086 | // shutdown_flag but per-instance because the exec carries the |
| 1085 | // candidate's path and memfd. | 1087 | // candidate's path and carrier. |
| 1086 | if (self.pending_upgrade) |up| { | 1088 | if (self.pending_upgrade) |up| { |
| 1087 | self.execUpgrade(up.path, up.memfd); | 1089 | self.execUpgrade(up.path, up.carrier); |
| 1088 | // execUpgrade only returns on failure; the daemon carries on. | 1090 | // execUpgrade only returns on failure; the daemon carries on. |
| 1089 | continue; | 1091 | continue; |
| 1090 | } | 1092 | } |
| @@ -1627,7 +1629,7 @@ pub const Server = struct { | |||
| 1627 | fn cancelUpgrade(self: *Server) void { | 1629 | fn cancelUpgrade(self: *Server) void { |
| 1628 | const up = self.pending_upgrade orelse return; | 1630 | const up = self.pending_upgrade orelse return; |
| 1629 | self.alloc.free(up.path); | 1631 | self.alloc.free(up.path); |
| 1630 | std.posix.close(up.memfd); | 1632 | std.posix.close(up.carrier); |
| 1631 | self.pending_upgrade = null; | 1633 | self.pending_upgrade = null; |
| 1632 | } | 1634 | } |
| 1633 | 1635 | ||
| @@ -2261,18 +2263,18 @@ pub const Server = struct { | |||
| 2261 | defer self.alloc.free(reason); | 2263 | defer self.alloc.free(reason); |
| 2262 | return self.refuseUpgrade(i, reason); | 2264 | return self.refuseUpgrade(i, reason); |
| 2263 | } | 2265 | } |
| 2264 | // Accepted: write the manifest to a memfd (no CLOEXEC — the | 2266 | // Accepted: write the manifest to its carrier (not CLOEXEC — |
| 2265 | // new binary must inherit it), reply, and arm the exec. | 2267 | // the new binary must inherit it), reply, and arm the exec. |
| 2266 | const memfd = std.posix.memfd_create("mux-upgrade", 0) catch return self.refuseUpgrade(i, "memfd"); | 2268 | const carrier = server_os.anonFd("mux-upgrade") catch return self.refuseUpgrade(i, "carrier"); |
| 2267 | self.writeManifestTo(memfd, self.version) catch { | 2269 | self.writeManifestTo(carrier, self.version) catch { |
| 2268 | std.posix.close(memfd); | 2270 | std.posix.close(carrier); |
| 2269 | return self.refuseUpgrade(i, "manifest"); | 2271 | return self.refuseUpgrade(i, "manifest"); |
| 2270 | }; | 2272 | }; |
| 2271 | // Owned, because `req.path` points into the frame payload | 2273 | // Owned, because `req.path` points into the frame payload |
| 2272 | // this handler's caller frees on return, and the exec runs a | 2274 | // this handler's caller frees on return, and the exec runs a |
| 2273 | // pump later. execUpgrade frees it if the exec fails. | 2275 | // pump later. execUpgrade frees it if the exec fails. |
| 2274 | const path = self.alloc.dupe(u8, req.path) catch { | 2276 | const path = self.alloc.dupe(u8, req.path) catch { |
| 2275 | std.posix.close(memfd); | 2277 | std.posix.close(carrier); |
| 2276 | return self.refuseUpgrade(i, "oom"); | 2278 | return self.refuseUpgrade(i, "oom"); |
| 2277 | }; | 2279 | }; |
| 2278 | var accepted: [1]u8 = undefined; | 2280 | var accepted: [1]u8 = undefined; |
| @@ -2281,7 +2283,7 @@ pub const Server = struct { | |||
| 2281 | .reason = "", | 2283 | .reason = "", |
| 2282 | })); | 2284 | })); |
| 2283 | self.dropObserver(i); | 2285 | self.dropObserver(i); |
| 2284 | self.pending_upgrade = .{ .path = path, .memfd = memfd }; | 2286 | self.pending_upgrade = .{ .path = path, .carrier = carrier }; |
| 2285 | }, | 2287 | }, |
| 2286 | // Where `mux a status` actually lands: it asks and exits without | 2288 | // Where `mux a status` actually lands: it asks and exits without |
| 2287 | // ever attaching. Blocking reply for the same reason the stats | 2289 | // ever attaching. Blocking reply for the same reason the stats |
| @@ -2814,7 +2816,7 @@ pub const Server = struct { | |||
| 2814 | const writer_path = try std.fs.selfExePath(&exe_buf); | 2816 | const writer_path = try std.fs.selfExePath(&exe_buf); |
| 2815 | 2817 | ||
| 2816 | // QUIC runtime state: arm, fd, bound address, idle, key. The key | 2818 | // QUIC runtime state: arm, fd, bound address, idle, key. The key |
| 2817 | // crosses as bytes — the memfd is anonymous and the file may have | 2819 | // crosses as bytes — no path names the carrier and the file may have |
| 2818 | // moved. None means no QUIC at all. | 2820 | // moved. None means no QUIC at all. |
| 2819 | var quic_state: upgrade.QuicState = .{}; | 2821 | var quic_state: upgrade.QuicState = .{}; |
| 2820 | switch (self.quic) { | 2822 | switch (self.quic) { |
| @@ -2977,21 +2979,21 @@ pub const Server = struct { | |||
| 2977 | return null; | 2979 | return null; |
| 2978 | } | 2980 | } |
| 2979 | 2981 | ||
| 2980 | // Child-run `path run --resume-fd N --check` must exit 0. The memfd | 2982 | // Child-run `path run --resume-fd N --check` must exit 0. The carrier |
| 2981 | // is written fresh (no CLOEXEC — children must inherit it). --check | 2983 | // is written fresh (not CLOEXEC — children must inherit it). --check |
| 2982 | // does not exist until chunk D; this helper is the structure the e2e | 2984 | // does not exist until chunk D; this helper is the structure the e2e |
| 2983 | // legs will drive. | 2985 | // legs will drive. |
| 2984 | fn checkManifestResume(self: *Server, path: []const u8, my_version: []const u8) ?[]const u8 { | 2986 | fn checkManifestResume(self: *Server, path: []const u8, my_version: []const u8) ?[]const u8 { |
| 2985 | const a = self.alloc; | 2987 | const a = self.alloc; |
| 2986 | const memfd = std.posix.memfd_create("mux-upgrade", 0) catch | 2988 | const carrier = server_os.anonFd("mux-upgrade") catch |
| 2987 | return a.dupe(u8, "check: cannot create memfd") catch null; | 2989 | return a.dupe(u8, "check: cannot create the manifest carrier") catch null; |
| 2988 | defer std.posix.close(memfd); | 2990 | defer std.posix.close(carrier); |
| 2989 | 2991 | ||
| 2990 | self.writeManifestTo(memfd, my_version) catch | 2992 | self.writeManifestTo(carrier, my_version) catch |
| 2991 | return a.dupe(u8, "check: cannot write manifest") catch null; | 2993 | return a.dupe(u8, "check: cannot write manifest") catch null; |
| 2992 | 2994 | ||
| 2993 | var fdbuf: [12]u8 = undefined; | 2995 | var fdbuf: [12]u8 = undefined; |
| 2994 | const fd_str = std.fmt.bufPrint(&fdbuf, "{d}", .{memfd}) catch | 2996 | const fd_str = std.fmt.bufPrint(&fdbuf, "{d}", .{carrier}) catch |
| 2995 | return a.dupe(u8, "check: oom") catch null; | 2997 | return a.dupe(u8, "check: oom") catch null; |
| 2996 | const result = std.process.Child.run(.{ | 2998 | const result = std.process.Child.run(.{ |
| 2997 | .allocator = a, | 2999 | .allocator = a, |
| @@ -3007,7 +3009,7 @@ pub const Server = struct { | |||
| 3007 | 3009 | ||
| 3008 | // Clear FD_CLOEXEC on a descriptor so it survives execve. The upgrade | 3010 | // Clear FD_CLOEXEC on a descriptor so it survives execve. The upgrade |
| 3009 | // exec keeps the listener, QUIC UDP, pty masters, agent listeners and | 3011 | // exec keeps the listener, QUIC UDP, pty masters, agent listeners and |
| 3010 | // the manifest memfd; all are CLOEXEC by default and must be cleared. | 3012 | // the manifest carrier; all are CLOEXEC by default and must be cleared. |
| 3011 | pub fn clearCloexec(fd: std.posix.fd_t) !void { | 3013 | pub fn clearCloexec(fd: std.posix.fd_t) !void { |
| 3012 | const flags = try std.posix.fcntl(fd, std.posix.F.GETFD, 0); | 3014 | const flags = try std.posix.fcntl(fd, std.posix.F.GETFD, 0); |
| 3013 | _ = try std.posix.fcntl(fd, std.posix.F.SETFD, flags & ~@as(usize, std.posix.FD_CLOEXEC)); | 3015 | _ = try std.posix.fcntl(fd, std.posix.F.SETFD, flags & ~@as(usize, std.posix.FD_CLOEXEC)); |
| @@ -3029,11 +3031,11 @@ pub const Server = struct { | |||
| 3029 | std.debug.print("mux d: seal fd {d}: {t}\n", .{ fd, e }); | 3031 | std.debug.print("mux d: seal fd {d}: {t}\n", .{ fd, e }); |
| 3030 | } | 3032 | } |
| 3031 | 3033 | ||
| 3032 | /// The flag back on every fd `execUpgrade` cleared; the memfd it also | 3034 | /// The flag back on every fd `execUpgrade` cleared; the manifest carrier |
| 3033 | /// cleared is the caller's to close. The caller runs this only once the | 3035 | /// it also cleared is the caller's to close. The caller runs this only |
| 3034 | /// last rollback point is behind it, which is why the fds no session | 3036 | /// once the last rollback point is behind it, which is why the fds no |
| 3035 | /// adopted are CLOSED here rather than sealed: before this point they | 3037 | /// session adopted are CLOSED here rather than sealed: before this point |
| 3036 | /// still belong to the binary a rollback would exec, and after it | 3038 | /// they still belong to the binary a rollback would exec, and after it |
| 3037 | /// nothing will ever name them again. Clearing the count keeps a second | 3039 | /// nothing will ever name them again. Clearing the count keeps a second |
| 3038 | /// call from double-closing. | 3040 | /// call from double-closing. |
| 3039 | pub fn sealAdoptedFds(self: *Server) void { | 3041 | pub fn sealAdoptedFds(self: *Server) void { |
| @@ -3054,7 +3056,7 @@ pub const Server = struct { | |||
| 3054 | // it becomes the new binary. Every sink is bare-closed, NEVER sent | 3056 | // it becomes the new binary. Every sink is bare-closed, NEVER sent |
| 3055 | // `exit_status` — that is a dying shell's word and makes clients exit | 3057 | // `exit_status` — that is a dying shell's word and makes clients exit |
| 3056 | // instead of redial. A failed exec must leave a working daemon. | 3058 | // instead of redial. A failed exec must leave a working daemon. |
| 3057 | fn execUpgrade(self: *Server, path: []const u8, memfd: std.posix.fd_t) void { | 3059 | fn execUpgrade(self: *Server, path: []const u8, carrier: std.posix.fd_t) void { |
| 3058 | const a = self.alloc; | 3060 | const a = self.alloc; |
| 3059 | 3061 | ||
| 3060 | // Every return below is a FAILED exec, so the arming is spent either | 3062 | // Every return below is a FAILED exec, so the arming is spent either |
| @@ -3062,13 +3064,13 @@ pub const Server = struct { | |||
| 3062 | // exec on a loop. | 3064 | // exec on a loop. |
| 3063 | defer { | 3065 | defer { |
| 3064 | a.free(path); | 3066 | a.free(path); |
| 3065 | std.posix.close(memfd); | 3067 | std.posix.close(carrier); |
| 3066 | self.pending_upgrade = null; | 3068 | self.pending_upgrade = null; |
| 3067 | } | 3069 | } |
| 3068 | 3070 | ||
| 3069 | // Collect the fds to clear CLOEXEC on, so they can be restored on | 3071 | // Collect the fds to clear CLOEXEC on, so they can be restored on |
| 3070 | // failure. The listener, QUIC UDP (if any), every pty master, every | 3072 | // failure. The listener, QUIC UDP (if any), every pty master, every |
| 3071 | // agent listener, and the memfd. | 3073 | // agent listener, and the manifest carrier. |
| 3072 | var cleared: std.ArrayList(std.posix.fd_t) = .empty; | 3074 | var cleared: std.ArrayList(std.posix.fd_t) = .empty; |
| 3073 | defer cleared.deinit(a); | 3075 | defer cleared.deinit(a); |
| 3074 | // After the list's own defer, so it runs before it: one restore | 3076 | // After the list's own defer, so it runs before it: one restore |
| @@ -3096,8 +3098,8 @@ pub const Server = struct { | |||
| 3096 | } | 3098 | } |
| 3097 | } | 3099 | } |
| 3098 | 3100 | ||
| 3099 | clearCloexec(memfd) catch return; | 3101 | clearCloexec(carrier) catch return; |
| 3100 | cleared.append(a, memfd) catch return; | 3102 | cleared.append(a, carrier) catch return; |
| 3101 | 3103 | ||
| 3102 | // Bare-close every client sink and observer. NOT exit_status: that | 3104 | // Bare-close every client sink and observer. NOT exit_status: that |
| 3103 | // tells a client its shell died, and it exits instead of redialing. | 3105 | // tells a client its shell died, and it exits instead of redialing. |
| @@ -3121,7 +3123,7 @@ pub const Server = struct { | |||
| 3121 | // candidate is by definition NEWER than this binary, so it reads the | 3123 | // candidate is by definition NEWER than this binary, so it reads the |
| 3122 | // mode word. | 3124 | // mode word. |
| 3123 | var fd_buf: [12]u8 = undefined; | 3125 | var fd_buf: [12]u8 = undefined; |
| 3124 | const fd_str = std.fmt.bufPrintZ(&fd_buf, "{d}", .{memfd}) catch return; | 3126 | const fd_str = std.fmt.bufPrintZ(&fd_buf, "{d}", .{carrier}) catch return; |
| 3125 | const argv = [_:null]?[*:0]const u8{ | 3127 | const argv = [_:null]?[*:0]const u8{ |
| 3126 | "mux", | 3128 | "mux", |
| 3127 | "d", | 3129 | "d", |
src/server/server_test_upgrade.zig
| Old | New | ||
|---|---|---|---|
| @@ -2,6 +2,7 @@ const std = @import("std"); | |||
| 2 | const proto = @import("term").protocol; | 2 | const proto = @import("term").protocol; |
| 3 | const upgrade = @import("upgrade.zig"); | 3 | const upgrade = @import("upgrade.zig"); |
| 4 | const TmpDir = @import("testtmp").TmpDir; | 4 | const TmpDir = @import("testtmp").TmpDir; |
| 5 | const server_os = @import("server_os"); | ||
| 5 | const h = @import("server_test_harness.zig"); | 6 | const h = @import("server_test_harness.zig"); |
| 6 | const dial = h.dial; | 7 | const dial = h.dial; |
| 7 | const srv_mod = @import("server.zig"); | 8 | const srv_mod = @import("server.zig"); |
| @@ -22,11 +23,11 @@ test "writeManifestTo: every session's own name crosses, not the last one's" { | |||
| 22 | td.srv.sessions.table[1] = try SessionTable.create(alloc, td.srv.spawn_plan, "work", 80, 24, null); | 23 | td.srv.sessions.table[1] = try SessionTable.create(alloc, td.srv.spawn_plan, "work", 80, 24, null); |
| 23 | td.srv.sessions.table[2] = try SessionTable.create(alloc, td.srv.spawn_plan, "logs", 80, 24, null); | 24 | td.srv.sessions.table[2] = try SessionTable.create(alloc, td.srv.spawn_plan, "logs", 80, 24, null); |
| 24 | 25 | ||
| 25 | const memfd = try std.posix.memfd_create("mux-names-test", 0); | 26 | const carrier = try server_os.anonFd("mux-names-test"); |
| 26 | defer std.posix.close(memfd); | 27 | defer std.posix.close(carrier); |
| 27 | try td.srv.writeManifestTo(memfd, "0.0.1-99"); | 28 | try td.srv.writeManifestTo(carrier, "0.0.1-99"); |
| 28 | 29 | ||
| 29 | var file = std.fs.File{ .handle = memfd }; | 30 | var file = std.fs.File{ .handle = carrier }; |
| 30 | try file.seekTo(0); | 31 | try file.seekTo(0); |
| 31 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); | 32 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); |
| 32 | defer alloc.free(buf); | 33 | defer alloc.free(buf); |
| @@ -56,13 +57,13 @@ test "writeManifestTo: what crosses is what a session cannot rebuild" { | |||
| 56 | // Feed some content so dumpState is non-empty. | 57 | // Feed some content so dumpState is non-empty. |
| 57 | s.eng.feed("hello\r\n"); | 58 | s.eng.feed("hello\r\n"); |
| 58 | 59 | ||
| 59 | const memfd = try std.posix.memfd_create("mux-upgrade-test", 0); | 60 | const carrier = try server_os.anonFd("mux-upgrade-test"); |
| 60 | defer std.posix.close(memfd); | 61 | defer std.posix.close(carrier); |
| 61 | 62 | ||
| 62 | try td.srv.writeManifestTo(memfd, "0.0.1-99"); | 63 | try td.srv.writeManifestTo(carrier, "0.0.1-99"); |
| 63 | 64 | ||
| 64 | // Read back and parse. | 65 | // Read back and parse. |
| 65 | var file = std.fs.File{ .handle = memfd }; | 66 | var file = std.fs.File{ .handle = carrier }; |
| 66 | try file.seekTo(0); | 67 | try file.seekTo(0); |
| 67 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); | 68 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); |
| 68 | defer alloc.free(buf); | 69 | defer alloc.free(buf); |
| @@ -119,9 +120,9 @@ test "initFromManifest: an adopted session answers a status_req without having b | |||
| 119 | const old_epoch = srv.sessions.table[0].?.epoch; | 120 | const old_epoch = srv.sessions.table[0].?.epoch; |
| 120 | const child = srv.sessions.table[0].?.pty.child; | 121 | const child = srv.sessions.table[0].?.pty.child; |
| 121 | 122 | ||
| 122 | const memfd = try std.posix.memfd_create("mux-adopt-test", 0); | 123 | const carrier = try server_os.anonFd("mux-adopt-test"); |
| 123 | defer std.posix.close(memfd); | 124 | defer std.posix.close(carrier); |
| 124 | try srv.writeManifestTo(memfd, "0.0.1-99"); | 125 | try srv.writeManifestTo(carrier, "0.0.1-99"); |
| 125 | 126 | ||
| 126 | // Release the first Server's MEMORY by hand instead of calling deinit: | 127 | // Release the first Server's MEMORY by hand instead of calling deinit: |
| 127 | // deinit is the demolition list — it unlinks the socket, SIGKILLs the | 128 | // deinit is the demolition list — it unlinks the socket, SIGKILLs the |
| @@ -138,7 +139,7 @@ test "initFromManifest: an adopted session answers a status_req without having b | |||
| 138 | if (srv.agents.dir) |d| alloc.free(d); | 139 | if (srv.agents.dir) |d| alloc.free(d); |
| 139 | srv.shellint_arena.deinit(); | 140 | srv.shellint_arena.deinit(); |
| 140 | 141 | ||
| 141 | var file = std.fs.File{ .handle = memfd }; | 142 | var file = std.fs.File{ .handle = carrier }; |
| 142 | try file.seekTo(0); | 143 | try file.seekTo(0); |
| 143 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); | 144 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); |
| 144 | defer alloc.free(buf); | 145 | defer alloc.free(buf); |
| @@ -191,9 +192,9 @@ test "initFromManifest: a session whose agent socket file vanished loses forward | |||
| 191 | // failed adoption nothing on either Server names it any more. | 192 | // failed adoption nothing on either Server names it any more. |
| 192 | const agent_fd = srv.sessions.table[0].?.agentFd(); | 193 | const agent_fd = srv.sessions.table[0].?.agentFd(); |
| 193 | 194 | ||
| 194 | const memfd = try std.posix.memfd_create("mux-goneagent-test", 0); | 195 | const carrier = try server_os.anonFd("mux-goneagent-test"); |
| 195 | defer std.posix.close(memfd); | 196 | defer std.posix.close(carrier); |
| 196 | try srv.writeManifestTo(memfd, "0.0.1-99"); | 197 | try srv.writeManifestTo(carrier, "0.0.1-99"); |
| 197 | 198 | ||
| 198 | // The seam: something outside mux cleaned the runtime directory between | 199 | // The seam: something outside mux cleaned the runtime directory between |
| 199 | // the manifest and the exec. Adoption re-stamps the socket's id from the | 200 | // the manifest and the exec. Adoption re-stamps the socket's id from the |
| @@ -213,7 +214,7 @@ test "initFromManifest: a session whose agent socket file vanished loses forward | |||
| 213 | if (srv.agents.dir) |d| alloc.free(d); | 214 | if (srv.agents.dir) |d| alloc.free(d); |
| 214 | srv.shellint_arena.deinit(); | 215 | srv.shellint_arena.deinit(); |
| 215 | 216 | ||
| 216 | var file = std.fs.File{ .handle = memfd }; | 217 | var file = std.fs.File{ .handle = carrier }; |
| 217 | try file.seekTo(0); | 218 | try file.seekTo(0); |
| 218 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); | 219 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); |
| 219 | defer alloc.free(buf); | 220 | defer alloc.free(buf); |
| @@ -271,9 +272,9 @@ test "initFromManifest: the return watermark is re-stamped, never carried across | |||
| 271 | .seq = 999, | 272 | .seq = 999, |
| 272 | }; | 273 | }; |
| 273 | 274 | ||
| 274 | const memfd = try std.posix.memfd_create("mux-watermark-test", 0); | 275 | const carrier = try server_os.anonFd("mux-watermark-test"); |
| 275 | defer std.posix.close(memfd); | 276 | defer std.posix.close(carrier); |
| 276 | try srv.writeManifestTo(memfd, "0.0.1-99"); | 277 | try srv.writeManifestTo(carrier, "0.0.1-99"); |
| 277 | 278 | ||
| 278 | // Memory only; the descriptors and the child are srv2's to tear down. | 279 | // Memory only; the descriptors and the child are srv2's to tear down. |
| 279 | { | 280 | { |
| @@ -286,7 +287,7 @@ test "initFromManifest: the return watermark is re-stamped, never carried across | |||
| 286 | if (srv.agents.dir) |d| alloc.free(d); | 287 | if (srv.agents.dir) |d| alloc.free(d); |
| 287 | srv.shellint_arena.deinit(); | 288 | srv.shellint_arena.deinit(); |
| 288 | 289 | ||
| 289 | var file = std.fs.File{ .handle = memfd }; | 290 | var file = std.fs.File{ .handle = carrier }; |
| 290 | try file.seekTo(0); | 291 | try file.seekTo(0); |
| 291 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); | 292 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); |
| 292 | defer alloc.free(buf); | 293 | defer alloc.free(buf); |
| @@ -353,10 +354,12 @@ test "validateUpgrade: a non-executable path is refused" { | |||
| 353 | if (reason) |r| alloc.free(r); | 354 | if (reason) |r| alloc.free(r); |
| 354 | } | 355 | } |
| 355 | 356 | ||
| 356 | test "clearCloexec: a CLOEXEC memfd's flag flips" { | 357 | test "clearCloexec: a CLOEXEC carrier's flag flips" { |
| 357 | // Create a memfd WITH CLOEXEC, then clear it and verify via F_GETFD. | 358 | // A carrier is never CLOEXEC, so arm the flag with the production |
| 358 | const fd = try std.posix.memfd_create("mux-clearcloexec-test", 0x0001); | 359 | // inverse of the call under test, then clear it and read F_GETFD. |
| 360 | const fd = try server_os.anonFd("mux-clearcloexec-test"); | ||
| 359 | defer std.posix.close(fd); | 361 | defer std.posix.close(fd); |
| 362 | try Server.setCloexec(fd); | ||
| 360 | 363 | ||
| 361 | // Confirm it starts with CLOEXEC. | 364 | // Confirm it starts with CLOEXEC. |
| 362 | const before = try std.posix.fcntl(fd, std.posix.F.GETFD, 0); | 365 | const before = try std.posix.fcntl(fd, std.posix.F.GETFD, 0); |
| @@ -387,9 +390,9 @@ test "sealAdoptedFds: the adopted fds are CLOEXEC again, and not one step before | |||
| 387 | defer alloc.free(sock_path); | 390 | defer alloc.free(sock_path); |
| 388 | 391 | ||
| 389 | var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); | 392 | var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); |
| 390 | const memfd = try std.posix.memfd_create("mux-cloexec-test", 0); | 393 | const carrier = try server_os.anonFd("mux-cloexec-test"); |
| 391 | defer std.posix.close(memfd); | 394 | defer std.posix.close(carrier); |
| 392 | try srv.writeManifestTo(memfd, "0.0.1-99"); | 395 | try srv.writeManifestTo(carrier, "0.0.1-99"); |
| 393 | 396 | ||
| 394 | // What execUpgrade does on the way out: the flag is cleared so the fds | 397 | // What execUpgrade does on the way out: the flag is cleared so the fds |
| 395 | // cross the exec. The adopting side must put it back. | 398 | // cross the exec. The adopting side must put it back. |
| @@ -410,7 +413,7 @@ test "sealAdoptedFds: the adopted fds are CLOEXEC again, and not one step before | |||
| 410 | if (srv.agents.dir) |d| alloc.free(d); | 413 | if (srv.agents.dir) |d| alloc.free(d); |
| 411 | srv.shellint_arena.deinit(); | 414 | srv.shellint_arena.deinit(); |
| 412 | 415 | ||
| 413 | var file = std.fs.File{ .handle = memfd }; | 416 | var file = std.fs.File{ .handle = carrier }; |
| 414 | try file.seekTo(0); | 417 | try file.seekTo(0); |
| 415 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); | 418 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); |
| 416 | defer alloc.free(buf); | 419 | defer alloc.free(buf); |
| @@ -557,9 +560,9 @@ test "Server: an EMPTY daemon upgrades — the manifest names no session and the | |||
| 557 | return error.EmptyDaemonRefusedUpgrade; | 560 | return error.EmptyDaemonRefusedUpgrade; |
| 558 | } | 561 | } |
| 559 | 562 | ||
| 560 | const memfd = try std.posix.memfd_create("mux-empty-upgrade", 0); | 563 | const carrier = try server_os.anonFd("mux-empty-upgrade"); |
| 561 | defer std.posix.close(memfd); | 564 | defer std.posix.close(carrier); |
| 562 | try srv.writeManifestTo(memfd, "0.0.1-1"); | 565 | try srv.writeManifestTo(carrier, "0.0.1-1"); |
| 563 | 566 | ||
| 564 | // The first Server's MEMORY only, never deinit: deinit unlinks the | 567 | // The first Server's MEMORY only, never deinit: deinit unlinks the |
| 565 | // socket and deleteTrees the dirs, and the adopting Server is about to | 568 | // socket and deleteTrees the dirs, and the adopting Server is about to |
| @@ -567,7 +570,7 @@ test "Server: an EMPTY daemon upgrades — the manifest names no session and the | |||
| 567 | if (srv.agents.dir) |d| alloc.free(d); | 570 | if (srv.agents.dir) |d| alloc.free(d); |
| 568 | srv.shellint_arena.deinit(); | 571 | srv.shellint_arena.deinit(); |
| 569 | 572 | ||
| 570 | var file = std.fs.File{ .handle = memfd }; | 573 | var file = std.fs.File{ .handle = carrier }; |
| 571 | try file.seekTo(0); | 574 | try file.seekTo(0); |
| 572 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); | 575 | const buf = try file.readToEndAlloc(alloc, 4 * 1024 * 1024); |
| 573 | defer alloc.free(buf); | 576 | defer alloc.free(buf); |
src/server/upgrade.zig
| Old | New | ||
|---|---|---|---|
| @@ -50,8 +50,8 @@ pub const QuicArm = enum(u8) { none = 0, borrowed = 1, owned = 2 }; | |||
| 50 | 50 | ||
| 51 | /// Runtime QUIC state, not launch flags: lazyBindQuic can own an | 51 | /// Runtime QUIC state, not launch flags: lazyBindQuic can own an |
| 52 | /// ephemeral-port listener no flag names, so the manifest records what IS | 52 | /// ephemeral-port listener no flag names, so the manifest records what IS |
| 53 | /// bound, arm and all. The key crosses as bytes, never a path — the memfd | 53 | /// bound, arm and all. The key crosses as bytes, never a path — no path |
| 54 | /// is anonymous memory and the file the key came from may have moved. | 54 | /// names the carrier and the file the key came from may have moved. |
| 55 | pub const QuicState = struct { | 55 | pub const QuicState = struct { |
| 56 | arm: QuicArm = .none, | 56 | arm: QuicArm = .none, |
| 57 | fd: i32 = -1, | 57 | fd: i32 = -1, |