lib/sys/src/process/isolated.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const builtin = @import("builtin");
3
4 const Allocator = std.mem.Allocator;
5 const linux = std.os.linux;
6 const native_os = builtin.os.tag;
7 const child_source_min = 4;
8 const default_path = std.Io.Threaded.default_PATH;
9 const syscall_interruptions_max = 8;
10
11 /// Specifies what to run and in what environment through the inputs for a
12 /// captured launch: the argument vector and the environment map.
13 pub const CapturedOptions = struct {
14 argv: []const []const u8,
15 environment: *const std.process.Environ.Map,
16 };
17
18 /// Starts one Linux child whose standard output and standard error are pipes
19 /// the caller reads, with standard input on `/dev/null`, for a caller that
20 /// needs to know the child holds none of its descriptors.
21 ///
22 /// Before it execs, the child arms the parent-death signal to `SIGKILL` and
23 /// starts its own process group. One `close_range` call in the child closes any
24 /// open descriptor above standard error, so that every inherited non-standard
25 /// descriptor is closed before the program runs, while the pre-exec launcher
26 /// keeps one private close-on-exec status descriptor at descriptor 3, whose
27 /// end-of-file after a successful exec tells the parent the launch reached the
28 /// program. Where a command name has no slash, the launcher finds it by
29 /// searching a fixed `PATH` list compiled into the launcher, which the
30 /// environment map does not change.
31 ///
32 /// A launch that fails anywhere before the program starts is killed and reaped,
33 /// and reports `DescriptorIsolationFailed`, while an empty argument vector
34 /// returns `EmptyArgv` and a host other than Linux returns
35 /// `UnsupportedPlatform`.
36 pub fn spawnCaptured(
37 allocator: Allocator,
38 options: CapturedOptions,
39 ) !std.process.Child {
40 if (comptime native_os != .linux) return error.UnsupportedPlatform;
41 if (options.argv.len == 0) return error.EmptyArgv;
42
43 var arena_state = std.heap.ArenaAllocator.init(allocator);
44 defer arena_state.deinit();
45 const arena = arena_state.allocator();
46
47 const argv = try prepareArgv(arena, options.argv);
48 const environment = try options.environment.createPosixBlock(arena, .{
49 .zig_progress_fd = -1,
50 });
51 var executable_storage: [std.posix.PATH_MAX]u8 = undefined;
52 const executable = try executablePath(
53 argv[0].?,
54 options.argv[0],
55 &executable_storage,
56 );
57
58 var descriptors = try Descriptors.init();
59 defer descriptors.deinit();
60 const parent_id = linux.getpid();
61 const fork_result = linux.fork();
62 switch (linux.errno(fork_result)) {
63 .SUCCESS => {},
64 .AGAIN, .NOMEM => return error.SystemResources,
65 else => return error.DescriptorIsolationFailed,
66 }
67 if (fork_result == 0) {
68 launchChild(
69 parent_id,
70 descriptors,
71 executable,
72 argv,
73 environment,
74 );
75 }
76
77 const child_id: linux.pid_t = @intCast(fork_result);
78 descriptors.closeSources();
79 if (!launchSucceeded(descriptors.status_read)) {
80 terminateAndReap(child_id);
81 return error.DescriptorIsolationFailed;
82 }
83 descriptors.closeStatus();
84
85 const stdout_descriptor = descriptors.takeStdout();
86 const stderr_descriptor = descriptors.takeStderr();
87 return .{
88 .id = child_id,
89 .thread_handle = {},
90 .stdin = null,
91 .stdout = .{
92 .handle = stdout_descriptor,
93 .flags = .{ .nonblocking = false },
94 },
95 .stderr = .{
96 .handle = stderr_descriptor,
97 .flags = .{ .nonblocking = false },
98 },
99 .request_resource_usage_statistics = false,
100 };
101 }
102
103 fn prepareArgv(
104 allocator: Allocator,
105 arguments: []const []const u8,
106 ) ![:null]?[*:0]const u8 {
107 const pointers = try allocator.allocSentinel(
108 ?[*:0]const u8,
109 arguments.len,
110 null,
111 );
112 for (arguments, pointers[0..arguments.len]) |argument, *pointer| {
113 if (std.mem.indexOfScalar(u8, argument, 0) != null) {
114 return error.InvalidArgument;
115 }
116 pointer.* = (try allocator.dupeSentinel(u8, argument, 0)).ptr;
117 }
118 return pointers;
119 }
120
121 fn executablePath(
122 prepared_name: [*:0]const u8,
123 name: []const u8,
124 candidate_storage: *[std.posix.PATH_MAX]u8,
125 ) ![:0]const u8 {
126 if (name.len == 0) return error.FileNotFound;
127 if (std.mem.indexOfScalar(u8, name, '/') != null) {
128 return prepared_name[0..name.len :0];
129 }
130
131 var directories = std.mem.splitScalar(u8, default_path, ':');
132 var access_denied = false;
133 while (directories.next()) |raw_directory| {
134 const directory = if (raw_directory.len == 0) "." else raw_directory;
135 const candidate_len = directory.len + 1 + name.len;
136 if (candidate_len >= candidate_storage.len) return error.NameTooLong;
137 @memcpy(candidate_storage[0..directory.len], directory);
138 candidate_storage[directory.len] = '/';
139 @memcpy(candidate_storage[directory.len + 1 .. candidate_len], name);
140 candidate_storage[candidate_len] = 0;
141 const candidate = candidate_storage[0..candidate_len :0];
142 switch (linux.errno(linux.access(candidate.ptr, linux.X_OK))) {
143 .SUCCESS => return candidate,
144 .ACCES => access_denied = true,
145 .NOENT, .NOTDIR => {},
146 else => return error.DescriptorIsolationFailed,
147 }
148 }
149 if (access_denied) return error.AccessDenied;
150 return error.FileNotFound;
151 }
152
153 const Descriptors = struct {
154 stdout_read: linux.fd_t = -1,
155 stdout_source: linux.fd_t = -1,
156 stderr_read: linux.fd_t = -1,
157 stderr_source: linux.fd_t = -1,
158 status_read: linux.fd_t = -1,
159 status_source: linux.fd_t = -1,
160 null_source: linux.fd_t = -1,
161
162 fn init() !Descriptors {
163 var self = Descriptors{};
164 errdefer self.deinit();
165 const stdout = try capturedPipe();
166 self.stdout_read = stdout.read;
167 self.stdout_source = stdout.source;
168 const stderr = try capturedPipe();
169 self.stderr_read = stderr.read;
170 self.stderr_source = stderr.source;
171 const status = try capturedPipe();
172 self.status_read = status.read;
173 self.status_source = status.source;
174 self.null_source = try nullSource();
175 return self;
176 }
177
178 fn closeSources(self: *Descriptors) void {
179 closeDescriptor(&self.null_source);
180 closeDescriptor(&self.status_source);
181 closeDescriptor(&self.stderr_source);
182 closeDescriptor(&self.stdout_source);
183 }
184
185 fn closeStatus(self: *Descriptors) void {
186 closeDescriptor(&self.status_read);
187 }
188
189 fn takeStdout(self: *Descriptors) linux.fd_t {
190 const descriptor = self.stdout_read;
191 self.stdout_read = -1;
192 return descriptor;
193 }
194
195 fn takeStderr(self: *Descriptors) linux.fd_t {
196 const descriptor = self.stderr_read;
197 self.stderr_read = -1;
198 return descriptor;
199 }
200
201 fn deinit(self: *Descriptors) void {
202 self.closeSources();
203 self.closeStatus();
204 closeDescriptor(&self.stderr_read);
205 closeDescriptor(&self.stdout_read);
206 self.* = undefined;
207 }
208 };
209
210 const CapturedPipe = struct {
211 read: linux.fd_t,
212 source: linux.fd_t,
213 };
214
215 fn capturedPipe() !CapturedPipe {
216 var descriptors: [2]linux.fd_t = undefined;
217 var interruptions: u8 = 0;
218 while (interruptions < syscall_interruptions_max) {
219 const result = linux.pipe2(&descriptors, .{ .CLOEXEC = true });
220 switch (linux.errno(result)) {
221 .SUCCESS => break,
222 .INTR => interruptions += 1,
223 else => return error.DescriptorIsolationFailed,
224 }
225 } else return error.DescriptorIsolationFailed;
226 errdefer {
227 _ = linux.close(descriptors[1]);
228 _ = linux.close(descriptors[0]);
229 }
230
231 const source = try promoteSource(descriptors[1]);
232 if (source != descriptors[1]) _ = linux.close(descriptors[1]);
233 return .{ .read = descriptors[0], .source = source };
234 }
235
236 fn nullSource() !linux.fd_t {
237 const descriptor_result = linux.open(
238 "/dev/null",
239 .{ .ACCMODE = .RDONLY, .CLOEXEC = true },
240 0,
241 );
242 if (linux.errno(descriptor_result) != .SUCCESS) {
243 return error.DescriptorIsolationFailed;
244 }
245 const descriptor: linux.fd_t = @intCast(descriptor_result);
246 errdefer _ = linux.close(descriptor);
247 const source = try promoteSource(descriptor);
248 if (source != descriptor) _ = linux.close(descriptor);
249 return source;
250 }
251
252 fn promoteSource(descriptor: linux.fd_t) !linux.fd_t {
253 if (descriptor >= child_source_min) return descriptor;
254 var interruptions: u8 = 0;
255 while (interruptions < syscall_interruptions_max) {
256 const result = linux.fcntl(
257 descriptor,
258 linux.F.DUPFD_CLOEXEC,
259 child_source_min,
260 );
261 switch (linux.errno(result)) {
262 .SUCCESS => return @intCast(result),
263 .INTR => interruptions += 1,
264 else => return error.DescriptorIsolationFailed,
265 }
266 }
267 return error.DescriptorIsolationFailed;
268 }
269
270 fn launchChild(
271 parent_id: linux.pid_t,
272 descriptors: Descriptors,
273 executable: [:0]const u8,
274 argv: [:null]?[*:0]const u8,
275 environment: std.process.Environ.PosixBlock,
276 ) noreturn {
277 if (!armParentDeath() or linux.getppid() != parent_id) {
278 launchFailure(descriptors.status_source);
279 }
280 if (!duplicateTo(descriptors.null_source, linux.STDIN_FILENO, false)) {
281 launchFailure(descriptors.status_source);
282 }
283 if (!duplicateTo(descriptors.stdout_source, linux.STDOUT_FILENO, false)) {
284 launchFailure(descriptors.status_source);
285 }
286 if (!duplicateTo(descriptors.stderr_source, linux.STDERR_FILENO, false)) {
287 launchFailure(descriptors.status_source);
288 }
289 if (!duplicateTo(descriptors.status_source, 3, true)) {
290 launchFailure(descriptors.status_source);
291 }
292 if (linux.errno(linux.setpgid(0, 0)) != .SUCCESS) launchFailure(3);
293
294 const last: linux.fd_t = @bitCast(@as(u32, std.math.maxInt(u32)));
295 const close_flags: linux.CLOSE_RANGE = @bitCast(@as(u32, 0));
296 if (linux.errno(linux.close_range(4, last, close_flags)) != .SUCCESS) {
297 launchFailure(3);
298 }
299 _ = linux.execve(executable.ptr, argv.ptr, environment.slice.ptr);
300 launchFailure(3);
301 }
302
303 fn armParentDeath() bool {
304 const result = linux.prctl(
305 @backingInt(linux.PR.SET_PDEATHSIG),
306 @backingInt(linux.SIG.KILL),
307 0,
308 0,
309 0,
310 );
311 return linux.errno(result) == .SUCCESS;
312 }
313
314 fn duplicateTo(
315 source: linux.fd_t,
316 target: linux.fd_t,
317 close_on_exec: bool,
318 ) bool {
319 const flags: u32 = if (close_on_exec)
320 @bitCast(linux.O{ .CLOEXEC = true })
321 else
322 0;
323 var interruptions: u8 = 0;
324 while (interruptions < syscall_interruptions_max) {
325 const result = linux.dup3(source, target, flags);
326 switch (linux.errno(result)) {
327 .SUCCESS => return true,
328 .INTR => interruptions += 1,
329 else => return false,
330 }
331 }
332 return false;
333 }
334
335 fn launchFailure(descriptor: linux.fd_t) noreturn {
336 const marker = [1]u8{1};
337 var interruptions: u8 = 0;
338 while (interruptions < syscall_interruptions_max) {
339 const result = linux.write(descriptor, &marker, marker.len);
340 switch (linux.errno(result)) {
341 .SUCCESS => break,
342 .INTR => interruptions += 1,
343 else => break,
344 }
345 }
346 linux.exit_group(127);
347 }
348
349 fn launchSucceeded(descriptor: linux.fd_t) bool {
350 var marker: [1]u8 = undefined;
351 var interruptions: u8 = 0;
352 while (interruptions < syscall_interruptions_max) {
353 const result = linux.read(descriptor, &marker, marker.len);
354 switch (linux.errno(result)) {
355 .SUCCESS => return result == 0,
356 .INTR => interruptions += 1,
357 else => return false,
358 }
359 }
360 return false;
361 }
362
363 fn terminateAndReap(child_id: linux.pid_t) void {
364 _ = linux.kill(-child_id, .KILL);
365 _ = linux.kill(child_id, .KILL);
366 var status: i32 = undefined;
367 while (true) {
368 const result = linux.wait4(child_id, &status, 0, null);
369 switch (linux.errno(result)) {
370 .SUCCESS, .CHILD => return,
371 .INTR => continue,
372 else => return,
373 }
374 }
375 }
376
377 fn closeDescriptor(descriptor: *linux.fd_t) void {
378 if (descriptor.* >= 0) _ = linux.close(descriptor.*);
379 descriptor.* = -1;
380 }
381
382 test "captured descriptors fit the standard launch capacity" {
383 if (comptime native_os != .linux) return error.SkipZigTest;
384 const limits = try std.posix.getrlimit(.NOFILE);
385 if (limits.max < 10) return error.SkipZigTest;
386
387 const fork_result = linux.fork();
388 if (linux.errno(fork_result) != .SUCCESS) {
389 return error.DescriptorCapacityForkFailed;
390 }
391 if (fork_result == 0) capacityChild(limits.max);
392
393 var status: i32 = undefined;
394 while (true) {
395 const wait_result = linux.wait4(
396 @intCast(fork_result),
397 &status,
398 0,
399 null,
400 );
401 switch (linux.errno(wait_result)) {
402 .SUCCESS => break,
403 .INTR => continue,
404 else => return error.DescriptorCapacityWaitFailed,
405 }
406 }
407 const decoded: u32 = @bitCast(status);
408 try std.testing.expect(linux.W.IFEXITED(decoded));
409 try std.testing.expectEqual(@as(u8, 0), linux.W.EXITSTATUS(decoded));
410 }
411
412 fn capacityChild(maximum: u64) noreturn {
413 const last: linux.fd_t = @bitCast(@as(u32, std.math.maxInt(u32)));
414 const close_flags: linux.CLOSE_RANGE = @bitCast(@as(u32, 0));
415 if (linux.errno(linux.close_range(3, last, close_flags)) != .SUCCESS) {
416 linux.exit_group(201);
417 }
418 const limited = linux.rlimit{ .cur = 10, .max = maximum };
419 if (linux.errno(linux.setrlimit(.NOFILE, &limited)) != .SUCCESS) {
420 linux.exit_group(202);
421 }
422 var descriptors = Descriptors.init() catch linux.exit_group(203);
423 descriptors.deinit();
424 linux.exit_group(0);
425 }