Skip to main content

alpha2_os/
lifetime.rs

1//! Process and child lifetime: exit without handlers, parent-exit watching, standard handle
2//! inheritance, and a job that kills its children.
3
4use std::io;
5
6use crate::Termination;
7
8/// Ends the process now with `code`, without running exit handlers. The caller must flush first.
9///
10/// A thread stuck inside a GPU driver's hardware decoder makes the driver's exit handlers wait
11/// forever. On Windows `std::process::exit` and `_exit` both go through `ExitProcess`, which runs
12/// DLL detach code that can wait on a lock a stopped thread holds; `TerminateProcess` runs none.
13pub fn exit_without_handlers(code: u8) -> ! {
14    #[cfg(unix)]
15    {
16        // SAFETY: `_exit` accepts any status and never returns.
17        unsafe { libc::_exit(libc::c_int::from(code)) }
18    }
19    #[cfg(windows)]
20    {
21        use windows_sys::Win32::System::Threading::{GetCurrentProcess, TerminateProcess};
22        // SAFETY: the current-process pseudo-handle has every access right, including terminate.
23        if unsafe { TerminateProcess(GetCurrentProcess(), u32::from(code)) } != 0 {
24            loop {
25                std::thread::park();
26            }
27        }
28        std::process::exit(i32::from(code))
29    }
30    #[cfg(not(any(unix, windows)))]
31    std::process::exit(i32::from(code))
32}
33
34/// Stops children from implicitly inheriting this process's standard handles (Windows only).
35///
36/// Windows hands a child every inheritable handle when its streams are redirected, so a child
37/// would keep this process's pipes open and a client waiting for EOF would wait for the child.
38/// `Stdio::inherit` still works because the standard library duplicates the handle.
39pub fn stop_std_handle_inheritance() {
40    #[cfg(windows)]
41    {
42        use windows_sys::Win32::Foundation::{
43            HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, SetHandleInformation,
44        };
45        use windows_sys::Win32::System::Console::{
46            GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
47        };
48        for stream in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
49            // SAFETY: any standard handle id may be asked for; the handle is not closed here.
50            let handle = unsafe { GetStdHandle(stream) };
51            if handle.is_null() || handle == INVALID_HANDLE_VALUE {
52                continue;
53            }
54            // SAFETY: `handle` is open for the process's life; only its inherit flag changes.
55            let _ = unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0) };
56        }
57    }
58}
59
60/// Reported like `SIGTERM`.
61const PARENT_EXITED: Termination = Termination::new("PARENT_EXITED", 143, false);
62
63/// Calls the [`crate::on_termination`] handler with a `PARENT_EXITED` termination (status 143)
64/// when the parent process exits, or at once if it already has.
65///
66/// - Linux: waits on a `pidfd`. Kernels before 5.3 fall back to `PR_SET_PDEATHSIG`, which
67///   delivers `SIGTERM` and also fires when the parent's spawning thread exits.
68/// - macOS: waits on a `kqueue` for `NOTE_EXIT`.
69/// - Windows: waits on the parent's handle; a process with that id created after this one means
70///   the id was reused and the parent is gone.
71///
72/// A Unix parent of pid 1 is not watched.
73///
74/// # Errors
75/// When no termination handler is installed, or the parent cannot be watched.
76pub fn watch_parent() -> io::Result<()> {
77    let handler = crate::platform::termination_handler().ok_or_else(|| {
78        io::Error::new(
79            io::ErrorKind::NotFound,
80            "no termination handler is installed",
81        )
82    })?;
83    let Some(parent) = platform::Parent::open()? else {
84        return Ok(());
85    };
86    std::thread::Builder::new()
87        .name("alpha2-parent-watch".to_owned())
88        .spawn(move || {
89            if parent.wait() {
90                handler(PARENT_EXITED);
91            }
92        })?;
93    Ok(())
94}
95
96/// A Windows job that kills its processes when dropped or when this process ends, for probe
97/// children that may hang. A no-op elsewhere.
98pub struct KillOnCloseJob {
99    #[cfg(windows)]
100    handle: platform::OwnedHandle,
101}
102
103impl KillOnCloseJob {
104    /// Creates the job.
105    ///
106    /// # Errors
107    /// When the operating system refuses the job.
108    pub fn new() -> io::Result<Self> {
109        Ok(Self {
110            #[cfg(windows)]
111            handle: platform::kill_on_close_job()?,
112        })
113    }
114
115    /// Puts `child` in the job; call right after spawning.
116    ///
117    /// # Errors
118    /// When the OS refuses, for example because the child already exited.
119    pub fn assign(&self, child: &std::process::Child) -> io::Result<()> {
120        #[cfg(windows)]
121        {
122            platform::assign(&self.handle, child)
123        }
124        #[cfg(not(windows))]
125        {
126            let _ = child;
127            Ok(())
128        }
129    }
130}
131
132impl std::fmt::Debug for KillOnCloseJob {
133    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        formatter
135            .debug_struct("KillOnCloseJob")
136            .finish_non_exhaustive()
137    }
138}
139
140#[cfg(target_os = "linux")]
141mod platform {
142    use std::io;
143
144    /// The parent's `pidfd`, or `None` if the parent is gone.
145    pub(super) struct Parent(Option<std::fs::File>);
146
147    impl Parent {
148        pub(super) fn open() -> io::Result<Option<Self>> {
149            use std::os::fd::FromRawFd;
150            // SAFETY: `getppid` cannot fail.
151            let parent = unsafe { libc::getppid() };
152            if parent <= 1 {
153                return Ok(None);
154            }
155            // SAFETY: `pidfd_open` takes a pid and flags and returns a new descriptor or -1.
156            let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, parent, 0) };
157            if fd < 0 {
158                let error = io::Error::last_os_error();
159                return match error.raw_os_error() {
160                    Some(libc::ESRCH) => Ok(Some(Self(None))),
161                    Some(libc::ENOSYS) => death_signal(parent),
162                    _ => Err(error),
163                };
164            }
165            let fd = libc::c_int::try_from(fd)
166                .map_err(|_| io::Error::other("pidfd_open returned an impossible descriptor"))?;
167            // SAFETY: `fd` was just opened by `pidfd_open` and is owned by nothing else.
168            let pidfd = unsafe { std::fs::File::from_raw_fd(fd) };
169            // The parent may have exited before `pidfd_open`; then this process was reparented.
170            // SAFETY: `getppid` cannot fail.
171            if unsafe { libc::getppid() } != parent {
172                return Ok(Some(Self(None)));
173            }
174            Ok(Some(Self(Some(pidfd))))
175        }
176
177        /// Blocks until the parent exits; `false` if the wait failed.
178        pub(super) fn wait(&self) -> bool {
179            use std::os::fd::AsRawFd;
180            let Some(pidfd) = &self.0 else {
181                return true;
182            };
183            let mut poll = libc::pollfd {
184                fd: pidfd.as_raw_fd(),
185                events: libc::POLLIN,
186                revents: 0,
187            };
188            loop {
189                // SAFETY: `poll` is one valid `pollfd` that outlives the call.
190                let ready = unsafe { libc::poll(&raw mut poll, 1, -1) };
191                if ready > 0 {
192                    return true;
193                }
194                if ready < 0 && io::Error::last_os_error().kind() != io::ErrorKind::Interrupted {
195                    return false;
196                }
197            }
198        }
199    }
200
201    /// Fallback for kernels without `pidfd_open`.
202    fn death_signal(parent: libc::pid_t) -> io::Result<Option<Parent>> {
203        // SAFETY: `PR_SET_PDEATHSIG` takes a signal number and changes nothing else.
204        if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) } != 0 {
205            return Err(io::Error::last_os_error());
206        }
207        // A parent that exited before the call sends nothing.
208        // SAFETY: `getppid` cannot fail.
209        if unsafe { libc::getppid() } != parent {
210            return Ok(Some(Parent(None)));
211        }
212        Ok(None)
213    }
214}
215
216#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
217mod platform {
218    use std::io;
219
220    /// A `kqueue` watching the parent's `NOTE_EXIT`, or `None` if the parent is gone.
221    pub(super) struct Parent(Option<std::fs::File>);
222
223    impl Parent {
224        pub(super) fn open() -> io::Result<Option<Self>> {
225            use std::os::fd::{AsRawFd, FromRawFd};
226            // SAFETY: `getppid` cannot fail.
227            let parent = unsafe { libc::getppid() };
228            if parent <= 1 {
229                return Ok(None);
230            }
231            // SAFETY: `kqueue` takes nothing and returns a new descriptor or -1.
232            let fd = unsafe { libc::kqueue() };
233            if fd < 0 {
234                return Err(io::Error::last_os_error());
235            }
236            // SAFETY: `fd` was just opened by `kqueue` and is owned by nothing else.
237            let queue = unsafe { std::fs::File::from_raw_fd(fd) };
238            // SAFETY: an all-zero `kevent` is valid; the fields that matter are set below.
239            let mut change: libc::kevent = unsafe { std::mem::zeroed() };
240            change.ident = usize::try_from(parent).unwrap_or(0);
241            change.filter = libc::EVFILT_PROC;
242            change.flags = libc::EV_ADD | libc::EV_ONESHOT;
243            change.fflags = libc::NOTE_EXIT;
244            // SAFETY: `change` is one valid `kevent` that outlives the call; no events are read.
245            let registered = unsafe {
246                libc::kevent(
247                    queue.as_raw_fd(),
248                    &raw const change,
249                    1,
250                    std::ptr::null_mut(),
251                    0,
252                    std::ptr::null(),
253                )
254            };
255            if registered < 0 {
256                let error = io::Error::last_os_error();
257                return if error.raw_os_error() == Some(libc::ESRCH) {
258                    Ok(Some(Self(None)))
259                } else {
260                    Err(error)
261                };
262            }
263            // The parent may have exited before registration.
264            // SAFETY: `getppid` cannot fail.
265            if unsafe { libc::getppid() } != parent {
266                return Ok(Some(Self(None)));
267            }
268            Ok(Some(Self(Some(queue))))
269        }
270
271        /// Blocks until the parent exits; `false` if the wait failed.
272        pub(super) fn wait(&self) -> bool {
273            use std::os::fd::AsRawFd;
274            let Some(queue) = &self.0 else {
275                return true;
276            };
277            loop {
278                // SAFETY: an all-zero `kevent` is valid; the call fills it.
279                let mut event: libc::kevent = unsafe { std::mem::zeroed() };
280                // SAFETY: `event` has room for the one event asked for and outlives the call.
281                let ready = unsafe {
282                    libc::kevent(
283                        queue.as_raw_fd(),
284                        std::ptr::null(),
285                        0,
286                        &raw mut event,
287                        1,
288                        std::ptr::null(),
289                    )
290                };
291                if ready > 0 {
292                    return true;
293                }
294                if ready < 0 && io::Error::last_os_error().kind() != io::ErrorKind::Interrupted {
295                    return false;
296                }
297            }
298        }
299    }
300}
301
302#[cfg(windows)]
303mod platform {
304    use std::io;
305    use std::os::windows::io::AsRawHandle;
306
307    use windows_sys::Win32::Foundation::{
308        CloseHandle, ERROR_INVALID_PARAMETER, FILETIME, HANDLE, INVALID_HANDLE_VALUE, WAIT_OBJECT_0,
309    };
310    use windows_sys::Win32::System::Diagnostics::ToolHelp::{
311        CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
312        TH32CS_SNAPPROCESS,
313    };
314    use windows_sys::Win32::System::JobObjects::{
315        AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
316        JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
317        SetInformationJobObject,
318    };
319    use windows_sys::Win32::System::Threading::{
320        GetCurrentProcess, GetCurrentProcessId, GetProcessTimes, INFINITE, OpenProcess,
321        PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, WaitForSingleObject,
322    };
323
324    pub(super) struct OwnedHandle(HANDLE);
325
326    // SAFETY: a kernel handle is a process-wide value; any thread may wait on or close it.
327    unsafe impl Send for OwnedHandle {}
328    // SAFETY: the calls made through a shared reference (waits, job assignment) are thread-safe.
329    unsafe impl Sync for OwnedHandle {}
330
331    impl Drop for OwnedHandle {
332        fn drop(&mut self) {
333            // SAFETY: the handle is open and owned by this value alone.
334            unsafe { CloseHandle(self.0) };
335        }
336    }
337
338    /// The parent's handle, or `None` if the parent is gone.
339    pub(super) struct Parent(Option<OwnedHandle>);
340
341    impl Parent {
342        pub(super) fn open() -> io::Result<Option<Self>> {
343            let Some(parent) = parent_id()? else {
344                return Ok(None);
345            };
346            // SAFETY: `OpenProcess` takes plain values and returns a new handle or null.
347            let handle = unsafe {
348                OpenProcess(
349                    PROCESS_SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION,
350                    0,
351                    parent,
352                )
353            };
354            if handle.is_null() {
355                let error = io::Error::last_os_error();
356                return if error.raw_os_error() == i32::try_from(ERROR_INVALID_PARAMETER).ok() {
357                    Ok(Some(Self(None)))
358                } else {
359                    Err(error)
360                };
361            }
362            let handle = OwnedHandle(handle);
363            // SAFETY: the pseudo-handle of the current process needs no closing.
364            let ours = created(unsafe { GetCurrentProcess() })?;
365            // A process created after this one cannot be the parent; the id was reused.
366            if created(handle.0)? > ours {
367                return Ok(Some(Self(None)));
368            }
369            Ok(Some(Self(Some(handle))))
370        }
371
372        /// Blocks until the parent exits; `false` if the wait failed.
373        pub(super) fn wait(&self) -> bool {
374            let Some(handle) = &self.0 else {
375                return true;
376            };
377            // SAFETY: the handle is open, with `SYNCHRONIZE` access, for the call.
378            unsafe { WaitForSingleObject(handle.0, INFINITE) == WAIT_OBJECT_0 }
379        }
380    }
381
382    fn parent_id() -> io::Result<Option<u32>> {
383        // SAFETY: a process snapshot takes no process id.
384        let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
385        if snapshot == INVALID_HANDLE_VALUE {
386            return Err(io::Error::last_os_error());
387        }
388        let snapshot = OwnedHandle(snapshot);
389        // SAFETY: the current process id cannot fail.
390        let ours = unsafe { GetCurrentProcessId() };
391        let mut entry = PROCESSENTRY32W {
392            dwSize: u32::try_from(std::mem::size_of::<PROCESSENTRY32W>()).unwrap_or(0),
393            ..PROCESSENTRY32W::default()
394        };
395        // SAFETY: `entry` is a `PROCESSENTRY32W` with its size set, outliving the call.
396        let mut more = unsafe { Process32FirstW(snapshot.0, &raw mut entry) } != 0;
397        while more {
398            if entry.th32ProcessID == ours {
399                return Ok(Some(entry.th32ParentProcessID).filter(|parent| *parent != 0));
400            }
401            // SAFETY: as for the first entry.
402            more = unsafe { Process32NextW(snapshot.0, &raw mut entry) } != 0;
403        }
404        Ok(None)
405    }
406
407    /// When `process` was created, in 100 ns intervals since 1601.
408    fn created(process: HANDLE) -> io::Result<u64> {
409        let zero = || FILETIME {
410            dwLowDateTime: 0,
411            dwHighDateTime: 0,
412        };
413        let (mut created, mut exited, mut kernel, mut user) = (zero(), zero(), zero(), zero());
414        // SAFETY: `process` has query access; the four structures outlive the call.
415        let read = unsafe {
416            GetProcessTimes(
417                process,
418                &raw mut created,
419                &raw mut exited,
420                &raw mut kernel,
421                &raw mut user,
422            )
423        };
424        if read == 0 {
425            return Err(io::Error::last_os_error());
426        }
427        Ok(u64::from(created.dwHighDateTime) << 32 | u64::from(created.dwLowDateTime))
428    }
429
430    pub(super) fn kill_on_close_job() -> io::Result<OwnedHandle> {
431        // SAFETY: an unnamed job with default security; the call returns a new handle or null.
432        let job = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) };
433        if job.is_null() {
434            return Err(io::Error::last_os_error());
435        }
436        let job = OwnedHandle(job);
437        let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
438        limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
439        // SAFETY: `limits` matches this information class and size, and outlives the call.
440        let set = unsafe {
441            SetInformationJobObject(
442                job.0,
443                JobObjectExtendedLimitInformation,
444                (&raw const limits).cast(),
445                u32::try_from(std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>())
446                    .unwrap_or(0),
447            )
448        };
449        if set == 0 {
450            return Err(io::Error::last_os_error());
451        }
452        Ok(job)
453    }
454
455    pub(super) fn assign(job: &OwnedHandle, child: &std::process::Child) -> io::Result<()> {
456        // SAFETY: both handles are open for the call; `child` keeps its process handle open.
457        if unsafe { AssignProcessToJobObject(job.0, child.as_raw_handle()) } == 0 {
458            return Err(io::Error::last_os_error());
459        }
460        Ok(())
461    }
462}
463
464#[cfg(not(any(
465    target_os = "linux",
466    target_os = "macos",
467    target_os = "ios",
468    target_os = "freebsd",
469    windows
470)))]
471mod platform {
472    use std::io;
473
474    /// No parent watch on this platform.
475    pub(super) struct Parent;
476
477    impl Parent {
478        pub(super) fn open() -> io::Result<Option<Self>> {
479            Ok(None)
480        }
481
482        pub(super) fn wait(&self) -> bool {
483            false
484        }
485    }
486}
487
488#[cfg(test)]
489#[path = "lifetime_tests.rs"]
490mod tests;