Skip to main content

coreshift_core/spawn/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Process spawning and lifecycle management.
6//!
7//! This module exposes explicit Linux/Android process primitives. Callers must
8//! provide the exact argument vector and choose the spawn backend. Core does not
9//! infer shell/root behavior, select backends from platform properties, or
10//! silently switch between backends.
11
12use std::ffi::CString;
13use std::mem::MaybeUninit;
14use std::os::unix::io::RawFd;
15use std::ptr;
16use std::time::{Duration, Instant};
17
18use crate::CoreError;
19use crate::error::{posix_ret, syscall_ret};
20use crate::fd::Fd;
21use crate::signal::SignalRuntime;
22use libc::{
23    O_CLOEXEC, O_NONBLOCK, WEXITSTATUS, WIFEXITED, WIFSIGNALED, WTERMSIG, c_char, pid_t, pipe2,
24    waitpid,
25};
26
27unsafe extern "C" {
28    pub(crate) static mut environ: *mut *mut libc::c_char;
29}
30
31pub(crate) const POSIX_SPAWN_SETPGROUP: i32 = 2;
32pub(crate) const POSIX_SPAWN_SETSIGDEF: i32 = 4;
33pub(crate) const POSIX_SPAWN_SETSIGMASK: i32 = 8;
34
35unsafe extern "C" {
36    pub(crate) fn posix_spawn(
37        pid: *mut libc::pid_t,
38        path: *const libc::c_char,
39        file_actions: *const libc::posix_spawn_file_actions_t,
40        attrp: *const libc::posix_spawnattr_t,
41        argv: *const *mut libc::c_char,
42        envp: *const *mut libc::c_char,
43    ) -> libc::c_int;
44
45    pub(crate) fn posix_spawn_file_actions_addclose(
46        file_actions: *mut libc::posix_spawn_file_actions_t,
47        fd: libc::c_int,
48    ) -> libc::c_int;
49
50    pub(crate) fn posix_spawn_file_actions_adddup2(
51        file_actions: *mut libc::posix_spawn_file_actions_t,
52        fd: libc::c_int,
53        newfd: libc::c_int,
54    ) -> libc::c_int;
55
56    pub(crate) fn posix_spawn_file_actions_destroy(
57        file_actions: *mut libc::posix_spawn_file_actions_t,
58    ) -> libc::c_int;
59
60    pub(crate) fn posix_spawn_file_actions_init(
61        file_actions: *mut libc::posix_spawn_file_actions_t,
62    ) -> libc::c_int;
63
64    pub(crate) fn posix_spawnattr_destroy(attr: *mut libc::posix_spawnattr_t) -> libc::c_int;
65
66    pub(crate) fn posix_spawnattr_init(attr: *mut libc::posix_spawnattr_t) -> libc::c_int;
67
68    pub(crate) fn posix_spawnattr_setflags(
69        attr: *mut libc::posix_spawnattr_t,
70        flags: libc::c_short,
71    ) -> libc::c_int;
72
73    pub(crate) fn posix_spawnattr_setpgroup(
74        attr: *mut libc::posix_spawnattr_t,
75        pgroup: libc::pid_t,
76    ) -> libc::c_int;
77
78    pub(crate) fn posix_spawnattr_setsigdefault(
79        attr: *mut libc::posix_spawnattr_t,
80        sigdefault: *const libc::sigset_t,
81    ) -> libc::c_int;
82
83    pub(crate) fn posix_spawnattr_setsigmask(
84        attr: *mut libc::posix_spawnattr_t,
85        sigmask: *const libc::sigset_t,
86    ) -> libc::c_int;
87}
88
89/// Policy for handling process cancellation or timeouts.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91pub enum CancelPolicy {
92    /// Do nothing on cancellation; let the process run to completion.
93    #[default]
94    None,
95    /// Send SIGTERM, then SIGKILL after a grace period.
96    Graceful,
97    /// Send SIGKILL immediately.
98    Kill,
99}
100
101/// Process group and session configuration.
102#[derive(Debug, Clone, Copy, Default)]
103pub struct ProcessGroup {
104    /// Join an existing process group leader.
105    pub leader: Option<pid_t>,
106    /// Create a new session (`setsid`).
107    pub isolated: bool,
108}
109
110impl ProcessGroup {
111    /// Create a new process group configuration.
112    pub fn new(leader: Option<pid_t>, isolated: bool) -> Self {
113        Self { leader, isolated }
114    }
115}
116
117#[inline(always)]
118fn errno() -> i32 {
119    std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
120}
121
122/// Creates a pipe with O_CLOEXEC | O_NONBLOCK flags.
123/// Invariants: FDs returned are strictly non-negative and will close automatically on drop.
124#[inline(always)]
125fn make_pipe() -> Result<(Fd, Fd), CoreError> {
126    let mut fds = [0; 2];
127    let r = unsafe { pipe2(fds.as_mut_ptr(), O_CLOEXEC | O_NONBLOCK) };
128    syscall_ret(r, "pipe2")?;
129    Ok((Fd::new(fds[0], "pipe2")?, Fd::new(fds[1], "pipe2")?))
130}
131
132fn make_cloexec_pipe() -> Result<(RawFd, RawFd), CoreError> {
133    let mut fds = [0; 2];
134    let r = unsafe { pipe2(fds.as_mut_ptr(), O_CLOEXEC) };
135    syscall_ret(r, "pipe2")?;
136    Ok((fds[0], fds[1]))
137}
138
139#[repr(u8)]
140#[derive(Clone, Copy)]
141enum ChildSetupOp {
142    DupStdin = 1,
143    DupStdout = 2,
144    DupStderr = 3,
145    Setsid = 4,
146    Chdir = 5,
147    Setpgid = 6,
148    SignalMask = 7,
149    Execve = 8,
150}
151
152impl ChildSetupOp {
153    fn as_str(self) -> &'static str {
154        match self {
155            Self::DupStdin => "fork child dup2 stdin",
156            Self::DupStdout => "fork child dup2 stdout",
157            Self::DupStderr => "fork child dup2 stderr",
158            Self::Setsid => "fork child setsid",
159            Self::Chdir => "fork child chdir",
160            Self::Setpgid => "fork child setpgid",
161            Self::SignalMask => "fork child signal setup",
162            Self::Execve => "fork child execve",
163        }
164    }
165
166    fn from_u8(value: u8) -> Self {
167        match value {
168            1 => Self::DupStdin,
169            2 => Self::DupStdout,
170            3 => Self::DupStderr,
171            4 => Self::Setsid,
172            5 => Self::Chdir,
173            6 => Self::Setpgid,
174            7 => Self::SignalMask,
175            _ => Self::Execve,
176        }
177    }
178}
179
180unsafe fn report_child_setup_error(fd: RawFd, op: ChildSetupOp, code: i32) -> ! {
181    let mut msg = [0u8; 5];
182    msg[..4].copy_from_slice(&code.to_ne_bytes());
183    msg[4] = op as u8;
184    let mut written = 0;
185    while written < msg.len() {
186        let n = unsafe {
187            libc::write(
188                fd,
189                msg[written..].as_ptr().cast::<libc::c_void>(),
190                msg.len() - written,
191            )
192        };
193        if n <= 0 {
194            break;
195        }
196        written += n as usize;
197    }
198    unsafe {
199        libc::_exit(127);
200    }
201}
202
203fn read_child_setup_error(fd: RawFd) -> Result<Option<CoreError>, CoreError> {
204    let mut msg = [0u8; 5];
205    let mut read_len = 0;
206    loop {
207        let n = unsafe {
208            libc::read(
209                fd,
210                msg[read_len..].as_mut_ptr().cast::<libc::c_void>(),
211                msg.len() - read_len,
212            )
213        };
214        if n == 0 {
215            return Ok(None);
216        }
217        if n < 0 {
218            let code = errno();
219            if code == libc::EINTR {
220                continue;
221            }
222            return Err(CoreError::sys(code, "read fork child setup error"));
223        }
224        read_len += n as usize;
225        if read_len == msg.len() {
226            let code = i32::from_ne_bytes([msg[0], msg[1], msg[2], msg[3]]);
227            return Ok(Some(CoreError::sys(
228                code,
229                ChildSetupOp::from_u8(msg[4]).as_str(),
230            )));
231        }
232    }
233}
234
235struct Pipes {
236    stdin_r: Option<Fd>,
237    stdin_w: Option<Fd>,
238    stdout_r: Option<Fd>,
239    stdout_w: Option<Fd>,
240    stderr_r: Option<Fd>,
241    stderr_w: Option<Fd>,
242}
243
244impl Pipes {
245    fn new(in_buf: Option<&[u8]>, out: bool, err: bool) -> Result<Self, CoreError> {
246        let (stdin_r, stdin_w) = if in_buf.is_some() {
247            let (r, w) = make_pipe()?;
248            (Some(r), Some(w))
249        } else {
250            (None, None)
251        };
252
253        let (stdout_r, stdout_w) = if out {
254            let (r, w) = make_pipe()?;
255            (Some(r), Some(w))
256        } else {
257            (None, None)
258        };
259
260        let (stderr_r, stderr_w) = if err {
261            let (r, w) = make_pipe()?;
262            (Some(r), Some(w))
263        } else {
264            (None, None)
265        };
266
267        Ok(Self {
268            stdin_r,
269            stdin_w,
270            stdout_r,
271            stdout_w,
272            stderr_r,
273            stderr_w,
274        })
275    }
276
277    #[inline(always)]
278    fn close_all(&mut self) {
279        self.stdin_r.take();
280        self.stdin_w.take();
281        self.stdout_r.take();
282        self.stdout_w.take();
283        self.stderr_r.take();
284        self.stderr_w.take();
285    }
286}
287
288/// Represents the termination status of a process.
289#[derive(Debug, PartialEq, Eq)]
290pub enum ExitStatus {
291    /// Process exited normally with the specified code.
292    Exited(i32),
293    /// Process was terminated by a signal.
294    Signaled(i32),
295}
296
297/// Explicit process spawning backend.
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub enum SpawnBackend {
300    /// Force the use of `posix_spawn`.
301    PosixSpawn,
302    /// Force the use of `fork`/`exec`.
303    ///
304    /// The fork backend supports explicit [`SpawnFdPolicy`] handling before
305    /// `execve`.
306    Fork,
307}
308
309/// Explicit file-descriptor inheritance policy for spawned children.
310#[derive(Debug, Clone, PartialEq, Eq, Default)]
311pub enum SpawnFdPolicy {
312    /// Inherit descriptors according to their existing `FD_CLOEXEC` flags.
313    #[default]
314    CloexecOnly,
315    /// For the fork backend, close every descriptor >= 3 before `execve`,
316    /// except Core-required pipe descriptors.
317    CloseFrom3,
318    /// For the fork backend, close every descriptor >= 3 before `execve`,
319    /// except Core-required pipe descriptors and the listed descriptors.
320    ///
321    /// Core does not close allowlisted descriptors, but their existing
322    /// `FD_CLOEXEC` state still applies. Callers that want an allowlisted
323    /// descriptor to survive `execve` must clear `FD_CLOEXEC` before spawning.
324    Allowlist(Vec<RawFd>),
325}
326
327/// Owned argument vector storage for spawn internals.
328#[derive(Clone)]
329enum ExecArgv {
330    /// Dynamically allocated C-compatible strings.
331    Dynamic(Vec<CString>),
332}
333
334/// Validated execution context for process spawning.
335#[derive(Clone)]
336struct ExecContext {
337    argv: ExecArgv,
338    envp: Option<Vec<CString>>,
339    cwd: Option<CString>,
340}
341
342impl ExecContext {
343    /// Build a validated execution context for process spawn.
344    fn new(
345        argv: Vec<String>,
346        env: Option<Vec<String>>,
347        cwd: Option<String>,
348    ) -> Result<Self, CoreError> {
349        if argv.is_empty() {
350            return Err(CoreError::sys(libc::EINVAL, "exec argv empty"));
351        }
352
353        let c_argv: Vec<CString> = argv
354            .into_iter()
355            .map(|s| {
356                CString::new(s).map_err(|_| CoreError::sys(libc::EINVAL, "exec argv contains nul"))
357            })
358            .collect::<Result<_, _>>()?;
359
360        let c_envp = match env {
361            Some(vars) => Some(
362                vars.into_iter()
363                    .map(|s| {
364                        CString::new(s)
365                            .map_err(|_| CoreError::sys(libc::EINVAL, "exec env contains nul"))
366                    })
367                    .collect::<Result<Vec<_>, _>>()?,
368            ),
369            None => None,
370        };
371
372        let c_cwd = match cwd {
373            Some(c) => Some(
374                CString::new(c)
375                    .map_err(|_| CoreError::sys(libc::EINVAL, "exec cwd contains nul"))?,
376            ),
377            None => None,
378        };
379
380        Ok(Self {
381            argv: ExecArgv::Dynamic(c_argv),
382            envp: c_envp,
383            cwd: c_cwd,
384        })
385    }
386
387    /// Return a vector of pointers to the argument strings.
388    fn get_argv_ptrs(&self) -> Vec<*mut libc::c_char> {
389        let mut ptrs = Vec::new();
390        match &self.argv {
391            ExecArgv::Dynamic(v) => {
392                for s in v {
393                    ptrs.push(s.as_ptr() as *mut libc::c_char);
394                }
395            }
396        }
397        ptrs.push(ptr::null_mut());
398        ptrs
399    }
400
401    /// Return a vector of pointers to the environment strings.
402    fn get_envp_ptrs(&self) -> Option<Vec<*mut libc::c_char>> {
403        self.envp.as_ref().map(|envp| {
404            let mut ptrs = Vec::new();
405            for s in envp {
406                ptrs.push(s.as_ptr() as *mut libc::c_char);
407            }
408            ptrs.push(ptr::null_mut());
409            ptrs
410        })
411    }
412}
413
414#[inline(always)]
415fn decode_status(status: i32) -> ExitStatus {
416    if WIFEXITED(status) {
417        ExitStatus::Exited(WEXITSTATUS(status))
418    } else if WIFSIGNALED(status) {
419        ExitStatus::Signaled(WTERMSIG(status))
420    } else {
421        ExitStatus::Exited(-1)
422    }
423}
424
425/// A handle to a spawned process.
426///
427/// ### Fork Safety
428/// The process handle contains a PID. After a `fork`, the child process will
429/// have a copy of this PID, but it refers to the same original process.
430/// Calling `wait` or `kill` from the child may lead to confusing results
431/// if multiple processes are managing the same PID.
432pub struct Process {
433    pid: pid_t,
434}
435
436impl Process {
437    /// Create a handle for an existing PID.
438    pub fn new(pid: pid_t) -> Self {
439        Self { pid }
440    }
441
442    /// Return the process ID.
443    pub fn pid(&self) -> pid_t {
444        self.pid
445    }
446
447    /// Perform a non-blocking wait for process termination.
448    ///
449    /// ### Errors
450    /// - `ECHILD`: The process does not exist or is not a child of the caller.
451    /// - `EINTR`: The call was interrupted by a signal (handled internally).
452    pub fn wait_step(&self) -> Result<Option<ExitStatus>, CoreError> {
453        loop {
454            let mut status = 0;
455            let r = unsafe { waitpid(self.pid, &mut status, libc::WNOHANG) };
456            if r == 0 {
457                return Ok(None);
458            }
459            if r < 0 {
460                let e = errno();
461                if e == libc::EINTR {
462                    continue;
463                }
464                return Err(CoreError::sys(e, "waitpid_step"));
465            }
466            return Ok(Some(decode_status(status)));
467        }
468    }
469
470    /// Block until the process terminates.
471    ///
472    /// ### Errors
473    /// - `ECHILD`: The process does not exist or is not a child of the caller.
474    pub fn wait_blocking(&self) -> Result<ExitStatus, CoreError> {
475        loop {
476            let mut status = 0;
477            let r = unsafe { waitpid(self.pid, &mut status, 0) };
478            if r < 0 {
479                let e = errno();
480                if e == libc::EINTR {
481                    continue;
482                }
483                return Err(CoreError::sys(e, "waitpid_blocking"));
484            }
485            return Ok(decode_status(status));
486        }
487    }
488
489    /// Send a signal to the process.
490    ///
491    /// ### Errors
492    /// - `EINVAL`: Invalid signal number.
493    /// - `EPERM`: The caller does not have permission to send the signal.
494    /// - `ESRCH`: The process does not exist.
495    pub fn kill(&self, sig: i32) -> Result<(), CoreError> {
496        let r = unsafe { libc::kill(self.pid, sig) };
497        if r < 0 {
498            let e = errno();
499            if e == libc::ESRCH {
500                return Ok(());
501            }
502            syscall_ret(-1, "kill")?;
503        }
504        Ok(())
505    }
506
507    /// Signal the process group whose id equals [`Self::pid`] — valid only
508    /// when the process is its own group/session leader. For a child placed
509    /// into a custom leader's group use [`Self::kill_group`].
510    ///
511    /// ### Errors
512    /// Same as [`Self::kill`].
513    pub fn kill_pgroup(&self, sig: i32) -> Result<(), CoreError> {
514        self.kill_group(self.pid, sig)
515    }
516
517    /// Send a signal to an explicit process group.
518    ///
519    /// The pgid must be the child's actual group (its own pid after `setsid`,
520    /// or the configured leader's id after `setpgid`), never guessed from the
521    /// pid.
522    ///
523    /// ### Errors
524    /// Same as [`Self::kill`].
525    pub fn kill_group(&self, pgid: pid_t, sig: i32) -> Result<(), CoreError> {
526        let r = unsafe { libc::kill(-pgid, sig) };
527        if r < 0 {
528            let e = errno();
529            if e == libc::ESRCH {
530                return Ok(());
531            }
532            syscall_ret(-1, "kill_group")?;
533        }
534        Ok(())
535    }
536}
537
538/// Configuration options for spawning a new process.
539#[derive(Clone)]
540pub struct SpawnOptions {
541    ctx: ExecContext,
542    stdin: Option<Box<[u8]>>,
543    capture_stdout: bool,
544    capture_stderr: bool,
545    wait: bool,
546    pgroup: ProcessGroup,
547    max_output: usize,
548    timeout_ms: Option<u32>,
549    kill_grace_ms: u32,
550    cancel: CancelPolicy,
551    backend: SpawnBackend,
552    fd_policy: SpawnFdPolicy,
553    early_exit: Option<fn(&[u8]) -> bool>,
554}
555
556impl SpawnOptions {
557    /// Create a new builder for process spawning.
558    pub fn builder(argv: Vec<String>, backend: SpawnBackend) -> SpawnOptionsBuilder {
559        SpawnOptionsBuilder::new(argv, backend)
560    }
561
562    /// Execute the process according to the options and block until completion.
563    pub fn run(self) -> Result<Output, CoreError> {
564        spawn(self)
565    }
566}
567
568/// Builder for [`SpawnOptions`].
569#[derive(Clone)]
570pub struct SpawnOptionsBuilder {
571    argv: Vec<String>,
572    env: Option<Vec<String>>,
573    cwd: Option<String>,
574    stdin: Option<Box<[u8]>>,
575    capture_stdout: bool,
576    capture_stderr: bool,
577    wait: bool,
578    pgroup: ProcessGroup,
579    max_output: usize,
580    timeout_ms: Option<u32>,
581    kill_grace_ms: u32,
582    cancel: CancelPolicy,
583    backend: SpawnBackend,
584    fd_policy: SpawnFdPolicy,
585    early_exit: Option<fn(&[u8]) -> bool>,
586}
587
588impl SpawnOptionsBuilder {
589    /// Create a new builder with the specified argument vector.
590    pub fn new(argv: Vec<String>, backend: SpawnBackend) -> Self {
591        Self {
592            argv,
593            env: None,
594            cwd: None,
595            stdin: None,
596            capture_stdout: false,
597            capture_stderr: false,
598            wait: true,
599            pgroup: ProcessGroup::default(),
600            max_output: 1024 * 1024,
601            timeout_ms: None,
602            kill_grace_ms: 2000,
603            cancel: CancelPolicy::Kill,
604            backend,
605            fd_policy: SpawnFdPolicy::default(),
606            early_exit: None,
607        }
608    }
609
610    /// Set environment variables.
611    pub fn env(mut self, env: Vec<String>) -> Self {
612        self.env = Some(env);
613        self
614    }
615
616    /// Set the working directory.
617    pub fn cwd(mut self, cwd: String) -> Self {
618        self.cwd = Some(cwd);
619        self
620    }
621
622    /// Provide data to be written to the child's stdin.
623    pub fn stdin(mut self, data: impl Into<Box<[u8]>>) -> Self {
624        self.stdin = Some(data.into());
625        self
626    }
627
628    /// Enable stdout capture.
629    pub fn capture_stdout(mut self) -> Self {
630        self.capture_stdout = true;
631        self
632    }
633
634    /// Enable stderr capture.
635    pub fn capture_stderr(mut self) -> Self {
636        self.capture_stderr = true;
637        self
638    }
639
640    /// Set whether to wait for the process to terminate (default: true).
641    pub fn wait(mut self, wait: bool) -> Self {
642        self.wait = wait;
643        self
644    }
645
646    /// Set process group and isolation policy.
647    pub fn pgroup(mut self, pgroup: ProcessGroup) -> Self {
648        self.pgroup = pgroup;
649        self
650    }
651
652    /// Set the combined stdout+stderr output buffer size (default: 1MB).
653    ///
654    /// If captured output exceeds this limit, spawn drains the child pipes to
655    /// completion and returns `EOVERFLOW`.
656    pub fn max_output(mut self, max: usize) -> Self {
657        self.max_output = max;
658        self
659    }
660
661    /// Set the execution timeout in milliseconds.
662    pub fn timeout_ms(mut self, ms: u32) -> Self {
663        self.timeout_ms = Some(ms);
664        self
665    }
666
667    /// Set the grace period before SIGKILL (default: 2s).
668    pub fn kill_grace_ms(mut self, ms: u32) -> Self {
669        self.kill_grace_ms = ms;
670        self
671    }
672
673    /// Set the cancellation policy (default: Kill).
674    pub fn cancel(mut self, policy: CancelPolicy) -> Self {
675        self.cancel = policy;
676        self
677    }
678
679    /// Set the child file-descriptor inheritance policy.
680    pub fn fd_policy(mut self, policy: SpawnFdPolicy) -> Self {
681        self.fd_policy = policy;
682        self
683    }
684
685    /// Set an early exit callback.
686    pub fn early_exit(mut self, callback: fn(&[u8]) -> bool) -> Self {
687        self.early_exit = Some(callback);
688        self
689    }
690
691    /// Build the spawn options.
692    pub fn build(self) -> Result<SpawnOptions, CoreError> {
693        let ctx = ExecContext::new(self.argv, self.env, self.cwd)?;
694        Ok(SpawnOptions {
695            ctx,
696            stdin: self.stdin,
697            capture_stdout: self.capture_stdout,
698            capture_stderr: self.capture_stderr,
699            wait: self.wait,
700            pgroup: self.pgroup,
701            max_output: self.max_output,
702            timeout_ms: self.timeout_ms,
703            kill_grace_ms: self.kill_grace_ms,
704            cancel: self.cancel,
705            backend: self.backend,
706            fd_policy: self.fd_policy,
707            early_exit: self.early_exit,
708        })
709    }
710}
711
712/// The result of a process execution.
713#[derive(Debug)]
714pub struct Output {
715    /// The PID of the finished process.
716    pub pid: pid_t,
717    /// Final exit status (None if `wait=false`).
718    pub status: Option<ExitStatus>,
719    /// Captured stdout buffer.
720    pub stdout: Vec<u8>,
721    /// Captured stderr buffer.
722    pub stderr: Vec<u8>,
723    /// Whether the process timed out.
724    pub timed_out: bool,
725    /// Whether stdout drain stopped because the early-exit callback matched.
726    pub stdout_early_exited: bool,
727}
728
729fn validate_backend(opts: &SpawnOptions) -> Result<(), CoreError> {
730    validate_fd_policy(&opts.fd_policy)?;
731    match opts.backend {
732        SpawnBackend::PosixSpawn => {
733            if opts.ctx.cwd.is_some() {
734                return Err(CoreError::sys(libc::EINVAL, "posix_spawn cwd unsupported"));
735            }
736            if opts.pgroup.isolated {
737                return Err(CoreError::sys(
738                    libc::EINVAL,
739                    "posix_spawn setsid unsupported",
740                ));
741            }
742            if opts.fd_policy != SpawnFdPolicy::CloexecOnly {
743                return Err(CoreError::sys(
744                    libc::EINVAL,
745                    "posix_spawn fd policy unsupported",
746                ));
747            }
748            Ok(())
749        }
750        SpawnBackend::Fork => Ok(()),
751    }
752}
753
754fn validate_fd_policy(policy: &SpawnFdPolicy) -> Result<(), CoreError> {
755    if let SpawnFdPolicy::Allowlist(fds) = policy {
756        let mut seen = Vec::with_capacity(fds.len());
757        for &fd in fds {
758            if fd < 0 {
759                return Err(CoreError::sys(libc::EINVAL, "spawn fd allowlist invalid"));
760            }
761            let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
762            if flags < 0 {
763                return Err(CoreError::sys(errno(), "spawn fd allowlist fcntl(F_GETFD)"));
764            }
765            if seen.contains(&fd) {
766                return Err(CoreError::sys(libc::EINVAL, "spawn fd allowlist duplicate"));
767            }
768            seen.push(fd);
769        }
770    }
771    Ok(())
772}
773
774use crate::io::DrainState;
775
776/// Specialized drain state for process spawning.
777pub type SpawnDrain = DrainState<fn(&[u8]) -> bool>;
778
779/// A process that is currently running and being monitored.
780///
781/// ### Fork Safety
782/// This handle contains both a PID and owned file descriptors for process I/O.
783/// Upon `fork`, the descriptors are inherited. Standard `O_CLOEXEC` behavior
784/// applies after `exec`.
785pub struct RunningProcess {
786    /// Handle to the process.
787    pub process: Process,
788    drain: SpawnDrain,
789}
790
791/// Full process lifecycle driven by a caller-owned reactor.
792///
793/// `ManagedProcess` preserves the blocking [`spawn`] semantics while allowing
794/// an application reactor to stay responsive: Core owns timeout/cancellation
795/// escalation, process-group signaling, pipe draining, overflow reporting, and
796/// `waitpid` reaping; the caller only routes readiness events and polls on
797/// [`Self::next_deadline`].
798pub struct ManagedProcess {
799    running: Option<RunningProcess>,
800    timeout_at: Option<Instant>,
801    kill_grace: Duration,
802    cancel: CancelPolicy,
803    pgroup: ProcessGroup,
804    cancel_at: Option<Instant>,
805    kill_state: KillState,
806    status: Option<ExitStatus>,
807    timed_out: bool,
808}
809
810impl RunningProcess {
811    /// Register active stdio pipe descriptors with a reactor.
812    ///
813    /// Call this once after [`spawn_start`] when the process was started with
814    /// captured output or stdin data. The assigned tokens are kept internally
815    /// and later matched by [`Self::handle_reactor_event`].
816    pub fn register_with_reactor(&mut self, reactor: &mut Reactor) -> Result<(), CoreError> {
817        self.drain.register_with_reactor(reactor)
818    }
819
820    /// Apply one reactor readiness event to this process' stdio drain state.
821    ///
822    /// Events for unrelated tokens are ignored. Callers remain responsible for
823    /// waiting on [`Self::process`] and driving the reactor until [`Self::io_done`]
824    /// returns true.
825    pub fn handle_reactor_event(
826        &mut self,
827        reactor: &mut Reactor,
828        event: &crate::fd::Event,
829    ) -> Result<(), CoreError> {
830        if self.drain.stdout_matches(event.token) {
831            if event.readable || event.hangup {
832                self.drain.handle_stdout_ready(reactor)?;
833            } else if event.error {
834                self.drain.drop_stdout(reactor)?;
835            }
836        } else if self.drain.stderr_matches(event.token) {
837            if event.readable || event.hangup {
838                self.drain.handle_stderr_ready(reactor)?;
839            } else if event.error {
840                self.drain.drop_stderr(reactor)?;
841            }
842        } else if self.drain.stdin_matches(event.token) {
843            if event.writable {
844                self.drain.handle_stdin_writable(reactor)?;
845            } else if event.error || event.hangup {
846                self.drain.drop_stdin(reactor)?;
847            }
848        }
849        Ok(())
850    }
851
852    /// Return whether all managed stdio pipes have been drained or closed.
853    pub fn io_done(&self) -> bool {
854        self.drain.is_done()
855    }
856
857    /// Consume the running process handle and return captured stdout/stderr buffers.
858    pub fn into_output_parts(self) -> (Vec<u8>, Vec<u8>) {
859        self.drain.into_parts()
860    }
861}
862
863impl ManagedProcess {
864    /// Return the child PID.
865    pub fn pid(&self) -> pid_t {
866        self.running
867            .as_ref()
868            .expect("managed process already completed")
869            .process
870            .pid()
871    }
872
873    /// Register active child I/O descriptors with the caller's reactor.
874    pub fn register_with_reactor(&mut self, reactor: &mut Reactor) -> Result<(), CoreError> {
875        self.running
876            .as_mut()
877            .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?
878            .register_with_reactor(reactor)
879    }
880
881    /// Route one reactor event to the child's I/O drain state.
882    pub fn handle_reactor_event(
883        &mut self,
884        reactor: &mut Reactor,
885        event: &crate::fd::Event,
886    ) -> Result<(), CoreError> {
887        self.running
888            .as_mut()
889            .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?
890            .handle_reactor_event(reactor, event)
891    }
892
893    /// Request cancellation using the daemon-owned policy from
894    /// [`SpawnOptionsBuilder::cancel`]. Repeated requests are idempotent.
895    pub fn request_cancel(&mut self) {
896        self.cancel_at.get_or_insert_with(Instant::now);
897    }
898
899    /// Earliest time at which [`Self::poll_completion`] should run again.
900    ///
901    /// A bounded reap tick is returned while the child is live, and exact
902    /// timeout / TERM-to-KILL deadlines take precedence. `None` means the
903    /// completion was already consumed.
904    pub fn next_deadline(&self) -> Option<Instant> {
905        self.running.as_ref()?;
906        let now = Instant::now();
907        let mut next = now + Duration::from_millis(100);
908        if !self.timed_out
909            && let Some(timeout_at) = self.timeout_at
910            && timeout_at < next
911        {
912            next = timeout_at;
913        }
914        if self.kill_state == KillState::TermSent
915            && let Some(cancel_at) = self.cancel_at
916        {
917            let kill_at = cancel_at + self.kill_grace;
918            if kill_at < next {
919                next = kill_at;
920            }
921        }
922        Some(next)
923    }
924
925    /// Advance timeout/cancellation, reap state, and completion.
926    ///
927    /// Returns `Ok(None)` while work remains, the normal [`Output`] once the
928    /// child is reaped and its pipes are drained, or `EOVERFLOW` when the
929    /// configured combined output limit was exceeded.
930    pub fn poll_completion(&mut self, reactor: &mut Reactor) -> Result<Option<Output>, CoreError> {
931        let now = Instant::now();
932        if !self.timed_out
933            && let Some(timeout_at) = self.timeout_at
934            && now >= timeout_at
935        {
936            self.timed_out = true;
937            self.cancel_at.get_or_insert(timeout_at);
938        }
939
940        self.advance_cancel(now)?;
941
942        let running = self
943            .running
944            .as_ref()
945            .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
946        if self.status.is_none() {
947            self.status = running.process.wait_step()?;
948        }
949
950        let io_done = running.io_done();
951        if self.status.is_some() && (io_done || self.cancel_at.is_some()) {
952            return self.finish(reactor, !io_done).map(Some);
953        }
954        Ok(None)
955    }
956
957    fn advance_cancel(&mut self, now: Instant) -> Result<(), CoreError> {
958        let Some(cancel_at) = self.cancel_at else {
959            return Ok(());
960        };
961        let running = self
962            .running
963            .as_ref()
964            .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
965        let process = &running.process;
966        let pid = process.pid();
967        let pgid = effective_pgid(pid, self.pgroup);
968        let target_is_group = self.pgroup.isolated || self.pgroup.leader.is_some();
969        match self.kill_state {
970            KillState::None => match self.cancel {
971                CancelPolicy::None => {}
972                CancelPolicy::Graceful => {
973                    let result = signal_process(process, target_is_group, pgid, libc::SIGTERM);
974                    self.kill_state = if result.is_ok() {
975                        KillState::TermSent
976                    } else {
977                        KillState::KillSent
978                    };
979                }
980                CancelPolicy::Kill => {
981                    let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
982                    self.kill_state = KillState::KillSent;
983                }
984            },
985            KillState::TermSent if now >= cancel_at + self.kill_grace => {
986                let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
987                self.kill_state = KillState::KillSent;
988            }
989            _ => {}
990        }
991        Ok(())
992    }
993
994    fn finish(&mut self, reactor: &mut Reactor, force_close: bool) -> Result<Output, CoreError> {
995        let mut running = self
996            .running
997            .take()
998            .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
999        for slot in running.drain.take_all_slots() {
1000            if force_close {
1001                let _ = reactor.del(&slot.fd);
1002            } else {
1003                reactor.del(&slot.fd)?;
1004            }
1005        }
1006        let pid = running.process.pid();
1007        let (stdout, stderr, output_limit_exceeded, stdout_early_exited) =
1008            running.drain.into_parts_with_state();
1009        if output_limit_exceeded {
1010            return Err(CoreError::sys(libc::EOVERFLOW, "spawn output limit"));
1011        }
1012        Ok(Output {
1013            pid,
1014            status: self.status.take(),
1015            stdout,
1016            stderr,
1017            timed_out: self.timed_out,
1018            stdout_early_exited,
1019        })
1020    }
1021}
1022
1023impl Drop for ManagedProcess {
1024    fn drop(&mut self) {
1025        let Some(running) = self.running.take() else {
1026            return;
1027        };
1028        let process = &running.process;
1029        let pid = process.pid();
1030        let pgid = effective_pgid(pid, self.pgroup);
1031        let target_is_group = self.pgroup.isolated || self.pgroup.leader.is_some();
1032        let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
1033        let _ = process.wait_blocking();
1034    }
1035}
1036
1037fn effective_pgid(pid: pid_t, pgroup: ProcessGroup) -> pid_t {
1038    match pgroup.leader {
1039        Some(0) | None => pid,
1040        Some(leader) => leader,
1041    }
1042}
1043
1044fn signal_process(
1045    process: &Process,
1046    target_is_group: bool,
1047    pgid: pid_t,
1048    signal: i32,
1049) -> Result<(), CoreError> {
1050    if target_is_group {
1051        process.kill_group(pgid, signal)
1052    } else {
1053        process.kill(signal)
1054    }
1055}
1056
1057use crate::reactor::Reactor;
1058
1059/// Start spawning a process and return a monitor handle.
1060///
1061/// This initializes the pipes and starts the process, but does not block. Use
1062/// [`RunningProcess::register_with_reactor`],
1063/// [`RunningProcess::handle_reactor_event`], [`RunningProcess::io_done`], and
1064/// [`RunningProcess::into_output_parts`] to drive captured stdio without
1065/// exposing internal drain state.
1066///
1067/// ### Errors
1068/// - `EACCES`: Permission denied for the executable.
1069/// - `EINVAL`: Invalid spawn options (e.g. background capture without wait).
1070/// - `EMFILE`: Process limit on open file descriptors hit.
1071/// - `ENOENT`: The executable was not found.
1072/// - `ENOMEM`: Insufficient memory to spawn the process.
1073pub fn spawn_start(opts: SpawnOptions) -> Result<RunningProcess, CoreError> {
1074    if !opts.wait && (opts.stdin.is_some() || opts.capture_stdout || opts.capture_stderr) {
1075        return Err(CoreError::sys(
1076            libc::EINVAL,
1077            "background I/O capture not supported (wait must be true)",
1078        ));
1079    }
1080
1081    validate_backend(&opts)?;
1082
1083    let (pid, drain) = match opts.backend {
1084        SpawnBackend::PosixSpawn => spawn_posix_internal(opts)?,
1085        SpawnBackend::Fork => spawn_fork_internal(opts)?,
1086    };
1087
1088    Ok(RunningProcess {
1089        process: Process::new(pid),
1090        drain,
1091    })
1092}
1093
1094/// Start a process whose complete lifecycle is driven by a caller-owned
1095/// reactor.
1096pub fn spawn_managed(opts: SpawnOptions) -> Result<ManagedProcess, CoreError> {
1097    if !opts.wait {
1098        return Err(CoreError::sys(
1099            libc::EINVAL,
1100            "managed process requires wait=true",
1101        ));
1102    }
1103    let timeout_at = opts
1104        .timeout_ms
1105        .map(|ms| Instant::now() + Duration::from_millis(ms as u64));
1106    let kill_grace = Duration::from_millis(opts.kill_grace_ms as u64);
1107    let cancel = opts.cancel;
1108    let pgroup = opts.pgroup;
1109    let running = spawn_start(opts)?;
1110    Ok(ManagedProcess {
1111        running: Some(running),
1112        timeout_at,
1113        kill_grace,
1114        cancel,
1115        pgroup,
1116        cancel_at: None,
1117        kill_state: KillState::None,
1118        status: None,
1119        timed_out: false,
1120    })
1121}
1122
1123/// Spawn a process and block until completion or timeout.
1124///
1125/// This is the primary high-level interface for process execution. It handles
1126/// the full lifecycle, including I/O multiplexing and signal management.
1127///
1128/// ### Errors
1129/// Returns the same errors as [`spawn_start`], plus any I/O or reactor errors
1130/// encountered during the wait loop.
1131pub fn spawn(opts: SpawnOptions) -> Result<Output, CoreError> {
1132    let wait = opts.wait;
1133    let timeout_ms = opts.timeout_ms;
1134    let kill_grace_ms = opts.kill_grace_ms;
1135    let cancel = opts.cancel;
1136    let pgroup = opts.pgroup;
1137
1138    let mut reactor = Reactor::new()?;
1139    let running = spawn_start(opts)?;
1140
1141    let pid = running.process.pid();
1142    let mut drain = running.drain;
1143
1144    drain.register_with_reactor(&mut reactor)?;
1145
1146    if !wait {
1147        let (stdout, stderr) = drain.into_parts();
1148        return Ok(Output {
1149            pid,
1150            status: None,
1151            stdout,
1152            stderr,
1153            timed_out: false,
1154            stdout_early_exited: false,
1155        });
1156    }
1157
1158    wait_loop(
1159        pid,
1160        drain,
1161        reactor,
1162        timeout_ms,
1163        kill_grace_ms,
1164        cancel,
1165        pgroup,
1166    )
1167}
1168
1169fn spawn_posix_internal(opts: SpawnOptions) -> Result<(pid_t, SpawnDrain), CoreError> {
1170    let mut pipes = Pipes::new(
1171        opts.stdin.as_deref(),
1172        opts.capture_stdout,
1173        opts.capture_stderr,
1174    )?;
1175
1176    let exe_ptr = match &opts.ctx.argv {
1177        ExecArgv::Dynamic(v) => v[0].as_ptr(),
1178    };
1179
1180    let argv = opts.ctx.get_argv_ptrs();
1181    let envp = opts.ctx.get_envp_ptrs();
1182
1183    let actions = MaybeUninit::zeroed();
1184    let mut actions = unsafe { actions.assume_init() };
1185    if let Err(e) = posix_ret(
1186        unsafe { posix_spawn_file_actions_init(&mut actions) },
1187        "file_actions_init",
1188    ) {
1189        pipes.close_all();
1190        return Err(e);
1191    }
1192
1193    struct Actions(*mut libc::posix_spawn_file_actions_t);
1194    impl Drop for Actions {
1195        fn drop(&mut self) {
1196            unsafe {
1197                posix_spawn_file_actions_destroy(self.0);
1198            }
1199        }
1200    }
1201    let _guard = Actions(&mut actions);
1202
1203    if let (Some(r), Some(w)) = (&pipes.stdin_r, &pipes.stdin_w) {
1204        if let Err(e) = posix_ret(
1205            unsafe { posix_spawn_file_actions_adddup2(&mut actions, r.raw(), 0) },
1206            "dup2 stdin",
1207        ) {
1208            pipes.close_all();
1209            return Err(e);
1210        }
1211        if let Err(e) = posix_ret(
1212            unsafe { posix_spawn_file_actions_addclose(&mut actions, r.raw()) },
1213            "close stdin pipe",
1214        ) {
1215            pipes.close_all();
1216            return Err(e);
1217        }
1218        if let Err(e) = posix_ret(
1219            unsafe { posix_spawn_file_actions_addclose(&mut actions, w.raw()) },
1220            "close stdin write pipe",
1221        ) {
1222            pipes.close_all();
1223            return Err(e);
1224        }
1225    }
1226
1227    if let (Some(r), Some(w)) = (&pipes.stdout_r, &pipes.stdout_w) {
1228        if let Err(e) = posix_ret(
1229            unsafe { posix_spawn_file_actions_adddup2(&mut actions, w.raw(), 1) },
1230            "dup2 stdout",
1231        ) {
1232            pipes.close_all();
1233            return Err(e);
1234        }
1235        if let Err(e) = posix_ret(
1236            unsafe { posix_spawn_file_actions_addclose(&mut actions, w.raw()) },
1237            "close stdout pipe",
1238        ) {
1239            pipes.close_all();
1240            return Err(e);
1241        }
1242        if let Err(e) = posix_ret(
1243            unsafe { posix_spawn_file_actions_addclose(&mut actions, r.raw()) },
1244            "close stdout read pipe",
1245        ) {
1246            pipes.close_all();
1247            return Err(e);
1248        }
1249    }
1250
1251    if let (Some(r), Some(w)) = (&pipes.stderr_r, &pipes.stderr_w) {
1252        if let Err(e) = posix_ret(
1253            unsafe { posix_spawn_file_actions_adddup2(&mut actions, w.raw(), 2) },
1254            "dup2 stderr",
1255        ) {
1256            pipes.close_all();
1257            return Err(e);
1258        }
1259        if let Err(e) = posix_ret(
1260            unsafe { posix_spawn_file_actions_addclose(&mut actions, w.raw()) },
1261            "close stderr pipe",
1262        ) {
1263            pipes.close_all();
1264            return Err(e);
1265        }
1266        if let Err(e) = posix_ret(
1267            unsafe { posix_spawn_file_actions_addclose(&mut actions, r.raw()) },
1268            "close stderr read pipe",
1269        ) {
1270            pipes.close_all();
1271            return Err(e);
1272        }
1273    }
1274
1275    let attr = MaybeUninit::zeroed();
1276    let mut attr = unsafe { attr.assume_init() };
1277    if let Err(e) = posix_ret(unsafe { posix_spawnattr_init(&mut attr) }, "attr_init") {
1278        pipes.close_all();
1279        return Err(e);
1280    }
1281
1282    struct Attr(*mut libc::posix_spawnattr_t);
1283    impl Drop for Attr {
1284        fn drop(&mut self) {
1285            unsafe {
1286                posix_spawnattr_destroy(self.0);
1287            }
1288        }
1289    }
1290    let _attr = Attr(&mut attr);
1291
1292    let mut flags = 0;
1293
1294    if let Some(pg) = opts.pgroup.leader {
1295        flags |= POSIX_SPAWN_SETPGROUP;
1296        if let Err(e) = posix_ret(
1297            unsafe { posix_spawnattr_setpgroup(&mut attr, pg) },
1298            "setpgroup",
1299        ) {
1300            pipes.close_all();
1301            return Err(e);
1302        }
1303    }
1304
1305    flags |= POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF;
1306
1307    if let Err(e) = posix_ret(
1308        unsafe { posix_spawnattr_setflags(&mut attr, flags as _) },
1309        "setflags",
1310    ) {
1311        pipes.close_all();
1312        return Err(e);
1313    }
1314
1315    let empty_mask = SignalRuntime::empty_set();
1316    let def = SignalRuntime::set_with(&[libc::SIGPIPE])?;
1317
1318    if let Err(e) = posix_ret(
1319        unsafe { posix_spawnattr_setsigmask(&mut attr, &empty_mask) },
1320        "setsigmask",
1321    ) {
1322        pipes.close_all();
1323        return Err(e);
1324    }
1325    if let Err(e) = posix_ret(
1326        unsafe { posix_spawnattr_setsigdefault(&mut attr, &def) },
1327        "setsigdefault",
1328    ) {
1329        pipes.close_all();
1330        return Err(e);
1331    }
1332
1333    let mut pid: pid_t = 0;
1334
1335    let envp_ptr = envp.as_ref().map_or_else(
1336        || unsafe { environ as *const *mut c_char },
1337        |e: &Vec<*mut c_char>| e.as_ptr(),
1338    );
1339
1340    if let Err(e) = posix_ret(
1341        unsafe { posix_spawn(&mut pid, exe_ptr, &actions, &attr, argv.as_ptr(), envp_ptr) },
1342        "posix_spawn",
1343    ) {
1344        pipes.close_all();
1345        return Err(e);
1346    }
1347
1348    drop(pipes.stdin_r.take());
1349    drop(pipes.stdout_w.take());
1350    drop(pipes.stderr_w.take());
1351
1352    let drain = crate::io::DrainState::new(
1353        pipes.stdin_w.take().filter(|_| opts.stdin.is_some()),
1354        opts.stdin,
1355        pipes.stdout_r.take(),
1356        pipes.stderr_r.take(),
1357        opts.max_output,
1358        opts.early_exit,
1359    )?;
1360
1361    Ok((pid, drain))
1362}
1363
1364fn collect_required_pipe_fds(pipes: &Pipes) -> Vec<RawFd> {
1365    let mut fds = Vec::new();
1366    if let Some(fd) = &pipes.stdin_r {
1367        fds.push(fd.raw());
1368    }
1369    if let Some(fd) = &pipes.stdin_w {
1370        fds.push(fd.raw());
1371    }
1372    if let Some(fd) = &pipes.stdout_r {
1373        fds.push(fd.raw());
1374    }
1375    if let Some(fd) = &pipes.stdout_w {
1376        fds.push(fd.raw());
1377    }
1378    if let Some(fd) = &pipes.stderr_r {
1379        fds.push(fd.raw());
1380    }
1381    if let Some(fd) = &pipes.stderr_w {
1382        fds.push(fd.raw());
1383    }
1384    fds
1385}
1386
1387fn collect_open_fds_for_child_policy(policy: &SpawnFdPolicy) -> Result<Vec<RawFd>, CoreError> {
1388    match policy {
1389        SpawnFdPolicy::CloexecOnly => Ok(Vec::new()),
1390        SpawnFdPolicy::CloseFrom3 | SpawnFdPolicy::Allowlist(_) => {
1391            let dir_fd = unsafe {
1392                libc::open(
1393                    c"/proc/self/fd".as_ptr(),
1394                    libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
1395                )
1396            };
1397            if dir_fd < 0 {
1398                return Err(CoreError::sys(errno(), "open /proc/self/fd"));
1399            }
1400
1401            let dir = unsafe { libc::fdopendir(dir_fd) };
1402            if dir.is_null() {
1403                let code = errno();
1404                unsafe {
1405                    libc::close(dir_fd);
1406                }
1407                return Err(CoreError::sys(code, "fdopendir /proc/self/fd"));
1408            }
1409
1410            let mut open_fds = Vec::new();
1411            loop {
1412                let entry = unsafe { libc::readdir(dir) };
1413                if entry.is_null() {
1414                    break;
1415                }
1416                let name = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) };
1417                if let Ok(s) = name.to_str()
1418                    && let Ok(fd) = s.parse::<RawFd>()
1419                    && fd != dir_fd
1420                {
1421                    open_fds.push(fd);
1422                }
1423            }
1424            unsafe {
1425                libc::closedir(dir);
1426            }
1427            Ok(open_fds)
1428        }
1429    }
1430}
1431
1432fn close_child_fds_for_policy(policy: &SpawnFdPolicy, required_fds: &[RawFd], open_fds: &[RawFd]) {
1433    match policy {
1434        SpawnFdPolicy::CloexecOnly => {}
1435        SpawnFdPolicy::CloseFrom3 | SpawnFdPolicy::Allowlist(_) => {
1436            for &fd in open_fds {
1437                if fd > 2
1438                    && !required_fds.contains(&fd)
1439                    && !matches!(policy, SpawnFdPolicy::Allowlist(allowlist) if allowlist.contains(&fd))
1440                {
1441                    unsafe {
1442                        libc::close(fd);
1443                    }
1444                }
1445            }
1446        }
1447    }
1448}
1449
1450fn spawn_fork_internal(opts: SpawnOptions) -> Result<(pid_t, SpawnDrain), CoreError> {
1451    let mut pipes = Pipes::new(
1452        opts.stdin.as_deref(),
1453        opts.capture_stdout,
1454        opts.capture_stderr,
1455    )?;
1456
1457    let exe_ptr = match &opts.ctx.argv {
1458        ExecArgv::Dynamic(v) => v[0].as_ptr(),
1459    };
1460
1461    let argv = opts.ctx.get_argv_ptrs();
1462    let envp = opts.ctx.get_envp_ptrs();
1463    let cwd_cstr = &opts.ctx.cwd;
1464    let (child_error_r, child_error_w) = make_cloexec_pipe()?;
1465    let mut required_fds = collect_required_pipe_fds(&pipes);
1466    required_fds.push(child_error_w);
1467    let open_fds = collect_open_fds_for_child_policy(&opts.fd_policy)?;
1468
1469    let pid = unsafe { libc::fork() };
1470
1471    if pid < 0 {
1472        unsafe {
1473            libc::close(child_error_r);
1474            libc::close(child_error_w);
1475        }
1476        pipes.close_all();
1477        syscall_ret(-1, "fork")?;
1478    }
1479
1480    if pid == 0 {
1481        // Child
1482        unsafe {
1483            libc::close(child_error_r);
1484        }
1485
1486        // dup stdin
1487        if let (Some(r), Some(_)) = (&pipes.stdin_r, &pipes.stdin_w) {
1488            unsafe {
1489                if libc::dup2(r.raw(), 0) < 0 {
1490                    report_child_setup_error(child_error_w, ChildSetupOp::DupStdin, errno());
1491                }
1492            }
1493        }
1494
1495        // dup stdout
1496        if let (Some(_), Some(w)) = (&pipes.stdout_r, &pipes.stdout_w) {
1497            unsafe {
1498                if libc::dup2(w.raw(), 1) < 0 {
1499                    report_child_setup_error(child_error_w, ChildSetupOp::DupStdout, errno());
1500                }
1501            }
1502        }
1503
1504        // dup stderr
1505        if let (Some(_), Some(w)) = (&pipes.stderr_r, &pipes.stderr_w) {
1506            unsafe {
1507                if libc::dup2(w.raw(), 2) < 0 {
1508                    report_child_setup_error(child_error_w, ChildSetupOp::DupStderr, errno());
1509                }
1510            }
1511        }
1512
1513        // SAFETY: Close all pipe FDs in child before exec, except the ones duped to 0,1,2.
1514        pipes.close_all();
1515
1516        close_child_fds_for_policy(&opts.fd_policy, &required_fds, &open_fds);
1517
1518        // setsid
1519        if opts.pgroup.isolated {
1520            // SAFETY: safe to call setsid in child.
1521            unsafe {
1522                if libc::setsid() < 0 {
1523                    report_child_setup_error(child_error_w, ChildSetupOp::Setsid, errno());
1524                }
1525            }
1526        }
1527
1528        // chdir
1529        if let Some(cwd) = cwd_cstr {
1530            // SAFETY: cwd is a valid null-terminated CString.
1531            unsafe {
1532                if libc::chdir(cwd.as_ptr()) != 0 {
1533                    report_child_setup_error(child_error_w, ChildSetupOp::Chdir, errno());
1534                }
1535            }
1536        }
1537
1538        // setpgid
1539        if let Some(pg) = opts.pgroup.leader {
1540            // SAFETY: valid pgroup.
1541            unsafe {
1542                if libc::setpgid(0, pg) < 0 {
1543                    report_child_setup_error(child_error_w, ChildSetupOp::Setpgid, errno());
1544                }
1545            }
1546        }
1547
1548        let envp_ptr = envp.as_ref().map_or_else(
1549            || unsafe { environ as *const *mut c_char },
1550            |e: &Vec<*mut c_char>| e.as_ptr(),
1551        );
1552
1553        // unblock signals and reset SIGPIPE
1554        // SAFETY: valid signal mask array manipulation
1555        if let Err(err) = SignalRuntime::unblock_all() {
1556            unsafe {
1557                report_child_setup_error(
1558                    child_error_w,
1559                    ChildSetupOp::SignalMask,
1560                    err.raw_os_error().unwrap_or(libc::EIO),
1561                );
1562            }
1563        }
1564        if let Err(err) = SignalRuntime::reset_default(libc::SIGPIPE) {
1565            unsafe {
1566                report_child_setup_error(
1567                    child_error_w,
1568                    ChildSetupOp::SignalMask,
1569                    err.raw_os_error().unwrap_or(libc::EIO),
1570                );
1571            }
1572        }
1573
1574        // exec
1575        // SAFETY: exe_ptr is null-terminated. argv and envp_ptr are valid null-terminated arrays.
1576        unsafe {
1577            libc::execve(
1578                exe_ptr,
1579                argv.as_ptr() as *const *const _,
1580                envp_ptr as *const *const _,
1581            );
1582            report_child_setup_error(child_error_w, ChildSetupOp::Execve, errno());
1583        }
1584    }
1585
1586    // Parent
1587    unsafe {
1588        libc::close(child_error_w);
1589    }
1590    match read_child_setup_error(child_error_r) {
1591        Ok(Some(err)) => {
1592            unsafe {
1593                libc::close(child_error_r);
1594                let mut status = 0;
1595                let _ = libc::waitpid(pid, &mut status, 0);
1596            }
1597            pipes.close_all();
1598            return Err(err);
1599        }
1600        Ok(None) => {}
1601        Err(err) => {
1602            unsafe {
1603                libc::close(child_error_r);
1604            }
1605            pipes.close_all();
1606            return Err(err);
1607        }
1608    }
1609    unsafe {
1610        libc::close(child_error_r);
1611    }
1612    drop(pipes.stdin_r.take());
1613    drop(pipes.stdout_w.take());
1614    drop(pipes.stderr_w.take());
1615
1616    let drain = crate::io::DrainState::new(
1617        pipes.stdin_w.take().filter(|_| opts.stdin.is_some()),
1618        opts.stdin,
1619        pipes.stdout_r.take(),
1620        pipes.stderr_r.take(),
1621        opts.max_output,
1622        opts.early_exit,
1623    )?;
1624
1625    Ok((pid, drain))
1626}
1627
1628#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1629enum KillState {
1630    None,
1631    TermSent,
1632    KillSent,
1633}
1634
1635fn wait_loop(
1636    pid: pid_t,
1637    mut drain: crate::io::DrainState<fn(&[u8]) -> bool>,
1638    mut reactor: Reactor,
1639    timeout_ms: Option<u32>,
1640    kill_grace_ms: u32,
1641    cancel: CancelPolicy,
1642    pgroup: ProcessGroup,
1643) -> Result<Output, CoreError> {
1644    let process = Process::new(pid);
1645    // M8: the child's effective pgid is the configured leader when one is set
1646    // (Setpgid is applied after Setsid in the child), else its own pid. A
1647    // timeout must signal `-pgid`; `kill(-pid)` would target a different
1648    // group for a custom leader and the child would never die.
1649    let pgid = effective_pgid(pid, pgroup);
1650    let mut status_raw = process.wait_step()?;
1651    let mut state = KillState::None;
1652    let mut timed_out = false;
1653
1654    let start_time = std::time::Instant::now();
1655    let deadline = timeout_ms.map(|t| std::time::Duration::from_millis(t as u64));
1656
1657    loop {
1658        let mut poll_timeout = -1;
1659
1660        if let Some(dl) = deadline {
1661            let elapsed = start_time.elapsed();
1662            if elapsed >= dl {
1663                timed_out = true;
1664                let elapsed_over = (elapsed - dl).as_millis();
1665
1666                let target_is_group = pgroup.isolated || pgroup.leader.is_some();
1667
1668                match state {
1669                    KillState::None => {
1670                        if cancel == CancelPolicy::Graceful {
1671                            let r = if target_is_group {
1672                                process.kill_group(pgid, libc::SIGTERM)
1673                            } else {
1674                                process.kill(libc::SIGTERM)
1675                            };
1676                            if r.is_err() {
1677                                state = KillState::KillSent; // Process already gone
1678                            } else {
1679                                state = KillState::TermSent;
1680                            }
1681                        } else if cancel == CancelPolicy::Kill {
1682                            let _ = if target_is_group {
1683                                process.kill_group(pgid, libc::SIGKILL)
1684                            } else {
1685                                process.kill(libc::SIGKILL)
1686                            };
1687                            state = KillState::KillSent;
1688                        } else {
1689                            // CancelPolicy::None just times out without killing
1690                        }
1691                    }
1692                    KillState::TermSent if elapsed_over > kill_grace_ms as u128 => {
1693                        let _ = if target_is_group {
1694                            process.kill_group(pgid, libc::SIGKILL)
1695                        } else {
1696                            process.kill(libc::SIGKILL)
1697                        };
1698                        state = KillState::KillSent;
1699                    }
1700                    _ => {}
1701                }
1702                poll_timeout = 100; // Poll frequently while waiting for kill to take effect
1703            } else {
1704                let remaining = dl - elapsed;
1705                poll_timeout = remaining.as_millis().min(i32::MAX as u128) as i32;
1706            }
1707        }
1708
1709        if status_raw.is_none()
1710            && let Some(s) = process.wait_step()?
1711        {
1712            status_raw = Some(s);
1713        }
1714
1715        if drain.is_done() {
1716            let s = if status_raw.is_some() {
1717                status_raw.take()
1718            } else if deadline.is_none() {
1719                // C1: all pipes drained but the child is still alive, and no
1720                // deadline is set → block until it exits (intended semantics).
1721                Some(process.wait_blocking()?)
1722            } else {
1723                // C1: pipes drained with a deadline set → never block here; fall
1724                // through to the bounded `reactor.wait` below so the deadline
1725                // logic at the top of the loop kills and reaps. A later
1726                // `wait_step` reaps the child and we return from this branch.
1727                None
1728            };
1729
1730            if let Some(s) = s {
1731                for slot in drain.take_all_slots() {
1732                    reactor.del(&slot.fd)?;
1733                }
1734                let (stdout, stderr, output_limit_exceeded, stdout_early_exited) =
1735                    drain.into_parts_with_state();
1736                if output_limit_exceeded {
1737                    return Err(CoreError::sys(libc::EOVERFLOW, "spawn output limit"));
1738                }
1739                return Ok(Output {
1740                    pid,
1741                    status: Some(s),
1742                    stdout,
1743                    stderr,
1744                    timed_out,
1745                    stdout_early_exited,
1746                });
1747            }
1748        }
1749
1750        // N4: the deadline has elapsed and the child is reaped, but a wedged
1751        // pipe (a descendant inheriting the write end) keeps the drain from
1752        // closing. The absolute deadline is authoritative — return the partial
1753        // output instead of spinning forever.
1754        if timed_out && status_raw.is_some() {
1755            for slot in drain.take_all_slots() {
1756                let _ = reactor.del(&slot.fd);
1757            }
1758            let (stdout, stderr, _output_limit_exceeded, stdout_early_exited) =
1759                drain.into_parts_with_state();
1760            return Ok(Output {
1761                pid,
1762                status: status_raw,
1763                stdout,
1764                stderr,
1765                timed_out: true,
1766                stdout_early_exited,
1767            });
1768        }
1769
1770        let timeout = poll_timeout;
1771
1772        let mut events = Vec::new();
1773        let nevents = reactor.wait(&mut events, 64, timeout)?;
1774
1775        for ev in events.iter().take(nevents) {
1776            if drain.stdout_matches(ev.token) {
1777                if ev.readable || ev.hangup {
1778                    drain.handle_stdout_ready(&mut reactor)?;
1779                } else if ev.error {
1780                    drain.drop_stdout(&mut reactor)?;
1781                }
1782            } else if drain.stderr_matches(ev.token) {
1783                if ev.readable || ev.hangup {
1784                    drain.handle_stderr_ready(&mut reactor)?;
1785                } else if ev.error {
1786                    drain.drop_stderr(&mut reactor)?;
1787                }
1788            } else if drain.stdin_matches(ev.token) {
1789                if ev.writable {
1790                    drain.handle_stdin_writable(&mut reactor)?;
1791                } else if ev.error || ev.hangup {
1792                    drain.drop_stdin(&mut reactor)?;
1793                }
1794            }
1795        }
1796    }
1797}