lib/sys/src/process/namespace.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! Linux descendant containment gives a caller one kill that reaches a whole
2 //! process tree: a child is forked as the init process of a new PID namespace,
3 //! the *PID namespace init*, so killing or exiting it ends everything below it.
4 //! The caller's filesystem namespace is left as it was, so its mounts stay
5 //! shared with the child.
6
7 const std = @import("std");
8 const builtin = @import("builtin");
9 const sys = @import("../root.zig");
10 const process = @import("root.zig");
11
12 const linux = std.os.linux;
13
14 /// Forks a child that is process id 1 inside a new PID namespace and a new user
15 /// namespace, preserving the caller's real and effective user and group
16 /// identity through an identity map, while on the parent side the call yields
17 /// the child's process id as the host numbers it. Killing or exiting that child
18 /// kills the namespace's descendants, including processes that create new
19 /// sessions. The child arms the parent-death signal, which delivers `SIGKILL`
20 /// to a child when its parent exits, and no-new-privileges, which stops a later
21 /// exec from gaining privileges, such as through a setuid binary, while the
22 /// parent waits for both over a socket pair before the call returns. The child
23 /// keeps every mount the caller had, procfs among them, so a numeric path under
24 /// `/proc` read inside the child resolves against the caller's process
25 /// numbering, and only a program whose behavior under that mismatch is known
26 /// belongs beneath this call. The child carries the same restriction any forked
27 /// child carries: it reaches an exec or an exit while touching none of the
28 /// runtime locks it inherited. A caller whose real and effective identity
29 /// differ fails with `NamespaceIdentityUnavailable`, and a host outside Linux
30 /// on x86-64 and aarch64 fails with `UnsupportedPlatform`.
31 pub fn forkContained() !process.ForkResult {
32 if (comptime builtin.os.tag != .linux) return error.UnsupportedPlatform;
33 if (comptime builtin.cpu.arch != .x86_64 and builtin.cpu.arch != .aarch64) {
34 return error.UnsupportedPlatform;
35 }
36 const uid = linux.getuid();
37 const gid = linux.getgid();
38 if (uid != linux.geteuid() or gid != linux.getegid()) {
39 return error.NamespaceIdentityUnavailable;
40 }
41 var channel: [2]sys.fd.Descriptor = undefined;
42 const channel_result = linux.socketpair(
43 linux.AF.UNIX,
44 linux.SOCK.SEQPACKET | linux.SOCK.CLOEXEC,
45 0,
46 &channel,
47 );
48 if (linux.errno(channel_result) != .SUCCESS) {
49 return error.NamespaceChannelFailed;
50 }
51 defer closeDescriptor(&channel[0]);
52 defer closeDescriptor(&channel[1]);
53 const flags = linux.CLONE.NEWUSER | linux.CLONE.NEWPID | @backingInt(linux.SIG.CHLD);
54 const result = linux.clone2(flags, 0);
55 switch (linux.errno(result)) {
56 .SUCCESS => {},
57 .PERM, .ACCES => return error.NamespacePermissionDenied,
58 .AGAIN, .NOMEM, .NOSPC => return error.SystemResources,
59 else => return error.NamespaceCreationFailed,
60 }
61 if (result == 0) {
62 closeDescriptor(&channel[0]);
63 childReady(channel[1]) catch process.exit(127);
64 std.debug.assert(linux.getpid() == 1);
65 std.debug.assert(linux.getuid() == uid);
66 std.debug.assert(linux.getgid() == gid);
67 return .child;
68 }
69 closeDescriptor(&channel[1]);
70 const child: process.ProcessId = @intCast(result);
71 std.debug.assert(child > 1);
72 errdefer {
73 process.forceKillChildId(child);
74 _ = process.waitDirect(child) catch {};
75 }
76 try receiveReady(channel[0]);
77 try mapIdentity(child, "uid_map", uid);
78 try writeControl(child, "setgroups", "deny");
79 try mapIdentity(child, "gid_map", gid);
80 try sendReady(channel[0]);
81 return .{ .parent = child };
82 }
83
84 fn childReady(channel: sys.fd.Descriptor) !void {
85 std.debug.assert(channel >= 0);
86 const death_signal = linux.prctl(
87 @backingInt(linux.PR.SET_PDEATHSIG),
88 @backingInt(linux.SIG.KILL),
89 0,
90 0,
91 0,
92 );
93 if (linux.errno(death_signal) != .SUCCESS) {
94 return error.NamespaceParentDeathFailed;
95 }
96 if (linux.errno(linux.prctl(@backingInt(linux.PR.SET_NO_NEW_PRIVS), 1, 0, 0, 0)) != .SUCCESS) {
97 return error.NamespacePrivilegesFailed;
98 }
99 try sendReady(channel);
100 try receiveReady(channel);
101 }
102
103 fn receiveReady(descriptor: sys.fd.Descriptor) !void {
104 std.debug.assert(descriptor >= 0);
105 var marker: [1]u8 = undefined;
106 if (try sys.fd.read(descriptor, &marker) != 1 or marker[0] != 1) {
107 return error.NamespaceLaunchFailed;
108 }
109 }
110
111 fn sendReady(descriptor: sys.fd.Descriptor) !void {
112 std.debug.assert(descriptor >= 0);
113 const marker = [1]u8{1};
114 const sent = linux.sendto(
115 descriptor,
116 &marker,
117 marker.len,
118 linux.MSG.NOSIGNAL | linux.MSG.DONTWAIT,
119 null,
120 0,
121 );
122 if (sent != 1) {
123 return error.NamespaceLaunchFailed;
124 }
125 }
126
127 fn mapIdentity(child: process.ProcessId, comptime name: []const u8, identity: u32) !void {
128 std.debug.assert(child > 1);
129 var bytes: [32]u8 = undefined;
130 const mapping = try std.fmt.bufPrint(&bytes, "{d} {d} 1\n", .{ identity, identity });
131 std.debug.assert(mapping.len <= 24);
132 try writeControl(child, name, mapping);
133 }
134
135 fn writeControl(child: process.ProcessId, comptime name: []const u8, bytes: []const u8) !void {
136 std.debug.assert(child > 1);
137 std.debug.assert(bytes.len > 0);
138 std.debug.assert(bytes.len <= 32);
139 var path: [64]u8 = undefined;
140 const name_z = try std.fmt.bufPrintSentinel(&path, "/proc/{d}/{s}", .{ child, name }, 0);
141 const descriptor = try sys.fd.openAtZ(std.posix.AT.FDCWD, name_z, .{
142 .ACCMODE = .WRONLY,
143 .CLOEXEC = true,
144 }, 0);
145 defer sys.fd.close(descriptor);
146 if (try sys.fd.write(descriptor, bytes) != bytes.len) return error.NamespaceMappingFailed;
147 }
148
149 fn closeDescriptor(descriptor: *sys.fd.Descriptor) void {
150 if (descriptor.* >= 0) sys.fd.close(descriptor.*);
151 descriptor.* = -1;
152 }
153
154 test "contained fork preserves user and mount identities with a host child handle" {
155 if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
156 const root = try sys.fd.openAt(sys.fd.cwd_descriptor, "/", .{
157 .ACCMODE = .RDONLY,
158 .CLOEXEC = true,
159 .DIRECTORY = true,
160 }, 0);
161 defer sys.fd.close(root);
162 const mount = try sys.fd.mountIdentity(root);
163 const uid = try process.currentUserId();
164 const gid = try process.currentGroupId();
165 const child = switch (try forkContained()) {
166 .parent => |id| id,
167 .child => {
168 const reopened = sys.fd.openAt(root, ".", .{
169 .ACCMODE = .RDONLY,
170 .DIRECTORY = true,
171 }, 0) catch process.exit(91);
172 const observed = sys.fd.mountIdentity(reopened) catch process.exit(92);
173 if (!mount.eql(observed)) process.exit(93);
174 if ((process.currentProcessId() catch process.exit(94)) != 1) process.exit(95);
175 if ((process.currentUserId() catch process.exit(96)) != uid) process.exit(97);
176 if ((process.currentGroupId() catch process.exit(98)) != gid) process.exit(99);
177 process.exit(0);
178 },
179 };
180 try std.testing.expect(child > 1);
181 try std.testing.expectEqual(process.Termination{ .exited = 0 }, try process.waitDirect(child));
182 }
183
184 test "contained fork drains an escaped lease holder after its parent dies" {
185 if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
186 var temporary = std.testing.tmpDir(.{});
187 defer temporary.cleanup();
188 var lease = try sys.fd.openAt(temporary.dir.handle, "lease", .{
189 .ACCMODE = .RDWR,
190 .CREAT = true,
191 .EXCL = true,
192 }, 0o600);
193 defer closeDescriptor(&lease);
194 try std.testing.expect(try sys.fd.lock(lease, .shared, true));
195 var ready = try sys.fd.pipeWithOptions(.{});
196 defer closeDescriptor(&ready[0]);
197 defer closeDescriptor(&ready[1]);
198 const owner = switch (try process.fork()) {
199 .parent => |id| id,
200 .child => {
201 sys.fd.close(ready[0]);
202 parentDeathWitness(ready[1]) catch process.exit(90);
203 process.exit(0);
204 },
205 };
206 closeDescriptor(&ready[1]);
207 var owner_pending = true;
208 defer if (owner_pending) {
209 process.forceKillChildId(owner);
210 _ = process.waitDirect(owner) catch {};
211 };
212 var marker: [1]u8 = undefined;
213 try std.testing.expectEqual(@as(usize, 1), try sys.fd.read(ready[0], &marker));
214 try std.testing.expectEqual(@as(u8, 1), marker[0]);
215 closeDescriptor(&lease);
216 const fresh = try sys.fd.openAt(temporary.dir.handle, "lease", .{ .ACCMODE = .RDWR }, 0);
217 defer sys.fd.close(fresh);
218 try std.testing.expect(!try sys.fd.lock(fresh, .exclusive, false));
219 process.forceKillChildId(owner);
220 _ = try process.waitDirect(owner);
221 owner_pending = false;
222 for (0..1000) |_| {
223 if (try sys.fd.lock(fresh, .exclusive, false)) return;
224 sys.time.sleepMilliseconds(1);
225 }
226 return error.ContainedDescendantRetainedLease;
227 }
228
229 fn parentDeathWitness(ready: sys.fd.Descriptor) !void {
230 switch (try forkContained()) {
231 .parent => |child| {
232 sys.fd.close(ready);
233 _ = try process.waitDirect(child);
234 },
235 .child => {
236 switch (try process.fork()) {
237 .parent => {},
238 .child => {
239 if (linux.errno(linux.setsid()) != .SUCCESS) process.exit(91);
240 if (try sys.fd.write(ready, &.{1}) != 1) process.exit(92);
241 },
242 }
243 sys.time.sleepMilliseconds(6000);
244 process.exit(0);
245 },
246 }
247 }