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 }