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 }