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 }