lib/sys/src/process/control.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const builtin = @import("builtin");
3 const sys = @import("../root.zig");
4
5 const linux = std.os.linux;
6 const posix = std.posix;
7 const system = posix.system;
8 const native_os = builtin.os.tag;
9 const is_darwin = switch (native_os) {
10 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => true,
11 else => false,
12 };
13
14 pub const required_capabilities = sys.capabilities.host(
15 &.{ .process, .environment, .threads },
16 );
17
18 pub const Child = std.process.Child;
19 pub const Init = std.process.Init;
20 pub const MinimalInit = std.process.Init.Minimal;
21 pub const Args = std.process.Args;
22 pub const Environ = std.process.Environ;
23 pub const SpawnOptions = std.process.SpawnOptions;
24 pub const SpawnError = std.process.SpawnError;
25 pub const Termination = Child.Term;
26 pub const WaitError = Child.WaitError;
27 pub const RunOptions = std.process.RunOptions;
28 pub const RunResult = std.process.RunResult;
29 pub const RunError = std.process.RunError;
30 pub const ExecutablePathAllocError = std.process.ExecutablePathAllocError;
31 pub const ProcessId = linux.pid_t;
32 pub const ThreadId = linux.pid_t;
33 pub const UserId = linux.uid_t;
34 pub const GroupId = linux.gid_t;
35
36 pub const AuxiliaryKey = enum {
37 hardware_capabilities,
38 hardware_capabilities_2,
39 };
40
41 pub const AuxiliaryValueError = error{UnsupportedPlatform};
42 pub const SystemControlError = error{
43 UnsupportedPlatform,
44 NotFound,
45 PermissionDenied,
46 BufferTooSmall,
47 QueryFailed,
48 };
49 pub const KernelReleaseError = error{ UnsupportedPlatform, BufferTooSmall };
50
51 pub const ResourceUsageSource = enum {
52 getrusage_self,
53 wait4_rusage,
54 };
55
56 pub const ResourceUsage = struct {
57 source: ?ResourceUsageSource = null,
58 maxrss_kib: ?i64 = null,
59 user_s: ?f64 = null,
60 system_s: ?f64 = null,
61 minor_page_faults: ?u64 = null,
62 major_page_faults: ?u64 = null,
63 voluntary_context_switches: ?u64 = null,
64 involuntary_context_switches: ?u64 = null,
65 };
66
67 pub const WaitNoHangOutcome = struct {
68 term: Termination,
69 usage: ResourceUsage,
70 };
71
72 pub const ChildSignal = enum {
73 terminate,
74 kill,
75 pause_child,
76 resume_child,
77 };
78
79 pub const ChildSignalPolicy = enum {
80 unsupported,
81 linux_syscall,
82 posix_host,
83 };
84
85 pub const ProcessIdentityPolicy = enum {
86 unsupported,
87 linux_syscall,
88 posix_host,
89 };
90
91 pub const ChildPeekPolicy = enum {
92 unsupported,
93 linux_waitid,
94 darwin_waitid,
95 };
96
97 pub const ChildSignalError = error{
98 UnsupportedPlatform,
99 ProcessNotFound,
100 PermissionDenied,
101 InvalidSignal,
102 SignalFailed,
103 };
104
105 pub const ProcessIdentityError = error{
106 UnsupportedPlatform,
107 };
108
109 pub const ProcessGroupError = error{
110 UnsupportedPlatform,
111 ProcessNotFound,
112 PermissionDenied,
113 };
114
115 pub const SubreaperError = error{
116 UnsupportedPlatform,
117 SubreaperFailed,
118 };
119
120 pub const ForkError = error{
121 UnsupportedPlatform,
122 SystemResources,
123 ForkFailed,
124 };
125
126 pub const ForkResult = union(enum) {
127 child,
128 parent: ProcessId,
129 };
130
131 pub const ExecDescriptorError = error{
132 UnsupportedPlatform,
133 InvalidDescriptor,
134 AccessDenied,
135 InvalidExecutable,
136 ExecFailed,
137 };
138 pub const ExecUnderStackError = error{
139 UnsupportedPlatform,
140 InvalidLimit,
141 PermissionDenied,
142 PersonaFailed,
143 AccessDenied,
144 InvalidExecutable,
145 ExecFailed,
146 };
147
148 pub const WaitDirectError = error{
149 UnsupportedPlatform,
150 ChildAlreadyReaped,
151 ChildWaitFailed,
152 };
153
154 pub const DetachPolicy = enum {
155 unsupported,
156 linux_cloexec_process_group,
157 };
158
159 pub const DetachOptions = struct {
160 argv: []const []const u8,
161 cwd: Child.Cwd = .inherit,
162 environ_map: ?*const Environ.Map = null,
163 };
164
165 pub const DetachError = SpawnError || error{
166 UnsupportedPlatform,
167 DescriptorIsolationFailed,
168 };
169 pub const DescriptorIsolationError = error{
170 UnsupportedPlatform,
171 DescriptorIsolationFailed,
172 };
173 pub const ReapHandoffError = sys.thread.SpawnError || error{
174 MissingChildId,
175 };
176
177 pub fn spawn(io: anytype, options: SpawnOptions) SpawnError!Child {
178 return std.process.spawn(io, options);
179 }
180
181 pub fn fork() ForkError!ForkResult {
182 if (comptime native_os != .linux) return error.UnsupportedPlatform;
183 const result = linux.fork();
184 return switch (linux.errno(result)) {
185 .SUCCESS => if (result == 0) .child else .{ .parent = @intCast(result) },
186 .AGAIN, .NOMEM => error.SystemResources,
187 else => error.ForkFailed,
188 };
189 }
190
191 pub fn execDescriptor(
192 descriptor: posix.fd_t,
193 argv: [*:null]const ?[*:0]const u8,
194 environment: [*:null]const ?[*:0]const u8,
195 ) ExecDescriptorError!void {
196 if (comptime native_os != .linux) return error.UnsupportedPlatform;
197 const result = linux.execveat(
198 descriptor,
199 "",
200 argv,
201 environment,
202 .{ .SYMLINK_NOFOLLOW = true, .EMPTY_PATH = true },
203 );
204 return switch (linux.errno(result)) {
205 .BADF => error.InvalidDescriptor,
206 .ACCES => error.AccessDenied,
207 .NOEXEC => error.InvalidExecutable,
208 else => error.ExecFailed,
209 };
210 }
211
212 /// The Linux persona value `ADDR_NO_RANDOMIZE`, set by exec under a stack
213 /// limit, gives a process carrying it its initial stack at one address on every
214 /// run.
215 const persona_no_randomize: usize = 0x0040000;
216
217 /// Caps this process at `stack_bytes` of stack and then hands it over to the
218 /// program at `path`. The initial stack lands at the address the kernel picks
219 /// when no randomization applies, so a harness that has to show a program stays
220 /// inside a stack budget gets the same answer on every run. Linux otherwise
221 /// shifts the initial stack pointer down by a random amount of up to eight
222 /// kilobytes before the program executes an instruction, in `arch_align_stack`,
223 /// which leaves a program at a fixed limit surviving one run and overflowing
224 /// the next. The order runs persona, then the soft and hard limit in one call,
225 /// and then the exec. A return means the exec failed, and both settings remain
226 /// in force on the calling process, so a caller with work left to do makes this
227 /// call inside a forked child. A zero limit fails with `InvalidLimit`, and a
228 /// host other than Linux with `UnsupportedPlatform`.
229 pub fn execUnderStack(
230 path: [*:0]const u8,
231 argv: [*:null]const ?[*:0]const u8,
232 environment: [*:null]const ?[*:0]const u8,
233 stack_bytes: u64,
234 ) ExecUnderStackError!void {
235 if (comptime native_os != .linux) return error.UnsupportedPlatform;
236 if (stack_bytes == 0) return error.InvalidLimit;
237 if (linux.errno(linux.syscall1(.personality, persona_no_randomize)) != .SUCCESS) {
238 return error.PersonaFailed;
239 }
240 const limit = linux.rlimit{ .cur = stack_bytes, .max = stack_bytes };
241 switch (linux.errno(linux.setrlimit(.STACK, &limit))) {
242 .SUCCESS => {},
243 .INVAL => return error.InvalidLimit,
244 .PERM => return error.PermissionDenied,
245 else => return error.InvalidLimit,
246 }
247 const result = linux.execve(path, argv, environment);
248 return switch (linux.errno(result)) {
249 .ACCES => error.AccessDenied,
250 .NOEXEC => error.InvalidExecutable,
251 else => error.ExecFailed,
252 };
253 }
254
255 pub fn auxiliaryValue(key: AuxiliaryKey) AuxiliaryValueError!usize {
256 if (comptime native_os != .linux) return error.UnsupportedPlatform;
257 return linux.getauxval(switch (key) {
258 .hardware_capabilities => std.elf.AT_HWCAP,
259 .hardware_capabilities_2 => std.elf.AT_HWCAP2,
260 });
261 }
262
263 /// Returns the GNU build-id descriptor of the running executable, or null when its link wrote
264 /// none. The kernel passes the program headers in auxv, so no file is read and no syscall runs.
265 /// The slice borrows the mapped image for the life of the process.
266 pub fn buildId() BuildIdError!?[]const u8 {
267 if (comptime native_os != .linux) return error.UnsupportedPlatform;
268 const headers = posix.getSelfPhdrs();
269 const bias = for (headers) |header| {
270 if (header.type == .PHDR) break @intFromPtr(headers.ptr) -% header.vaddr;
271 } else return error.ProgramHeaderMissing;
272 for (headers) |header| {
273 if (header.type != .NOTE) continue;
274 const start: [*]const u8 = @ptrFromInt(bias +% header.vaddr);
275 const alignment: usize = if (header.@"align" == 8) 8 else 4;
276 const segment = start[0..header.filesz];
277 if (noteDescriptor(segment, alignment, std.elf.NT_GNU_BUILD_ID)) |id| return id;
278 }
279 return null;
280 }
281
282 pub const BuildIdError = error{ UnsupportedPlatform, ProgramHeaderMissing };
283
284 /// Returns the descriptor of the first `GNU` note of `kind` in one note segment. Entries pad
285 /// their name and descriptor to the segment's `alignment`. A truncated entry ends the walk.
286 pub fn noteDescriptor(segment: []const u8, alignment: usize, kind: u32) ?[]const u8 {
287 std.debug.assert(std.math.isPowerOfTwo(alignment));
288 std.debug.assert(alignment <= 8);
289 const header_bytes = @sizeOf(std.elf.Elf64_Nhdr);
290 var offset: usize = 0;
291 while (segment.len - offset >= header_bytes) {
292 const header = std.mem.bytesAsValue(
293 std.elf.Elf64_Nhdr,
294 segment[offset..][0..header_bytes],
295 ).*;
296 const name_start = offset + header_bytes;
297 if (header.n_namesz > segment.len - name_start) return null;
298 const name_end = name_start + header.n_namesz;
299 const descriptor_start = std.mem.alignForward(usize, name_end, alignment);
300 if (descriptor_start > segment.len or
301 header.n_descsz > segment.len - descriptor_start) return null;
302 const name = segment[name_start..][0..header.n_namesz];
303 if (header.n_type == kind and std.mem.eql(u8, name, "GNU\x00"))
304 return segment[descriptor_start..][0..header.n_descsz];
305 const next = std.mem.alignForward(usize, descriptor_start + header.n_descsz, alignment);
306 if (next > segment.len) return null;
307 offset = next;
308 }
309 return null;
310 }
311
312 pub fn systemControl(
313 comptime name: [:0]const u8,
314 output: anytype,
315 ) SystemControlError!usize {
316 if (comptime !is_darwin) return error.UnsupportedPlatform;
317 const pointer = @typeInfo(@TypeOf(output)).pointer;
318 var byte_len: usize = @sizeOf(pointer.child);
319 const rc = system.sysctlbyname(name, output, &byte_len, null, 0);
320 return switch (posix.errno(rc)) {
321 .SUCCESS => byte_len,
322 .NOENT => error.NotFound,
323 .ACCES, .PERM => error.PermissionDenied,
324 .NOMEM => error.BufferTooSmall,
325 else => error.QueryFailed,
326 };
327 }
328
329 pub fn kernelRelease(storage: []u8) KernelReleaseError![]const u8 {
330 if (comptime native_os == .windows or native_os == .wasi or native_os == .freestanding) {
331 return error.UnsupportedPlatform;
332 }
333 const identity = posix.uname();
334 const release = std.mem.sliceTo(&identity.release, 0);
335 if (release.len > storage.len) return error.BufferTooSmall;
336 @memcpy(storage[0..release.len], release);
337 return storage[0..release.len];
338 }
339
340 pub fn detachPolicy() DetachPolicy {
341 return switch (comptime native_os) {
342 .linux => .linux_cloexec_process_group,
343 else => .unsupported,
344 };
345 }
346
347 pub fn spawnDetached(io: anytype, options: DetachOptions) DetachError!Child {
348 if (comptime detachPolicy() == .unsupported) return error.UnsupportedPlatform;
349 std.debug.assert(options.argv.len != 0);
350 try isolateChildDescriptors();
351 return try spawn(io, .{
352 .argv = options.argv,
353 .cwd = options.cwd,
354 .environ_map = options.environ_map,
355 .stdin = .ignore,
356 .stdout = .ignore,
357 .stderr = .ignore,
358 .pgid = 0,
359 });
360 }
361
362 pub fn handoffReap(
363 child: *Child,
364 ) ReapHandoffError!void {
365 const child_id = child.id orelse
366 return error.MissingChildId;
367 const reaper = try sys.thread.spawn(
368 reapChildId,
369 .{child_id},
370 );
371 child.id = null;
372 reaper.detach();
373 }
374
375 fn reapChildId(child_id: Child.Id) void {
376 if (comptime native_os == .windows or
377 native_os == .wasi)
378 {
379 return;
380 }
381 var status: i32 = undefined;
382 var child_usage: posix.rusage = undefined;
383 while (true) {
384 const rc = system.wait4(
385 child_id,
386 &status,
387 0,
388 &child_usage,
389 );
390 switch (posix.errno(rc)) {
391 .SUCCESS => return,
392 .INTR => continue,
393 else => return,
394 }
395 }
396 }
397
398 pub fn isolateChildDescriptors() DescriptorIsolationError!void {
399 return switch (comptime native_os) {
400 .linux => isolateChildDescriptorsLinux(),
401 .macos => isolateChildDescriptorsDarwin(),
402 else => error.UnsupportedPlatform,
403 };
404 }
405
406 fn isolateChildDescriptorsLinux() DescriptorIsolationError!void {
407 if (comptime native_os != .linux) return error.UnsupportedPlatform;
408 const last: linux.fd_t = @bitCast(
409 @as(u32, std.math.maxInt(u32)),
410 );
411 const close_on_exec: linux.CLOSE_RANGE =
412 @bitCast(@as(u32, 1 << 2));
413 const rc = linux.close_range(3, last, close_on_exec);
414 return switch (linux.errno(rc)) {
415 .SUCCESS => {},
416 .NOSYS => error.UnsupportedPlatform,
417 else => error.DescriptorIsolationFailed,
418 };
419 }
420
421 const DescriptorIsolationStatus = enum {
422 success,
423 absent,
424 interrupted,
425 failed,
426 };
427
428 const DarwinDescriptorIsolationOperation = struct {
429 fn mark(
430 _: *@This(),
431 descriptor: posix.fd_t,
432 ) DescriptorIsolationStatus {
433 const flags = system.fcntl(descriptor, posix.F.GETFD, @as(usize, 0));
434 const current: usize = switch (posix.errno(flags)) {
435 .SUCCESS => @intCast(flags),
436 .INTR => return .interrupted,
437 .BADF => return .absent,
438 else => return .failed,
439 };
440 const rc = system.fcntl(descriptor, posix.F.SETFD, current | posix.FD_CLOEXEC);
441 return switch (posix.errno(rc)) {
442 .SUCCESS => .success,
443 .INTR => .interrupted,
444 .BADF => .absent,
445 else => .failed,
446 };
447 }
448 };
449
450 fn isolateChildDescriptorsDarwin() DescriptorIsolationError!void {
451 if (comptime native_os != .macos) return error.UnsupportedPlatform;
452 const limit = darwin.getdtablesize();
453 if (limit < 3) return error.DescriptorIsolationFailed;
454 var operation: DarwinDescriptorIsolationOperation = .{};
455 return isolateChildDescriptorsWith(&operation, 3, limit);
456 }
457
458 fn isolateChildDescriptorsWith(
459 operation: anytype,
460 first: posix.fd_t,
461 limit: posix.fd_t,
462 ) DescriptorIsolationError!void {
463 std.debug.assert(first >= 0);
464 std.debug.assert(limit >= first);
465 var descriptor = first;
466 while (descriptor < limit) : (descriptor += 1) {
467 while (true) switch (operation.mark(descriptor)) {
468 .success, .absent => break,
469 .interrupted => continue,
470 .failed => return error.DescriptorIsolationFailed,
471 };
472 }
473 }
474
475 const darwin = struct {
476 extern "c" fn getdtablesize() c_int;
477 extern "c" fn getpgid(process_id: c_int) c_int;
478 extern "c" fn waitid(kind: c_uint, id: c_uint, info: *std.c.siginfo_t, flags: c_int) c_int;
479
480 const pid: c_uint = 1;
481 const exited: c_int = 0x4;
482 const nowait: c_int = 0x20;
483 };
484
485 pub fn processGroupId(process_id: ProcessId) ProcessGroupError!ProcessId {
486 if (process_id <= 0) return error.ProcessNotFound;
487 return switch (comptime processIdentityPolicy()) {
488 .unsupported => error.UnsupportedPlatform,
489 .linux_syscall => processGroupIdInspected(process_id),
490 .posix_host => processGroupIdPosix(process_id),
491 };
492 }
493
494 pub fn processAlive(process_id: ProcessId) ProcessGroupError!bool {
495 _ = processGroupId(process_id) catch |err| switch (err) {
496 error.ProcessNotFound => return false,
497 else => return err,
498 };
499 return true;
500 }
501
502 pub fn createOwnProcessGroup() ProcessGroupError!void {
503 const outcome = if (comptime native_os == .linux)
504 linux.errno(linux.setpgid(0, 0))
505 else if (comptime native_os == .macos)
506 std.c.errno(std.c.setpgid(0, 0))
507 else
508 return error.UnsupportedPlatform;
509 return switch (outcome) {
510 .SUCCESS => {},
511 .ACCES, .PERM => error.PermissionDenied,
512 else => error.UnsupportedPlatform,
513 };
514 }
515
516 /// Ensures a descendant whose parent died still reports its exit to the
517 /// supervisor by making the calling process the Linux child subreaper, so
518 /// descendants orphaned below it reparent to it and stay reapable. Other
519 /// targets refuse with `UnsupportedPlatform`.
520 pub fn becomeChildSubreaper() SubreaperError!void {
521 if (comptime native_os != .linux) return error.UnsupportedPlatform;
522 return switch (linux.errno(linux.prctl(
523 @backingInt(linux.PR.SET_CHILD_SUBREAPER),
524 1,
525 0,
526 0,
527 0,
528 ))) {
529 .SUCCESS => {},
530 else => error.SubreaperFailed,
531 };
532 }
533
534 pub fn processGroupAlive(group_id: ProcessId) ProcessGroupError!bool {
535 if (group_id <= 1) return error.ProcessNotFound;
536 return switch (comptime processIdentityPolicy()) {
537 .unsupported => error.UnsupportedPlatform,
538 .linux_syscall => switch (linux.errno(linux.kill(
539 -group_id,
540 @fromBackingInt(@intCast(0)),
541 ))) {
542 .SUCCESS => true,
543 .SRCH => false,
544 .PERM => true,
545 else => error.UnsupportedPlatform,
546 },
547 .posix_host => switch (posix.errno(system.kill(
548 -group_id,
549 @as(system.SIG, @fromBackingInt(@intCast(0))),
550 ))) {
551 .SUCCESS, .PERM => true,
552 .SRCH => false,
553 else => error.UnsupportedPlatform,
554 },
555 };
556 }
557
558 fn processGroupIdInspected(process_id: ProcessId) ProcessGroupError!ProcessId {
559 return sys.process.inspect.group(process_id) catch |err| switch (err) {
560 error.ProcessNotFound => error.ProcessNotFound,
561 error.PermissionDenied => error.PermissionDenied,
562 else => error.UnsupportedPlatform,
563 };
564 }
565
566 fn processGroupIdPosix(process_id: ProcessId) ProcessGroupError!ProcessId {
567 if (comptime native_os == .windows or native_os == .wasi) return error.UnsupportedPlatform;
568 if (comptime native_os == .macos) return processGroupIdInspected(process_id);
569 const rc = if (comptime is_darwin)
570 darwin.getpgid(process_id)
571 else
572 system.getpgid(process_id);
573 return switch (posix.errno(rc)) {
574 .SUCCESS => @intCast(rc),
575 .SRCH => error.ProcessNotFound,
576 else => error.UnsupportedPlatform,
577 };
578 }
579
580 pub fn wait(child: *Child, io: anytype) WaitError!Termination {
581 return child.wait(io);
582 }
583
584 pub fn waitDirect(child_id: ProcessId) WaitDirectError!Termination {
585 if (comptime native_os == .windows or
586 native_os == .wasi or
587 native_os == .freestanding)
588 {
589 return error.UnsupportedPlatform;
590 }
591 var status: i32 = undefined;
592 var child_usage: posix.rusage = undefined;
593 while (true) {
594 const result = system.wait4(child_id, &status, 0, &child_usage);
595 switch (posix.errno(result)) {
596 .SUCCESS => return statusToTerm(@bitCast(status)),
597 .INTR => continue,
598 .CHILD => return error.ChildAlreadyReaped,
599 else => return error.ChildWaitFailed,
600 }
601 }
602 }
603
604 pub fn waitNoHang(child_id: Child.Id) !?WaitNoHangOutcome {
605 if (comptime native_os == .windows or native_os == .wasi) return error.UnsupportedPlatform;
606 var status: i32 = undefined;
607 var child_usage: posix.rusage = undefined;
608 const rc = system.wait4(child_id, &status, posix.W.NOHANG, &child_usage);
609 return switch (posix.errno(rc)) {
610 .SUCCESS => if (rc == 0) null else .{
611 .term = statusToTerm(@bitCast(status)),
612 .usage = resourceUsage(.wait4_rusage, child_usage),
613 },
614 .INTR => null,
615 else => |err| return posix.unexpectedErrno(err),
616 };
617 }
618
619 pub fn currentResourceUsage() ResourceUsage {
620 if (comptime native_os == .windows or
621 native_os == .wasi or
622 native_os == .freestanding)
623 {
624 return .{};
625 }
626 return resourceUsage(
627 .getrusage_self,
628 posix.getrusage(posix.rusage.SELF),
629 );
630 }
631
632 fn resourceUsage(source: ResourceUsageSource, usage: posix.rusage) ResourceUsage {
633 return .{
634 .source = source,
635 .maxrss_kib = normalizeMaxRss(usage.maxrss, is_darwin),
636 .user_s = timevalSeconds(usage.utime),
637 .system_s = timevalSeconds(usage.stime),
638 .minor_page_faults = linuxResourceCount(usage.minflt),
639 .major_page_faults = linuxResourceCount(usage.majflt),
640 .voluntary_context_switches = linuxResourceCount(usage.nvcsw),
641 .involuntary_context_switches = linuxResourceCount(usage.nivcsw),
642 };
643 }
644
645 fn normalizeMaxRss(value: anytype, darwin_units: bool) ?i64 {
646 if (value < 0) return null;
647 const nonnegative: i64 = @intCast(value);
648 return if (darwin_units) @divFloor(nonnegative, 1024) else nonnegative;
649 }
650
651 pub fn peekNoHang(child_id: Child.Id) !?Termination {
652 return switch (comptime childPeekPolicy()) {
653 .unsupported => error.UnsupportedPlatform,
654 .linux_waitid => peekLinux(child_id),
655 .darwin_waitid => peekDarwin(child_id),
656 };
657 }
658
659 fn peekLinux(child_id: Child.Id) !?Termination {
660 if (comptime native_os != .linux) return error.UnsupportedPlatform;
661 var info: linux.siginfo_t = std.mem.zeroes(linux.siginfo_t);
662 while (true) {
663 const flags = linux.W.EXITED | linux.W.NOHANG | linux.W.NOWAIT;
664 switch (linux.errno(linux.waitid(.PID, child_id, &info, flags, null))) {
665 .SUCCESS => {
666 if (info.fields.common.first.piduid.pid == 0) return null;
667 const status: u32 = @bitCast(
668 info.fields.common.second.sigchld.status,
669 );
670 const code: linux.CLD = @fromBackingInt(@intCast(info.code));
671 return switch (code) {
672 .EXITED => .{ .exited = @truncate(status) },
673 .KILLED, .DUMPED => .{
674 .signal = @fromBackingInt(@intCast(status)),
675 },
676 .TRAPPED, .STOPPED => .{
677 .stopped = @fromBackingInt(@intCast(status)),
678 },
679 _, .CONTINUED => .{ .unknown = status },
680 };
681 },
682 .INTR => return null,
683 .CHILD => return error.ProcessNotFound,
684 else => |err| return posix.unexpectedErrno(err),
685 }
686 }
687 }
688
689 pub fn childPeekPolicy() ChildPeekPolicy {
690 return switch (native_os) {
691 .linux => .linux_waitid,
692 .macos => .darwin_waitid,
693 else => .unsupported,
694 };
695 }
696
697 fn peekDarwin(child_id: Child.Id) !?Termination {
698 if (comptime !is_darwin) return error.UnsupportedPlatform;
699 var info: std.c.siginfo_t = std.mem.zeroes(std.c.siginfo_t);
700 const flags = darwin.exited | std.c.W.NOHANG | darwin.nowait;
701 switch (posix.errno(darwin.waitid(darwin.pid, @intCast(child_id), &info, flags))) {
702 .SUCCESS => {
703 if (info.pid == 0) return null;
704 const status: u32 = @bitCast(info.status);
705 return darwinTermination(info.code, status);
706 },
707 .INTR => return null,
708 .CHILD => return error.ProcessNotFound,
709 else => |err| return posix.unexpectedErrno(err),
710 }
711 }
712
713 fn darwinTermination(code: c_int, status: u32) Termination {
714 return switch (code) {
715 1 => .{ .exited = @truncate(status) },
716 2, 3 => .{ .signal = @fromBackingInt(@intCast(status & 0x7f)) },
717 else => .{ .unknown = status },
718 };
719 }
720
721 test "process waitid Darwin status preserves termination across extended exit bits" {
722 try std.testing.expectEqual(
723 Termination{ .exited = 143 },
724 darwinTermination(1, 0xff00008f),
725 );
726 try std.testing.expectEqual(
727 Termination{ .signal = .KILL },
728 darwinTermination(2, 0xff000009),
729 );
730 try std.testing.expectEqual(
731 Termination{ .signal = .TERM },
732 darwinTermination(3, 0xff00000f),
733 );
734 }
735
736 pub fn run(allocator: std.mem.Allocator, io: anytype, options: RunOptions) RunError!RunResult {
737 return std.process.run(allocator, io, options);
738 }
739
740 pub fn executableDirPathAlloc(allocator: std.mem.Allocator) ExecutablePathAllocError![]u8 {
741 return std.process.executableDirPathAlloc(std.Options.debug_io, allocator);
742 }
743
744 pub fn executablePathAlloc(allocator: std.mem.Allocator) ExecutablePathAllocError![:0]u8 {
745 return std.process.executablePathAlloc(std.Options.debug_io, allocator);
746 }
747
748 pub fn exit(code: u8) noreturn {
749 std.process.exit(code);
750 }
751
752 pub fn abort() noreturn {
753 std.process.abort();
754 }
755
756 pub fn exitCode(term: Termination) i64 {
757 return switch (term) {
758 .exited => |code| @intCast(code),
759 .signal => |sig| -@as(i64, @intCast(@backingInt(sig))),
760 .stopped => |sig| -@as(i64, @intCast(@backingInt(sig))),
761 .unknown => -1,
762 };
763 }
764
765 pub fn childSignalPolicy() ChildSignalPolicy {
766 return switch (native_os) {
767 .windows, .wasi => .unsupported,
768 .linux => .linux_syscall,
769 else => .posix_host,
770 };
771 }
772
773 pub fn processIdentityPolicy() ProcessIdentityPolicy {
774 return switch (native_os) {
775 .windows, .wasi, .freestanding => .unsupported,
776 .linux => .linux_syscall,
777 else => .posix_host,
778 };
779 }
780
781 pub fn signalChildId(child_id: Child.Id, signal: ChildSignal) ChildSignalError!void {
782 return switch (comptime childSignalPolicy()) {
783 .unsupported => error.UnsupportedPlatform,
784 .linux_syscall => signalChildIdLinux(child_id, signal),
785 .posix_host => signalChildIdPosix(child_id, signal),
786 };
787 }
788
789 pub fn signalChildGroup(child_id: Child.Id, signal: ChildSignal) ChildSignalError!void {
790 return switch (comptime childSignalPolicy()) {
791 .unsupported => error.UnsupportedPlatform,
792 .linux_syscall, .posix_host => signalChildId(-child_id, signal),
793 };
794 }
795
796 pub fn currentProcessId() ProcessIdentityError!ProcessId {
797 return switch (comptime processIdentityPolicy()) {
798 .unsupported => error.UnsupportedPlatform,
799 .linux_syscall => linux.getpid(),
800 .posix_host => currentProcessIdPosix(),
801 };
802 }
803
804 pub fn parentProcessId() ProcessIdentityError!ProcessId {
805 return switch (comptime processIdentityPolicy()) {
806 .unsupported => error.UnsupportedPlatform,
807 .linux_syscall => linux.getppid(),
808 .posix_host => parentProcessIdPosix(),
809 };
810 }
811
812 pub fn currentThreadId() ProcessIdentityError!ThreadId {
813 return switch (comptime processIdentityPolicy()) {
814 .unsupported => error.UnsupportedPlatform,
815 .linux_syscall => linux.gettid(),
816 .posix_host => currentThreadIdPosix(),
817 };
818 }
819
820 pub fn currentUserId() ProcessIdentityError!UserId {
821 return switch (comptime processIdentityPolicy()) {
822 .unsupported => error.UnsupportedPlatform,
823 .linux_syscall => linux.getuid(),
824 .posix_host => currentUserIdPosix(),
825 };
826 }
827
828 pub fn currentGroupId() ProcessIdentityError!GroupId {
829 return switch (comptime processIdentityPolicy()) {
830 .unsupported => error.UnsupportedPlatform,
831 .linux_syscall => linux.getgid(),
832 .posix_host => currentGroupIdPosix(),
833 };
834 }
835
836 pub fn effectiveUserId() ProcessIdentityError!UserId {
837 return switch (comptime processIdentityPolicy()) {
838 .unsupported => error.UnsupportedPlatform,
839 .linux_syscall => linux.geteuid(),
840 .posix_host => effectiveUserIdPosix(),
841 };
842 }
843
844 pub fn effectiveGroupId() ProcessIdentityError!GroupId {
845 return switch (comptime processIdentityPolicy()) {
846 .unsupported => error.UnsupportedPlatform,
847 .linux_syscall => linux.getegid(),
848 .posix_host => effectiveGroupIdPosix(),
849 };
850 }
851
852 pub fn requestTermination(child: *Child, io: anytype) void {
853 if (native_os == .windows or native_os == .wasi) {
854 killAndReap(child, io);
855 return;
856 }
857
858 const child_id = child.id orelse return;
859 signalChildId(child_id, .terminate) catch {};
860 }
861
862 pub fn forceKillChildId(child_id: Child.Id) void {
863 if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return;
864 signalChildId(child_id, .kill) catch {};
865 }
866
867 pub fn killAndReap(child: *Child, io: anytype) void {
868 if (child.id != null) child.kill(io);
869 }
870
871 fn signalChildIdLinux(child_id: Child.Id, signal: ChildSignal) ChildSignalError!void {
872 if (comptime native_os != .linux) return error.UnsupportedPlatform;
873 return childSignalErrorFromLinuxErrno(linux.errno(linux.kill(child_id, linuxChildSignal(signal))));
874 }
875
876 fn signalChildIdPosix(child_id: Child.Id, signal: ChildSignal) ChildSignalError!void {
877 if (comptime native_os == .windows or native_os == .wasi) return error.UnsupportedPlatform;
878 posix.kill(child_id, posixChildSignal(signal)) catch |err| switch (err) {
879 error.ProcessNotFound => return error.ProcessNotFound,
880 error.PermissionDenied => return error.PermissionDenied,
881 else => return error.SignalFailed,
882 };
883 }
884
885 fn currentProcessIdPosix() ProcessIdentityError!ProcessId {
886 if (!@hasDecl(system, "getpid") or @TypeOf(system.getpid) == void) return error.UnsupportedPlatform;
887 return @intCast(system.getpid());
888 }
889
890 fn parentProcessIdPosix() ProcessIdentityError!ProcessId {
891 if (!@hasDecl(system, "getppid") or @TypeOf(system.getppid) == void) return error.UnsupportedPlatform;
892 return @intCast(system.getppid());
893 }
894
895 fn currentThreadIdPosix() ProcessIdentityError!ThreadId {
896 if (!@hasDecl(system, "gettid") or @TypeOf(system.gettid) == void) return error.UnsupportedPlatform;
897 return @intCast(system.gettid());
898 }
899
900 fn currentUserIdPosix() ProcessIdentityError!UserId {
901 if (!@hasDecl(system, "getuid") or @TypeOf(system.getuid) == void) return error.UnsupportedPlatform;
902 return @intCast(system.getuid());
903 }
904
905 fn currentGroupIdPosix() ProcessIdentityError!GroupId {
906 if (!@hasDecl(system, "getgid") or @TypeOf(system.getgid) == void) return error.UnsupportedPlatform;
907 return @intCast(system.getgid());
908 }
909
910 fn effectiveUserIdPosix() ProcessIdentityError!UserId {
911 if (!@hasDecl(system, "geteuid") or @TypeOf(system.geteuid) == void) return error.UnsupportedPlatform;
912 return @intCast(system.geteuid());
913 }
914
915 fn effectiveGroupIdPosix() ProcessIdentityError!GroupId {
916 if (!@hasDecl(system, "getegid") or @TypeOf(system.getegid) == void) return error.UnsupportedPlatform;
917 return @intCast(system.getegid());
918 }
919
920 fn statusToTerm(status: u32) Termination {
921 return if (posix.W.IFEXITED(status))
922 .{ .exited = posix.W.EXITSTATUS(status) }
923 else if (posix.W.IFSIGNALED(status))
924 .{ .signal = posix.W.TERMSIG(status) }
925 else if (posix.W.IFSTOPPED(status))
926 .{ .stopped = posix.W.STOPSIG(status) }
927 else
928 .{ .unknown = status };
929 }
930
931 fn timevalSeconds(value: @TypeOf(@as(posix.rusage, undefined).utime)) f64 {
932 return @as(f64, @floatFromInt(value.sec)) + @as(f64, @floatFromInt(value.usec)) / 1_000_000.0;
933 }
934
935 fn linuxResourceCount(value: anytype) ?u64 {
936 if (comptime native_os != .linux) return null;
937 return if (value >= 0) @intCast(value) else null;
938 }
939
940 fn linuxChildSignal(signal: ChildSignal) linux.SIG {
941 return switch (signal) {
942 .terminate => .TERM,
943 .kill => .KILL,
944 .pause_child => .STOP,
945 .resume_child => .CONT,
946 };
947 }
948
949 fn posixChildSignal(signal: ChildSignal) posix.SIG {
950 return switch (signal) {
951 .terminate => .TERM,
952 .kill => .KILL,
953 .pause_child => .STOP,
954 .resume_child => .CONT,
955 };
956 }
957
958 fn childSignalErrorFromLinuxErrno(err: linux.E) ChildSignalError!void {
959 return switch (err) {
960 .SUCCESS => {},
961 .INVAL => error.InvalidSignal,
962 .PERM => error.PermissionDenied,
963 .SRCH => error.ProcessNotFound,
964 else => error.SignalFailed,
965 };
966 }
967
968 const DescriptorIsolationFixture = struct {
969 results: []const DescriptorIsolationStatus,
970 descriptors: [8]posix.fd_t = undefined,
971 attempts: usize = 0,
972
973 fn mark(self: *@This(), descriptor: posix.fd_t) DescriptorIsolationStatus {
974 std.debug.assert(self.attempts < self.results.len);
975 std.debug.assert(self.attempts < self.descriptors.len);
976 const index = self.attempts;
977 self.descriptors[index] = descriptor;
978 self.attempts += 1;
979 return self.results[index];
980 }
981 };
982
983 test "descriptor isolation scans a bounded range and retries interruptions" {
984 const results = [_]DescriptorIsolationStatus{
985 .absent,
986 .interrupted,
987 .success,
988 .success,
989 };
990 var operation: DescriptorIsolationFixture = .{ .results = &results };
991 try isolateChildDescriptorsWith(&operation, 3, 6);
992 const expected = [_]posix.fd_t{ 3, 4, 4, 5 };
993 try std.testing.expectEqualSlices(
994 posix.fd_t,
995 &expected,
996 operation.descriptors[0..operation.attempts],
997 );
998 }
999
1000 test "descriptor isolation maps operation failure" {
1001 const results = [_]DescriptorIsolationStatus{.failed};
1002 var operation: DescriptorIsolationFixture = .{ .results = &results };
1003 try std.testing.expectError(
1004 error.DescriptorIsolationFailed,
1005 isolateChildDescriptorsWith(&operation, 3, 4),
1006 );
1007 }
1008
1009 test "exec under stack sets the limit and the persona before it execs" {
1010 if (comptime native_os != .linux) return error.SkipZigTest;
1011 const fork_result = linux.fork();
1012 if (linux.errno(fork_result) != .SUCCESS) return error.StackLimitForkFailed;
1013 if (fork_result == 0) stackLimitChild();
1014
1015 var status: i32 = undefined;
1016 while (true) {
1017 const wait_result = linux.wait4(@intCast(fork_result), &status, 0, null);
1018 switch (linux.errno(wait_result)) {
1019 .SUCCESS => break,
1020 .INTR => continue,
1021 else => return error.StackLimitWaitFailed,
1022 }
1023 }
1024 const decoded: u32 = @bitCast(status);
1025 try std.testing.expect(linux.W.IFEXITED(decoded));
1026 try std.testing.expectEqual(@as(u8, 0), linux.W.EXITSTATUS(decoded));
1027 }
1028
1029 /// Runs as the child the test forks to prove both settings survive to the exec.
1030 /// The child hands the call a path no file occupies, which fails the exec at
1031 /// the last step and leaves the process alive with both settings applied, then
1032 /// reads both back from the kernel and exits zero when both hold, with a
1033 /// distinct code for each check that fails.
1034 fn stackLimitChild() noreturn {
1035 const bytes: u64 = 16 * 1024;
1036 const argv = [_:null]?[*:0]const u8{"/nonexistent/stack-limit-probe"};
1037 const environment = [_:null]?[*:0]const u8{};
1038 const failed = execUnderStack(argv[0].?, &argv, &environment, bytes);
1039 if (failed != error.ExecFailed) linux.exit_group(201);
1040 var read: linux.rlimit = undefined;
1041 if (linux.errno(linux.getrlimit(.STACK, &read)) != .SUCCESS) linux.exit_group(202);
1042 if (read.cur != bytes or read.max != bytes) linux.exit_group(203);
1043 const persona = linux.syscall1(.personality, 0xffffffff);
1044 if (persona & persona_no_randomize == 0) linux.exit_group(204);
1045 linux.exit_group(0);
1046 }
1047
1048 test "process note walk finds the GNU build-id past other notes at either alignment" {
1049 for ([_]usize{ 4, 8 }) |alignment| {
1050 var segment: [96]u8 = @splat(0);
1051 var writer = std.Io.Writer.fixed(&segment);
1052 const Entry = struct { name: []const u8, kind: u32, descriptor: []const u8 };
1053 const build_id = std.elf.NT_GNU_BUILD_ID;
1054 const entries = [_]Entry{
1055 .{ .name = "GNU\x00", .kind = 5, .descriptor = "property" },
1056 .{ .name = "Go\x00\x00", .kind = build_id, .descriptor = "other" },
1057 .{ .name = "GNU\x00", .kind = build_id, .descriptor = "0123456789abcdefghij" },
1058 };
1059 for (entries) |entry| {
1060 try writer.writeInt(u32, @intCast(entry.name.len), .little);
1061 try writer.writeInt(u32, @intCast(entry.descriptor.len), .little);
1062 try writer.writeInt(u32, entry.kind, .little);
1063 try writer.writeAll(entry.name);
1064 try writer.splatByteAll(0, padding(writer.end, alignment));
1065 try writer.writeAll(entry.descriptor);
1066 try writer.splatByteAll(0, padding(writer.end, alignment));
1067 }
1068 const used = segment[0..writer.end];
1069 try std.testing.expectEqualStrings(
1070 "0123456789abcdefghij",
1071 noteDescriptor(used, alignment, build_id).?,
1072 );
1073 const past_padding = used[0 .. used.len - 5];
1074 try std.testing.expectEqual(null, noteDescriptor(past_padding, alignment, build_id));
1075 try std.testing.expectEqual(null, noteDescriptor(used, alignment, 99));
1076 }
1077 }
1078
1079 fn padding(offset: usize, alignment: usize) usize {
1080 return std.mem.alignForward(usize, offset, alignment) - offset;
1081 }
1082
1083 test "process build-id reads the running image without failing" {
1084 if (native_os != .linux) return error.SkipZigTest;
1085 if (try buildId()) |id| try std.testing.expect(id.len != 0);
1086 }
1087
1088 test "process policy exports child type" {
1089 try std.testing.expect(Child == std.process.Child);
1090 }
1091
1092 test "process exitCode maps termination records" {
1093 try std.testing.expectEqual(@as(i64, 7), exitCode(.{ .exited = 7 }));
1094 try std.testing.expectEqual(@as(i64, -1), exitCode(.{ .unknown = 42 }));
1095 }
1096
1097 test "process resource counts are explicit Linux wait accounting" {
1098 const expected: ?u64 = if (native_os == .linux) 7 else null;
1099 try std.testing.expectEqual(expected, linuxResourceCount(@as(isize, 7)));
1100 try std.testing.expectEqual(@as(?u64, null), linuxResourceCount(@as(isize, -1)));
1101 }
1102
1103 test "process current resource usage names its accounting source" {
1104 const usage = currentResourceUsage();
1105 const expected: ?ResourceUsageSource = switch (native_os) {
1106 .windows, .wasi, .freestanding => null,
1107 else => .getrusage_self,
1108 };
1109 try std.testing.expectEqual(expected, usage.source);
1110 if (usage.source != null) {
1111 try std.testing.expect(usage.maxrss_kib.? > 0);
1112 try std.testing.expect(usage.user_s.? >= 0);
1113 try std.testing.expect(usage.system_s.? >= 0);
1114 }
1115 }
1116
1117 test "process peak rss normalizes Darwin bytes to kibibytes" {
1118 try std.testing.expectEqual(@as(?i64, 714_208), normalizeMaxRss(731_348_992, true));
1119 try std.testing.expectEqual(@as(?i64, 731_348_992), normalizeMaxRss(731_348_992, false));
1120 try std.testing.expectEqual(@as(?i64, null), normalizeMaxRss(-1, true));
1121 }
1122
1123 test "process child signal policy is explicit" {
1124 const expected: ChildSignalPolicy = switch (native_os) {
1125 .windows, .wasi => .unsupported,
1126 .linux => .linux_syscall,
1127 else => .posix_host,
1128 };
1129 try std.testing.expectEqual(expected, childSignalPolicy());
1130 }
1131
1132 test "process child signals cover supervisor lifecycle actions" {
1133 if (comptime native_os == .linux) {
1134 try std.testing.expectEqual(linux.SIG.STOP, linuxChildSignal(.pause_child));
1135 try std.testing.expectEqual(linux.SIG.CONT, linuxChildSignal(.resume_child));
1136 }
1137 if (comptime native_os != .windows and native_os != .wasi) {
1138 try std.testing.expectEqual(posix.SIG.STOP, posixChildSignal(.pause_child));
1139 try std.testing.expectEqual(posix.SIG.CONT, posixChildSignal(.resume_child));
1140 }
1141 }
1142
1143 test "process identity policy is explicit" {
1144 const expected: ProcessIdentityPolicy = switch (native_os) {
1145 .windows, .wasi, .freestanding => .unsupported,
1146 .linux => .linux_syscall,
1147 else => .posix_host,
1148 };
1149 try std.testing.expectEqual(expected, processIdentityPolicy());
1150 }
1151
1152 test "process child signal errno mapping is explicit" {
1153 if (comptime native_os != .linux) return;
1154 try childSignalErrorFromLinuxErrno(.SUCCESS);
1155 try std.testing.expectError(error.InvalidSignal, childSignalErrorFromLinuxErrno(.INVAL));
1156 try std.testing.expectError(error.PermissionDenied, childSignalErrorFromLinuxErrno(.PERM));
1157 try std.testing.expectError(error.ProcessNotFound, childSignalErrorFromLinuxErrno(.SRCH));
1158 try std.testing.expectError(error.SignalFailed, childSignalErrorFromLinuxErrno(.IO));
1159 }
1160
1161 test "process identity values are queryable" {
1162 if (processIdentityPolicy() == .unsupported) return error.SkipZigTest;
1163
1164 try std.testing.expect(try currentProcessId() > 0);
1165 _ = try parentProcessId();
1166 _ = try currentUserId();
1167 _ = try currentGroupId();
1168 _ = try effectiveUserId();
1169 _ = try effectiveGroupId();
1170 if (comptime native_os == .linux) try std.testing.expect(try currentThreadId() > 0);
1171 }
1172
1173 test "process executableDirPathAlloc returns a non-empty path" {
1174 const path = try executableDirPathAlloc(std.testing.allocator);
1175 defer std.testing.allocator.free(path);
1176 try std.testing.expect(path.len > 0);
1177 }
1178
1179 test "process detach policy is explicit" {
1180 const expected: DetachPolicy = switch (comptime native_os) {
1181 .linux => .linux_cloexec_process_group,
1182 else => .unsupported,
1183 };
1184 try std.testing.expectEqual(expected, detachPolicy());
1185 }
1186
1187 test "detached spawn leads its own process group and leaves no stdio pipe" {
1188 const testing = std.testing;
1189 if (comptime detachPolicy() == .unsupported) return error.SkipZigTest;
1190 var io_state = std.Io.Threaded.init(testing.allocator, .{});
1191 defer io_state.deinit();
1192 const io = io_state.io();
1193
1194 var child = try spawnDetached(io, .{
1195 .argv = &.{ "sh", "-c", "read line; exit 0" },
1196 });
1197 var reaped = false;
1198 defer if (!reaped) killAndReap(&child, io);
1199 const child_id = child.id orelse return error.MissingChildId;
1200
1201 try testing.expect(child.stdin == null);
1202 try testing.expect(child.stdout == null);
1203 try testing.expect(child.stderr == null);
1204 try testing.expectEqual(child_id, try processGroupId(child_id));
1205 try testing.expect(try processGroupId(child_id) !=
1206 try processGroupId(try currentProcessId()));
1207 try testing.expect(try processAlive(child_id));
1208
1209 forceKillChildId(child_id);
1210 _ = try wait(&child, io);
1211 reaped = true;
1212 }
1213
1214 test "detached spawn excludes an exact inherited descriptor" {
1215 const testing = std.testing;
1216 if (comptime native_os != .linux) return error.SkipZigTest;
1217 var tmp = testing.tmpDir(.{});
1218 defer tmp.cleanup();
1219 var identity_buffer: [64]u8 = undefined;
1220 const identity = try std.fmt.bufPrint(
1221 &identity_buffer,
1222 "tiny-detach-{s}",
1223 .{tmp.sub_path},
1224 );
1225 try tmp.dir.writeFile(std.Options.debug_io, .{
1226 .sub_path = "probe",
1227 .data = identity,
1228 });
1229 const probe = try posix.openat(
1230 tmp.dir.handle,
1231 "probe",
1232 .{},
1233 0,
1234 );
1235 defer _ = linux.close(probe);
1236 const before = linux.fcntl(probe, linux.F.GETFD, 0);
1237 try testing.expectEqual(linux.E.SUCCESS, linux.errno(before));
1238 try testing.expect(before & linux.FD_CLOEXEC == 0);
1239
1240 var script_buffer: [256]u8 = undefined;
1241 const script = try std.fmt.bufPrint(
1242 &script_buffer,
1243 "test \"$(cat /proc/self/fd/{d} 2>/dev/null)\" != '{s}'",
1244 .{ probe, identity },
1245 );
1246 var io_state = std.Io.Threaded.init(testing.allocator, .{});
1247 defer io_state.deinit();
1248 var child = try spawnDetached(io_state.io(), .{
1249 .argv = &.{ "/bin/sh", "-c", script },
1250 });
1251 var reaped = false;
1252 defer if (!reaped) killAndReap(&child, io_state.io());
1253 const term = try wait(&child, io_state.io());
1254 reaped = true;
1255 try testing.expectEqual(@as(i64, 0), exitCode(term));
1256 const after = linux.fcntl(probe, linux.F.GETFD, 0);
1257 try testing.expectEqual(linux.E.SUCCESS, linux.errno(after));
1258 try testing.expect(after & linux.FD_CLOEXEC != 0);
1259 const seek = linux.lseek(probe, 0, linux.SEEK.SET);
1260 try testing.expectEqual(linux.E.SUCCESS, linux.errno(seek));
1261 var read_buffer: [identity_buffer.len]u8 = undefined;
1262 const read = linux.read(probe, &read_buffer, read_buffer.len);
1263 try testing.expectEqual(linux.E.SUCCESS, linux.errno(read));
1264 try testing.expectEqualStrings(
1265 identity,
1266 read_buffer[0..read],
1267 );
1268 }
1269
1270 test "detached child transfers exact reaping ownership" {
1271 const testing = std.testing;
1272 if (comptime detachPolicy() == .unsupported) {
1273 return error.SkipZigTest;
1274 }
1275 var io_state = std.Io.Threaded.init(
1276 testing.allocator,
1277 .{},
1278 );
1279 defer io_state.deinit();
1280 var child = try spawnDetached(io_state.io(), .{
1281 .argv = &.{ "sh", "-c", "exit 0" },
1282 });
1283 var child_owned = true;
1284 defer if (child_owned) killAndReap(
1285 &child,
1286 io_state.io(),
1287 );
1288 const child_id = child.id orelse
1289 return error.MissingChildId;
1290 try handoffReap(&child);
1291 child_owned = false;
1292 try testing.expect(child.id == null);
1293 const deadline = (try sys.time.awakeNow()).deadlineAfter(.fromMilliseconds(1_000));
1294 while (try processAlive(child_id)) {
1295 if ((try sys.time.awakeNow()).reached(deadline)) {
1296 return error.DetachedChildNotReaped;
1297 }
1298 sys.time.sleepMilliseconds(1);
1299 }
1300 }
1301
1302 test "process wait deadline ignores wall jumps and suspend gaps" {
1303 var clock = sys.time.FakeClock.zero();
1304 const deadline = clock.awake.deadlineAfter(.fromMilliseconds(10));
1305 clock.setWall(.fromNanoseconds(std.math.maxInt(u64)));
1306 clock.suspendGap(.fromMilliseconds(20));
1307 try std.testing.expect(!clock.awake.reached(deadline));
1308 clock.advance(.fromMilliseconds(10));
1309 try std.testing.expect(clock.awake.reached(deadline));
1310 }
1311
1312 test "process group lookup rejects absent and invalid identities" {
1313 const testing = std.testing;
1314 if (comptime processIdentityPolicy() == .unsupported) return error.SkipZigTest;
1315 try testing.expectError(error.ProcessNotFound, processGroupId(0));
1316 try testing.expectError(error.ProcessNotFound, processGroupId(-1));
1317 try testing.expect(!try processAlive(0));
1318 try testing.expect(try processAlive(try currentProcessId()));
1319 }