a73x

94838e6a

refactor: the upgrade manifest rides server_os.anonFd

a73x   2026-09-03 15:10

Commit message
refactor: the upgrade manifest rides server_os.anonFd

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW

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,