Skip to main content

fork/
command.rs

1use std::{
2    collections::BTreeMap,
3    error::Error,
4    ffi::{CString, OsStr, OsString},
5    fmt::{self, Display, Formatter},
6    fs::File,
7    io::{self, Read},
8    os::{
9        fd::{AsRawFd, OwnedFd, RawFd},
10        unix::ffi::OsStrExt,
11    },
12    thread,
13    time::{Duration, Instant},
14};
15
16use crate::{
17    ChildSignalState, ProcessGroupId, ProcessId, Signal,
18    child_signal::Prepared as PreparedSignalState,
19    descriptor::{Actions as DescriptorActions, Prepared as PreparedDescriptors, close_raw},
20    pipe_cloexec,
21    raw::{last_errno, write_all},
22    signal_process, wait_event_nohang,
23};
24
25const FAILURE_MAGIC: u32 = 0x494d_4d4f;
26const CHILD_FAILURE_EXIT: libc::c_int = 127;
27const CLEANUP_TIMEOUT: Duration = Duration::from_secs(1);
28const CLEANUP_POLL_INTERVAL: Duration = Duration::from_millis(5);
29
30/// Process-group setup requested for a spawned program.
31#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
32pub enum ProcessGroup {
33    /// Keep the broker's process group.
34    #[default]
35    Inherit,
36    /// Make the child the leader of a new process group.
37    New,
38    /// Join an existing process group.
39    Join(ProcessGroupId),
40}
41
42/// Supplementary-group behavior for a child identity transition.
43#[derive(Debug, Clone, Eq, PartialEq)]
44pub enum SupplementaryGroups {
45    /// Retain the broker's supplementary groups explicitly.
46    Preserve,
47    /// Replace the list, or clear it when the vector is empty.
48    Set(Vec<libc::gid_t>),
49}
50
51/// Numeric credentials materialized before `fork`.
52///
53/// Account-name and group-database resolution belongs in the caller. The child
54/// applies supplementary groups first, then the primary GID, then the UID so
55/// privilege cannot be lost before group setup is complete.
56#[derive(Debug, Clone, Eq, PartialEq)]
57pub struct ProcessCredentials {
58    user: libc::uid_t,
59    group: libc::gid_t,
60    supplementary_groups: SupplementaryGroups,
61}
62
63impl ProcessCredentials {
64    /// Construct a numeric identity transition.
65    #[must_use]
66    pub const fn new(
67        user: libc::uid_t,
68        group: libc::gid_t,
69        supplementary_groups: SupplementaryGroups,
70    ) -> Self {
71        Self {
72            user,
73            group,
74            supplementary_groups,
75        }
76    }
77
78    /// Return the target UID.
79    #[must_use]
80    pub const fn user(&self) -> libc::uid_t {
81        self.user
82    }
83
84    /// Return the target primary GID.
85    #[must_use]
86    pub const fn group(&self) -> libc::gid_t {
87        self.group
88    }
89
90    /// Return the requested supplementary-group behavior.
91    #[must_use]
92    pub const fn supplementary_groups(&self) -> &SupplementaryGroups {
93        &self.supplementary_groups
94    }
95}
96
97/// A reviewed child-side operation that can fail before `execve`.
98#[derive(Debug, Clone, Copy, Eq, PartialEq)]
99#[repr(u8)]
100pub enum SpawnStage {
101    Fork = 1,
102    ProcessGroup = 2,
103    CurrentDirectory = 3,
104    DescriptorDuplication = 4,
105    Execute = 5,
106    StartupHandshake = 6,
107    Identity = 7,
108    SignalState = 8,
109}
110
111impl Display for SpawnStage {
112    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
113        let name = match self {
114            Self::Fork => "fork",
115            Self::ProcessGroup => "process-group setup",
116            Self::CurrentDirectory => "working-directory change",
117            Self::DescriptorDuplication => "descriptor mapping",
118            Self::Execute => "execve",
119            Self::StartupHandshake => "startup handshake",
120            Self::Identity => "identity transition",
121            Self::SignalState => "signal-state reset",
122        };
123        formatter.write_str(name)
124    }
125}
126
127/// Failure to fork, prepare, or execute a child process.
128#[derive(Debug)]
129pub enum SpawnError {
130    /// An operating-system operation failed at a known startup stage.
131    OperatingSystem {
132        stage: SpawnStage,
133        error: io::Error,
134        cleanup_pending: Option<ProcessId>,
135    },
136    /// The child did not complete its exec handshake before the deadline.
137    TimedOut {
138        timeout: Duration,
139        cleanup_pending: Option<ProcessId>,
140    },
141    /// The child returned a malformed startup record.
142    InvalidHandshake { cleanup_pending: Option<ProcessId> },
143}
144
145impl SpawnError {
146    fn operating_system(stage: SpawnStage, error: io::Error) -> Self {
147        Self::OperatingSystem {
148            stage,
149            error,
150            cleanup_pending: None,
151        }
152    }
153
154    fn with_cleanup_pending(self, process: Option<ProcessId>) -> Self {
155        match self {
156            Self::OperatingSystem { stage, error, .. } => Self::OperatingSystem {
157                stage,
158                error,
159                cleanup_pending: process,
160            },
161            Self::TimedOut { timeout, .. } => Self::TimedOut {
162                timeout,
163                cleanup_pending: process,
164            },
165            Self::InvalidHandshake { .. } => Self::InvalidHandshake {
166                cleanup_pending: process,
167            },
168        }
169    }
170
171    /// Return a killed but not yet reaped child that the broker must retain.
172    ///
173    /// This is normally `None`. A value is returned only when bounded cleanup
174    /// could not observe termination, for example while the child is stuck in
175    /// uninterruptible kernel I/O. The PID remains a live ownership obligation
176    /// and must be reaped through the normal child-event loop.
177    #[must_use]
178    pub const fn cleanup_pending(&self) -> Option<ProcessId> {
179        match self {
180            Self::OperatingSystem {
181                cleanup_pending, ..
182            }
183            | Self::TimedOut {
184                cleanup_pending, ..
185            }
186            | Self::InvalidHandshake { cleanup_pending } => *cleanup_pending,
187        }
188    }
189}
190
191impl Display for SpawnError {
192    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
193        match self {
194            Self::OperatingSystem { stage, error, .. } => {
195                write!(formatter, "child {stage} failed: {error}")
196            }
197            Self::TimedOut { timeout, .. } => {
198                write!(formatter, "child startup timed out after {timeout:?}")
199            }
200            Self::InvalidHandshake { .. } => {
201                formatter.write_str("child returned an invalid handshake")
202            }
203        }
204    }
205}
206
207impl Error for SpawnError {
208    fn source(&self) -> Option<&(dyn Error + 'static)> {
209        match self {
210            Self::OperatingSystem { error, .. } => Some(error),
211            Self::TimedOut { .. } | Self::InvalidHandshake { .. } => None,
212        }
213    }
214}
215
216/// A successfully executed direct child of the process broker.
217#[derive(Debug, Clone, Copy, Eq, PartialEq)]
218pub struct SpawnedChild {
219    process: ProcessId,
220    group: Option<ProcessGroupId>,
221}
222
223impl SpawnedChild {
224    /// Return the live child process handle.
225    #[must_use]
226    pub const fn process(&self) -> ProcessId {
227        self.process
228    }
229
230    /// Return the explicitly configured process group, when any.
231    #[must_use]
232    pub const fn process_group(&self) -> Option<ProcessGroupId> {
233        self.group
234    }
235}
236
237/// A command whose child-visible data is materialized before `fork`.
238///
239/// The executable path is passed directly to `execve`; no shell or `PATH`
240/// lookup is performed. Arguments, environment, working directory, descriptor
241/// mappings, pointer arrays, and close bounds are all prepared in the broker
242/// process. The child performs only reviewed system calls before `execve` or
243/// `_exit`.
244#[derive(Debug)]
245pub struct PreparedCommand {
246    program: CString,
247    arguments: Vec<CString>,
248    environment: BTreeMap<OsString, OsString>,
249    current_directory: Option<CString>,
250    process_group: ProcessGroup,
251    credentials: Option<ProcessCredentials>,
252    signal_state: ChildSignalState,
253    descriptors: DescriptorActions,
254}
255
256impl PreparedCommand {
257    /// Create a direct-exec command and snapshot the broker's environment.
258    ///
259    /// # Errors
260    ///
261    /// Returns `InvalidInput` when the path is empty or contains a NUL byte.
262    pub fn new(program: impl AsRef<OsStr>) -> io::Result<Self> {
263        let program = os_string_to_cstring(program.as_ref(), "program")?;
264        if program.as_bytes().is_empty() {
265            return Err(io::Error::new(
266                io::ErrorKind::InvalidInput,
267                "program must not be empty",
268            ));
269        }
270        Ok(Self {
271            program,
272            arguments: Vec::new(),
273            environment: std::env::vars_os().collect(),
274            current_directory: None,
275            process_group: ProcessGroup::Inherit,
276            credentials: None,
277            signal_state: ChildSignalState::Reset,
278            descriptors: DescriptorActions::default(),
279        })
280    }
281
282    /// Append one argument without invoking a shell.
283    ///
284    /// # Errors
285    ///
286    /// Returns `InvalidInput` when the argument contains a NUL byte.
287    pub fn arg(&mut self, argument: impl AsRef<OsStr>) -> io::Result<&mut Self> {
288        self.arguments
289            .push(os_string_to_cstring(argument.as_ref(), "argument")?);
290        Ok(self)
291    }
292
293    /// Remove every inherited environment entry.
294    pub fn clear_environment(&mut self) -> &mut Self {
295        self.environment.clear();
296        self
297    }
298
299    /// Set one child environment entry.
300    ///
301    /// # Errors
302    ///
303    /// Returns `InvalidInput` when the key is empty, contains `=` or NUL, or
304    /// when the value contains NUL.
305    pub fn environment(
306        &mut self,
307        key: impl AsRef<OsStr>,
308        value: impl AsRef<OsStr>,
309    ) -> io::Result<&mut Self> {
310        let key = key.as_ref();
311        let key_bytes = key.as_bytes();
312        if key_bytes.is_empty() || key_bytes.contains(&b'=') || key_bytes.contains(&0) {
313            return Err(io::Error::new(
314                io::ErrorKind::InvalidInput,
315                "environment key must be nonempty and contain neither '=' nor NUL",
316            ));
317        }
318        if value.as_ref().as_bytes().contains(&0) {
319            return Err(io::Error::new(
320                io::ErrorKind::InvalidInput,
321                "environment value must not contain NUL",
322            ));
323        }
324        self.environment
325            .insert(key.to_os_string(), value.as_ref().to_os_string());
326        Ok(self)
327    }
328
329    /// Set the working directory used immediately before `execve`.
330    ///
331    /// # Errors
332    ///
333    /// Returns `InvalidInput` when the path contains a NUL byte.
334    pub fn current_directory(&mut self, path: impl AsRef<OsStr>) -> io::Result<&mut Self> {
335        self.current_directory = Some(os_string_to_cstring(path.as_ref(), "working directory")?);
336        Ok(self)
337    }
338
339    /// Configure child process-group membership.
340    pub fn process_group(&mut self, group: ProcessGroup) -> &mut Self {
341        self.process_group = group;
342        self
343    }
344
345    /// Configure a numeric UID/GID transition performed immediately before exec.
346    pub fn credentials(&mut self, credentials: ProcessCredentials) -> &mut Self {
347        self.credentials = Some(credentials);
348        self
349    }
350
351    /// Configure whether the child resets or inherits signal state.
352    pub fn signal_state(&mut self, state: ChildSignalState) -> &mut Self {
353        self.signal_state = state;
354        self
355    }
356
357    /// Map an owned source descriptor onto a child descriptor number.
358    ///
359    /// The source is duplicated above every requested target before `fork`, so
360    /// cyclic and overlapping mappings cannot overwrite another source. The
361    /// target is the only descriptor inherited through a successful `exec`.
362    ///
363    /// # Errors
364    ///
365    /// Returns `InvalidInput` for a negative or duplicate target.
366    pub fn map_descriptor(&mut self, source: OwnedFd, target: RawFd) -> io::Result<&mut Self> {
367        self.descriptors.map(source, target)?;
368        Ok(self)
369    }
370
371    /// Preserve an owned descriptor at its current number through `execve`.
372    ///
373    /// # Errors
374    ///
375    /// Returns `InvalidInput` when that descriptor is already mapped or closed.
376    pub fn inherit_descriptor(&mut self, descriptor: OwnedFd) -> io::Result<&mut Self> {
377        self.descriptors.inherit(descriptor)?;
378        Ok(self)
379    }
380
381    /// Explicitly close a descriptor in the child before `execve`.
382    ///
383    /// This is primarily useful for standard input, output, or error because
384    /// every descriptor above standard error is already closed unless mapped or
385    /// explicitly inherited.
386    ///
387    /// # Errors
388    ///
389    /// Returns `InvalidInput` for a negative or mapped descriptor.
390    pub fn close_descriptor(&mut self, descriptor: RawFd) -> io::Result<&mut Self> {
391        self.descriptors.close(descriptor)?;
392        Ok(self)
393    }
394
395    /// Fork and execute the command with a bounded startup handshake.
396    ///
397    /// Success means `execve` completed and closed the handshake descriptor,
398    /// not that the program remains running. Every failure path terminates and
399    /// reaps the pre-exec child before returning.
400    ///
401    /// # Errors
402    ///
403    /// Returns a typed startup stage and OS error, a timeout, or a malformed
404    /// handshake error.
405    pub fn spawn(self, startup_timeout: Duration) -> Result<SpawnedChild, SpawnError> {
406        SpawnPreparation::new(self)?.spawn(startup_timeout)
407    }
408}
409
410struct SpawnPreparation {
411    program: CString,
412    _arguments: Vec<CString>,
413    argument_pointers: Vec<*const libc::c_char>,
414    _environment: Vec<CString>,
415    environment_pointers: Vec<*const libc::c_char>,
416    current_directory: Option<CString>,
417    process_group: ProcessGroup,
418    credentials: Option<ProcessCredentials>,
419    signal_state: Option<PreparedSignalState>,
420    descriptors: PreparedDescriptors,
421    status_reader: OwnedFd,
422    status_writer: OwnedFd,
423}
424
425impl SpawnPreparation {
426    fn new(command: PreparedCommand) -> Result<Self, SpawnError> {
427        let pipe = pipe_cloexec()
428            .map_err(|error| SpawnError::operating_system(SpawnStage::StartupHandshake, error))?;
429        let (status_reader, initial_status_writer) = pipe.into_parts();
430        let (descriptors, status_writer) = command
431            .descriptors
432            .prepare(initial_status_writer)
433            .map_err(|error| {
434                SpawnError::operating_system(SpawnStage::DescriptorDuplication, error)
435            })?;
436
437        let environment = materialize_environment(command.environment)
438            .map_err(|error| SpawnError::operating_system(SpawnStage::Execute, error))?;
439        let mut arguments = Vec::with_capacity(command.arguments.len() + 1);
440        arguments.push(command.program.clone());
441        arguments.extend(command.arguments);
442        let argument_pointers = cstring_pointers(&arguments);
443        let environment_pointers = cstring_pointers(&environment);
444
445        validate_credentials(command.credentials.as_ref())
446            .map_err(|error| SpawnError::operating_system(SpawnStage::Identity, error))?;
447        let signal_state = PreparedSignalState::new(command.signal_state)
448            .map_err(|error| SpawnError::operating_system(SpawnStage::SignalState, error))?;
449
450        Ok(Self {
451            program: command.program,
452            _arguments: arguments,
453            argument_pointers,
454            _environment: environment,
455            environment_pointers,
456            current_directory: command.current_directory,
457            process_group: command.process_group,
458            credentials: command.credentials,
459            signal_state,
460            descriptors,
461            status_reader,
462            status_writer,
463        })
464    }
465
466    fn spawn(self, startup_timeout: Duration) -> Result<SpawnedChild, SpawnError> {
467        // SAFETY: all child-visible data and pointer arrays are fully prepared;
468        // the child branch calls only the reviewed syscall-only routine below.
469        let raw_child = unsafe { libc::fork() };
470        if raw_child == -1 {
471            return Err(SpawnError::operating_system(
472                SpawnStage::Fork,
473                io::Error::last_os_error(),
474            ));
475        }
476        if raw_child == 0 {
477            self.run_child();
478        }
479
480        let process = ProcessId::new(raw_child).ok_or_else(|| {
481            SpawnError::operating_system(
482                SpawnStage::Fork,
483                io::Error::new(
484                    io::ErrorKind::InvalidData,
485                    "fork returned a nonpositive child PID",
486                ),
487            )
488        })?;
489        drop(self.status_writer);
490        parent_set_process_group(process, self.process_group);
491
492        let group = match self.process_group {
493            ProcessGroup::Inherit => None,
494            ProcessGroup::New => ProcessGroupId::new(process.get()),
495            ProcessGroup::Join(group) => Some(group),
496        };
497        let result = read_startup_handshake(self.status_reader, startup_timeout);
498        match result {
499            Ok(()) => Ok(SpawnedChild { process, group }),
500            Err(error) => {
501                let cleanup_pending = terminate_and_reap_bounded(process);
502                Err(error.with_cleanup_pending(cleanup_pending))
503            }
504        }
505    }
506
507    fn run_child(&self) -> ! {
508        close_raw(self.status_reader.as_raw_fd());
509
510        if let Some(signal_state) = &self.signal_state
511            && let Err(errno) = signal_state.apply_in_child()
512        {
513            child_fail(
514                self.status_writer.as_raw_fd(),
515                SpawnStage::SignalState,
516                errno,
517            );
518        }
519
520        if let Err(errno) = child_set_process_group(self.process_group) {
521            child_fail(
522                self.status_writer.as_raw_fd(),
523                SpawnStage::ProcessGroup,
524                errno,
525            );
526        }
527        if let Some(directory) = &self.current_directory {
528            // SAFETY: the directory is a live, NUL-terminated CString.
529            if unsafe { libc::chdir(directory.as_ptr()) } == -1 {
530                child_fail(
531                    self.status_writer.as_raw_fd(),
532                    SpawnStage::CurrentDirectory,
533                    last_errno(),
534                );
535            }
536        }
537        if let Err(errno) = self.descriptors.apply_in_child() {
538            child_fail(
539                self.status_writer.as_raw_fd(),
540                SpawnStage::DescriptorDuplication,
541                errno,
542            );
543        }
544
545        if let Some(credentials) = &self.credentials
546            && let Err(errno) = apply_credentials(credentials)
547        {
548            child_fail(self.status_writer.as_raw_fd(), SpawnStage::Identity, errno);
549        }
550
551        // The backing CString vectors remain fields of `self` through exec;
552        // their pointer arrays were prepared before fork and end with null.
553        // SAFETY: program, argv, and envp are valid C strings/pointer arrays.
554        unsafe {
555            libc::execve(
556                self.program.as_ptr(),
557                self.argument_pointers.as_ptr(),
558                self.environment_pointers.as_ptr(),
559            );
560        }
561        child_fail(
562            self.status_writer.as_raw_fd(),
563            SpawnStage::Execute,
564            last_errno(),
565        );
566    }
567}
568
569#[repr(C)]
570#[derive(Clone, Copy)]
571struct FailureRecord {
572    magic: u32,
573    errno: i32,
574    stage: u8,
575    reserved: [u8; 3],
576}
577
578fn os_string_to_cstring(value: &OsStr, label: &str) -> io::Result<CString> {
579    CString::new(value.as_bytes()).map_err(|_| {
580        io::Error::new(
581            io::ErrorKind::InvalidInput,
582            format!("{label} must not contain NUL"),
583        )
584    })
585}
586
587fn materialize_environment(environment: BTreeMap<OsString, OsString>) -> io::Result<Vec<CString>> {
588    environment
589        .into_iter()
590        .map(|(key, value)| {
591            let mut entry = Vec::with_capacity(key.as_bytes().len() + value.as_bytes().len() + 1);
592            entry.extend_from_slice(key.as_bytes());
593            entry.push(b'=');
594            entry.extend_from_slice(value.as_bytes());
595            CString::new(entry).map_err(|_| {
596                io::Error::new(io::ErrorKind::InvalidInput, "environment contains NUL")
597            })
598        })
599        .collect()
600}
601
602fn cstring_pointers(strings: &[CString]) -> Vec<*const libc::c_char> {
603    let mut pointers = Vec::with_capacity(strings.len() + 1);
604    pointers.extend(strings.iter().map(|value| value.as_ptr()));
605    pointers.push(std::ptr::null());
606    pointers
607}
608
609fn parent_set_process_group(process: ProcessId, group: ProcessGroup) {
610    let target = match group {
611        ProcessGroup::Inherit => return,
612        ProcessGroup::New => process.get(),
613        ProcessGroup::Join(group) => group.get(),
614    };
615    loop {
616        // SAFETY: checked positive identifiers are passed to setpgid.
617        if unsafe { libc::setpgid(process.get(), target) } == 0 {
618            return;
619        }
620        let error = io::Error::last_os_error();
621        if error.kind() == io::ErrorKind::Interrupted {
622            continue;
623        }
624        // The child performs the same operation and reports authoritative
625        // failure through the handshake. EACCES/ESRCH mean it won the race.
626        return;
627    }
628}
629
630fn read_startup_handshake(descriptor: OwnedFd, timeout: Duration) -> Result<(), SpawnError> {
631    let deadline = Instant::now()
632        .checked_add(timeout)
633        .unwrap_or_else(Instant::now);
634    let mut poll_descriptor = libc::pollfd {
635        fd: descriptor.as_raw_fd(),
636        events: libc::POLLIN | libc::POLLHUP,
637        revents: 0,
638    };
639    loop {
640        let remaining = deadline.saturating_duration_since(Instant::now());
641        let timeout_millis = duration_to_poll_timeout(remaining);
642        // SAFETY: poll_descriptor points to one initialized pollfd.
643        let result = unsafe { libc::poll(&raw mut poll_descriptor, 1, timeout_millis) };
644        if result > 0 {
645            break;
646        }
647        if result == 0 {
648            return Err(SpawnError::TimedOut {
649                timeout,
650                cleanup_pending: None,
651            });
652        }
653        let error = io::Error::last_os_error();
654        if error.kind() != io::ErrorKind::Interrupted {
655            return Err(SpawnError::operating_system(
656                SpawnStage::StartupHandshake,
657                error,
658            ));
659        }
660    }
661
662    let mut file = File::from(descriptor);
663    let mut bytes = [0_u8; size_of::<FailureRecord>()];
664    let mut read = 0;
665    while read < bytes.len() {
666        let remaining = bytes.get_mut(read..).ok_or(SpawnError::InvalidHandshake {
667            cleanup_pending: None,
668        })?;
669        match file.read(remaining) {
670            Ok(0) if read == 0 => return Ok(()),
671            Ok(0) => {
672                return Err(SpawnError::InvalidHandshake {
673                    cleanup_pending: None,
674                });
675            }
676            Ok(count) => read += count,
677            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
678            Err(error) => {
679                return Err(SpawnError::operating_system(
680                    SpawnStage::StartupHandshake,
681                    error,
682                ));
683            }
684        }
685    }
686
687    // SAFETY: FailureRecord is Copy and bytes has its exact size; read_unaligned
688    // avoids imposing alignment on the byte array.
689    let record = unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast::<FailureRecord>()) };
690    if record.magic != FAILURE_MAGIC {
691        return Err(SpawnError::InvalidHandshake {
692            cleanup_pending: None,
693        });
694    }
695    let stage = spawn_stage_from_wire(record.stage).ok_or(SpawnError::InvalidHandshake {
696        cleanup_pending: None,
697    })?;
698    Err(SpawnError::operating_system(
699        stage,
700        io::Error::from_raw_os_error(record.errno),
701    ))
702}
703
704fn duration_to_poll_timeout(duration: Duration) -> libc::c_int {
705    if duration.is_zero() {
706        return 0;
707    }
708    let millis = duration.as_millis().max(1);
709    libc::c_int::try_from(millis).unwrap_or(libc::c_int::MAX)
710}
711
712const fn spawn_stage_from_wire(stage: u8) -> Option<SpawnStage> {
713    match stage {
714        2 => Some(SpawnStage::ProcessGroup),
715        3 => Some(SpawnStage::CurrentDirectory),
716        4 => Some(SpawnStage::DescriptorDuplication),
717        5 => Some(SpawnStage::Execute),
718        7 => Some(SpawnStage::Identity),
719        8 => Some(SpawnStage::SignalState),
720        _ => None,
721    }
722}
723
724fn validate_credentials(credentials: Option<&ProcessCredentials>) -> io::Result<()> {
725    let Some(ProcessCredentials {
726        supplementary_groups: SupplementaryGroups::Set(groups),
727        ..
728    }) = credentials
729    else {
730        return Ok(());
731    };
732    let maximum = loop {
733        // SAFETY: sysconf reads the process group-count limit without pointers.
734        let result = unsafe { libc::sysconf(libc::_SC_NGROUPS_MAX) };
735        if result >= 0 {
736            break usize::try_from(result).unwrap_or(usize::MAX);
737        }
738        let error = io::Error::last_os_error();
739        if error.kind() != io::ErrorKind::Interrupted {
740            return Err(error);
741        }
742    };
743    if groups.len() > maximum {
744        return Err(io::Error::new(
745            io::ErrorKind::InvalidInput,
746            "supplementary group count exceeds the operating-system limit",
747        ));
748    }
749    Ok(())
750}
751
752fn terminate_and_reap_bounded(process: ProcessId) -> Option<ProcessId> {
753    let _ = signal_process(process, Signal::KILL);
754    let deadline = Instant::now() + CLEANUP_TIMEOUT;
755    loop {
756        match wait_event_nohang(process) {
757            Ok(Some(event)) if event.is_terminal() => return None,
758            Err(error) if error.raw_os_error() == Some(libc::ECHILD) => return None,
759            Err(_) => return Some(process),
760            Ok(Some(_) | None) => {}
761        }
762        if Instant::now() >= deadline {
763            return Some(process);
764        }
765        thread::sleep(CLEANUP_POLL_INTERVAL);
766    }
767}
768
769fn child_set_process_group(group: ProcessGroup) -> Result<(), libc::c_int> {
770    let target = match group {
771        ProcessGroup::Inherit => return Ok(()),
772        ProcessGroup::New => 0,
773        ProcessGroup::Join(group) => group.get(),
774    };
775    loop {
776        // SAFETY: pid zero targets the current child; target is zero or checked positive.
777        if unsafe { libc::setpgid(0, target) } == 0 {
778            return Ok(());
779        }
780        let errno = last_errno();
781        if errno != libc::EINTR {
782            return Err(errno);
783        }
784    }
785}
786
787fn apply_credentials(credentials: &ProcessCredentials) -> Result<(), libc::c_int> {
788    if let SupplementaryGroups::Set(groups) = &credentials.supplementary_groups {
789        loop {
790            if set_supplementary_groups(groups) == 0 {
791                break;
792            }
793            let errno = last_errno();
794            if errno != libc::EINTR {
795                return Err(errno);
796            }
797        }
798    }
799    loop {
800        // SAFETY: the numeric primary GID was fully materialized before fork.
801        if unsafe { libc::setgid(credentials.group) } == 0 {
802            break;
803        }
804        let errno = last_errno();
805        if errno != libc::EINTR {
806            return Err(errno);
807        }
808    }
809    loop {
810        // SAFETY: the numeric UID was fully materialized before fork.
811        if unsafe { libc::setuid(credentials.user) } == 0 {
812            return Ok(());
813        }
814        let errno = last_errno();
815        if errno != libc::EINTR {
816            return Err(errno);
817        }
818    }
819}
820
821#[cfg(any(target_os = "linux", target_os = "android"))]
822fn set_supplementary_groups(groups: &[libc::gid_t]) -> libc::c_int {
823    // SAFETY: the group slice was bounded before fork and remains live.
824    unsafe { libc::setgroups(groups.len(), groups.as_ptr()) }
825}
826
827#[cfg(not(any(target_os = "linux", target_os = "android")))]
828fn set_supplementary_groups(groups: &[libc::gid_t]) -> libc::c_int {
829    let Ok(count) = libc::c_int::try_from(groups.len()) else {
830        return -1;
831    };
832    // SAFETY: the group slice was bounded before fork and remains live.
833    unsafe { libc::setgroups(count, groups.as_ptr()) }
834}
835
836fn child_fail(descriptor: RawFd, stage: SpawnStage, errno: libc::c_int) -> ! {
837    let record = FailureRecord {
838        magic: FAILURE_MAGIC,
839        errno,
840        stage: stage as u8,
841        reserved: [0; 3],
842    };
843    // SAFETY: record remains live and immutable while the byte view is written.
844    let bytes = unsafe {
845        std::slice::from_raw_parts((&raw const record).cast::<u8>(), size_of::<FailureRecord>())
846    };
847    let _ = write_all(descriptor, bytes);
848    // SAFETY: _exit terminates only the fork child without running destructors.
849    unsafe { libc::_exit(CHILD_FAILURE_EXIT) }
850}
851
852#[cfg(test)]
853mod tests {
854    use super::{
855        SpawnError, SpawnStage, duration_to_poll_timeout, read_startup_handshake,
856        spawn_stage_from_wire,
857    };
858    use crate::pipe_cloexec;
859    use std::{error::Error, thread, time::Duration};
860
861    #[test]
862    fn wire_stages_reject_parent_only_values() {
863        assert_eq!(spawn_stage_from_wire(2), Some(SpawnStage::ProcessGroup));
864        assert_eq!(spawn_stage_from_wire(5), Some(SpawnStage::Execute));
865        assert_eq!(spawn_stage_from_wire(7), Some(SpawnStage::Identity));
866        assert_eq!(spawn_stage_from_wire(8), Some(SpawnStage::SignalState));
867        assert_eq!(spawn_stage_from_wire(1), None);
868        assert_eq!(spawn_stage_from_wire(255), None);
869    }
870
871    #[test]
872    fn poll_timeout_rounds_up_and_saturates() {
873        assert_eq!(duration_to_poll_timeout(Duration::ZERO), 0);
874        assert_eq!(duration_to_poll_timeout(Duration::from_nanos(1)), 1);
875        assert_eq!(
876            duration_to_poll_timeout(Duration::from_secs(u64::MAX)),
877            libc::c_int::MAX
878        );
879    }
880
881    #[test]
882    fn startup_handshake_obeys_timeout() -> Result<(), Box<dyn Error>> {
883        let pipe = pipe_cloexec()?;
884        let (reader, writer) = pipe.into_parts();
885        let holder = thread::spawn(move || {
886            thread::sleep(Duration::from_millis(25));
887            drop(writer);
888        });
889        let Err(error) = read_startup_handshake(reader, Duration::from_millis(1)) else {
890            return Err(std::io::Error::other("open handshake did not time out").into());
891        };
892        assert!(matches!(error, SpawnError::TimedOut { .. }));
893        holder
894            .join()
895            .map_err(|_| std::io::Error::other("handshake holder panicked"))?;
896        Ok(())
897    }
898}