Skip to main content

orbit_core/
process.rs

1//! Process ownership for handles that must survive being copied by fork.
2//!
3//! Acquire ownership during boot/open. A child opens fresh handles. An
4//! inherited owner never becomes current again, even after the child opens
5//! its own handles. On Unix systems without a kernel-cleared mapping this
6//! relies on the libc fork path invoking pthread_atfork handlers.
7
8use std::io;
9use std::ptr;
10use std::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
11
12static CURRENT: AtomicPtr<AtomicU32> = AtomicPtr::new(ptr::null_mut());
13
14/// A process identity captured when a resource is opened.
15///
16/// The marker is process-private and lives until exit. Checking ownership
17/// performs one atomic load and does not call into the kernel.
18#[derive(Clone, Copy)]
19pub struct ProcessOwner {
20    marker: &'static AtomicU32,
21    pid: u32,
22}
23
24#[cfg(all(test, target_os = "linux", target_arch = "x86_64"))]
25#[test]
26fn kernel_marker_invalidates_on_raw_fork_and_stays_invalid_after_rejoin() {
27    let parent = ProcessOwner::current().unwrap();
28    assert_eq!(parent.pid(), std::process::id());
29    // SAFETY: the child only accesses private atomics and makes raw OS calls.
30    let child = unsafe { libc::syscall(libc::SYS_fork) } as libc::pid_t;
31    assert!(child >= 0, "fork failed");
32    if child == 0 {
33        let invalid = !parent.is_current();
34        let joined = ProcessOwner::current().is_ok_and(|owner| {
35            owner.is_current() && owner.pid() == std::process::id()
36        });
37        // SAFETY: exit without using inherited runtime state.
38        unsafe {
39            libc::_exit(i32::from(!(invalid && joined && !parent.is_current())))
40        };
41    }
42    let mut status = 0;
43    // SAFETY: child is the PID returned by fork and status is writable.
44    assert_eq!(unsafe { libc::waitpid(child, &mut status, 0) }, child);
45    assert!(libc::WIFEXITED(status));
46    assert_eq!(libc::WEXITSTATUS(status), 0);
47    assert!(parent.is_current());
48}
49
50impl ProcessOwner {
51    pub fn current() -> io::Result<Self> {
52        loop {
53            let previous = CURRENT.load(Ordering::Acquire);
54            // SAFETY: published mappings stay alive, including in a child.
55            if let Some(marker) = unsafe { previous.as_ref() } {
56                let pid = marker.load(Ordering::Relaxed);
57                if pid != 0 {
58                    return Ok(Self { marker, pid });
59                }
60            }
61
62            let candidate = platform::Marker::new()?;
63            let pid = std::process::id();
64            candidate.initialize(pid);
65            if CURRENT
66                .compare_exchange(
67                    previous,
68                    candidate.as_ptr(),
69                    Ordering::AcqRel,
70                    Ordering::Acquire,
71                )
72                .is_ok()
73            {
74                return Ok(Self { marker: candidate.publish(), pid });
75            }
76        }
77    }
78
79    /// The PID captured at boot/open, including on an inherited handle.
80    pub fn pid(self) -> u32 {
81        self.pid
82    }
83
84    /// Whether this resource belongs to the process using it.
85    #[inline]
86    pub fn is_current(self) -> bool {
87        self.marker.load(Ordering::Relaxed) != 0
88    }
89}
90
91#[cfg(any(target_os = "linux", target_os = "freebsd"))]
92mod platform {
93    use std::io;
94    use std::mem::size_of;
95    use std::ptr::{self, NonNull};
96    use std::sync::atomic::AtomicU32;
97
98    pub(super) struct Marker {
99        ptr: NonNull<AtomicU32>,
100        len: usize,
101    }
102
103    impl Marker {
104        pub(super) fn new() -> io::Result<Self> {
105            // SAFETY: sysconf takes only the constant selector.
106            let len = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
107            if len < size_of::<AtomicU32>() as libc::c_long {
108                return Err(io::Error::other(
109                    "invalid process marker page size",
110                ));
111            }
112            let len = len as usize;
113            // SAFETY: this requests a new private anonymous page.
114            let page = unsafe {
115                libc::mmap(
116                    ptr::null_mut(),
117                    len,
118                    libc::PROT_READ | libc::PROT_WRITE,
119                    libc::MAP_PRIVATE | libc::MAP_ANON,
120                    -1,
121                    0,
122                )
123            };
124            if page == libc::MAP_FAILED {
125                return Err(io::Error::last_os_error());
126            }
127            let Some(ptr) = NonNull::new(page.cast()) else {
128                // SAFETY: mmap returned an otherwise valid mapping at zero.
129                unsafe { libc::munmap(page, len) };
130                return Err(io::Error::other("null process marker mapping"));
131            };
132            let marker = Self { ptr, len };
133            // SAFETY: the range is the private mapping allocated above.
134            #[cfg(target_os = "linux")]
135            let status =
136                unsafe { libc::madvise(page, len, libc::MADV_WIPEONFORK) };
137            #[cfg(target_os = "freebsd")]
138            let status =
139                unsafe { libc::minherit(page, len, libc::INHERIT_ZERO) };
140            if status != 0 {
141                return Err(io::Error::last_os_error());
142            }
143            Ok(marker)
144        }
145
146        pub(super) fn initialize(
147            &self,
148            pid: u32,
149        ) {
150            // SAFETY: this mapping is aligned and has not been published yet.
151            unsafe { self.ptr.as_ptr().write(AtomicU32::new(pid)) };
152        }
153
154        pub(super) fn as_ptr(&self) -> *mut AtomicU32 {
155            self.ptr.as_ptr()
156        }
157
158        pub(super) fn publish(self) -> &'static AtomicU32 {
159            let ptr = self.ptr;
160            // Keep old markers mapped in a child so inherited owners remain
161            // invalid even after CURRENT points to the child's new marker.
162            std::mem::forget(self);
163            // SAFETY: the initialized mapping now lives until process exit.
164            unsafe { ptr.as_ref() }
165        }
166    }
167
168    impl Drop for Marker {
169        fn drop(&mut self) {
170            // SAFETY: only unpublished candidates are unmapped here.
171            unsafe { libc::munmap(self.ptr.as_ptr().cast(), self.len) };
172        }
173    }
174}
175
176#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
177mod platform {
178    use std::io;
179    use std::sync::atomic::{AtomicU32, Ordering};
180
181    #[cfg(unix)]
182    use super::CURRENT;
183    #[cfg(unix)]
184    use std::sync::atomic::AtomicBool;
185
186    #[cfg(unix)]
187    static HOOK_READY: AtomicBool = AtomicBool::new(false);
188
189    #[cfg(unix)]
190    extern "C" fn after_fork() {
191        // SAFETY: a published marker always lives until process exit.
192        if let Some(marker) =
193            unsafe { CURRENT.load(Ordering::Acquire).as_ref() }
194        {
195            marker.store(0, Ordering::Relaxed);
196        }
197    }
198
199    pub(super) struct Marker(Box<AtomicU32>);
200
201    impl Marker {
202        pub(super) fn new() -> io::Result<Self> {
203            #[cfg(unix)]
204            if !HOOK_READY.load(Ordering::Acquire) {
205                // SAFETY: the child handler accesses only lock-free atomics.
206                // Concurrent startup can register the same idempotent handler.
207                let status = unsafe {
208                    libc::pthread_atfork(None, None, Some(after_fork))
209                };
210                if status != 0 {
211                    return Err(io::Error::from_raw_os_error(status));
212                }
213                HOOK_READY.store(true, Ordering::Release);
214            }
215            Ok(Self(Box::new(AtomicU32::new(0))))
216        }
217
218        pub(super) fn initialize(
219            &self,
220            pid: u32,
221        ) {
222            self.0.store(pid, Ordering::Relaxed);
223        }
224
225        pub(super) fn as_ptr(&self) -> *mut AtomicU32 {
226            ptr::from_ref(&*self.0).cast_mut()
227        }
228
229        pub(super) fn publish(self) -> &'static AtomicU32 {
230            Box::leak(self.0)
231        }
232    }
233
234    use std::ptr;
235}