Skip to main content

coreshift_core/
process.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//! Low-level process management primitives.
6//!
7//! Safe wrappers around `fork`, `setsid`, `setpgid`, `dup2`, `prctl`,
8//! and fd-range close — building blocks for double-fork supervisor patterns.
9
10use crate::CoreError;
11use crate::error::syscall_ret;
12
13/// Result of a [`fork`] call.
14pub enum ForkResult {
15    /// Returned in the parent with the child's PID.
16    Parent(i32),
17    /// Returned in the child (PID = 0).
18    Child,
19}
20
21/// Fork the current process.
22///
23/// # Safety
24/// After `fork`, only async-signal-safe operations are safe in the child
25/// before `exec`. Rust's allocator is not async-signal-safe; use this only
26/// in the narrow pattern of fork → exec or fork → immediate `_exit`.
27pub unsafe fn fork() -> Result<ForkResult, CoreError> {
28    let pid = unsafe { libc::fork() };
29    if pid < 0 {
30        return Err(CoreError::sys(
31            std::io::Error::last_os_error().raw_os_error().unwrap_or(-1),
32            "fork",
33        ));
34    }
35    if pid == 0 {
36        Ok(ForkResult::Child)
37    } else {
38        Ok(ForkResult::Parent(pid))
39    }
40}
41
42/// Create a new session and set the calling process as leader.
43pub fn setsid() -> Result<(), CoreError> {
44    let ret = unsafe { libc::setsid() };
45    if ret < 0 {
46        return Err(CoreError::sys(
47            std::io::Error::last_os_error().raw_os_error().unwrap_or(-1),
48            "setsid",
49        ));
50    }
51    Ok(())
52}
53
54/// Set the process group ID of `pid` to `pgid` (use 0 for self).
55pub fn setpgid(pid: i32, pgid: i32) -> Result<(), CoreError> {
56    syscall_ret(unsafe { libc::setpgid(pid, pgid) }, "setpgid")
57}
58
59/// Redirect stdin, stdout, and stderr to `/dev/null`.
60///
61/// # Safety
62/// Uses `dup2` on file descriptors 0/1/2.
63pub unsafe fn redirect_stdio_to_devnull() -> Result<(), CoreError> {
64    let fd = unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_RDWR) };
65    if fd < 0 {
66        return Err(CoreError::sys(
67            std::io::Error::last_os_error().raw_os_error().unwrap_or(-1),
68            "open:/dev/null",
69        ));
70    }
71    unsafe {
72        libc::dup2(fd, 0);
73        libc::dup2(fd, 1);
74        libc::dup2(fd, 2);
75        if fd > 2 {
76            libc::close(fd);
77        }
78    }
79    Ok(())
80}
81
82/// Set the signal sent to this process when its **parent thread** dies
83/// (`PR_SET_PDEATHSIG`).
84///
85/// Precision: the signal is delivered when the parent *thread that created
86/// this task* exits (`forget_original_parent` runs on every thread's
87/// `do_exit`), not when the parent process dies — in a thread-pool caller a
88/// worker-thread exit kills its children while the process lives. The signal
89/// is retained across exec except under `bprm->secureexec`. For spawn
90/// wiring (which is opt-in and leader-only) see
91/// [`SpawnOptionsBuilder::pdeath_signal`](crate::spawn::SpawnOptionsBuilder::pdeath_signal).
92pub fn set_pdeathsig(sig: i32) -> Result<(), CoreError> {
93    syscall_ret(
94        unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, sig as libc::c_ulong, 0, 0, 0) },
95        "prctl:PR_SET_PDEATHSIG",
96    )
97}
98
99/// Set the process's `PR_SET_DUMPABLE` flag.
100///
101/// `dumpable = false` is daemon self-hardening (A17-02): the process cannot
102/// produce core dumps and its `/proc/self` memory is not readable by children
103/// via `process_vm_readv`/`pidfd_getfd`. Used together with
104/// [`set_ptracer`] to make a privileged daemon resistant to child tracing.
105/// The flag is inherited by `fork` children, which is fine for the exec jobs
106/// (they drop privileges under `setresuid` anyway).
107///
108/// ### Errors
109/// - `EINVAL`: unsupported `PR_SET_DUMPABLE` value (only 0/1 are valid).
110pub fn set_dumpable(dumpable: bool) -> Result<(), CoreError> {
111    syscall_ret(
112        unsafe { libc::prctl(libc::PR_SET_DUMPABLE, dumpable as libc::c_ulong, 0, 0, 0) },
113        "prctl:PR_SET_DUMPABLE",
114    )
115}
116
117/// Restrict which processes may ptrace this one (`PR_SET_PTRACER`).
118///
119/// `pid = 0` denies all tracing — the strongest setting and the one the
120/// daemon uses (A17-02). This is an additional per-process restriction that
121/// works even where YAMA is not compiled (`CONFIG_SECURITY_YAMA` unset): a
122/// child cannot `ptrace`/`pidfd_getfd`/`process_vm_readv` the daemon.
123///
124/// **Platform caveat (verified on the audit target, an Android 5.10 kernel):
125/// `PR_SET_PTRACER` is implemented under `CONFIG_CHECKPOINT_RESTORE`, which
126/// Android kernels build without — the prctl then returns `EINVAL` for every
127/// argument.** Callers must treat `EINVAL` as "unsupported, proceed with
128/// `PR_SET_DUMPABLE=0` + self-seccomp as the actual protection" and only fail
129/// hard on unexpected errors. See the daemon's hub hook for the tolerant
130/// wrapper.
131///
132/// ### Errors
133/// - `EINVAL`: unsupported on this kernel (no `CONFIG_CHECKPOINT_RESTORE`).
134pub fn set_ptracer(pid: i32) -> Result<(), CoreError> {
135    syscall_ret(
136        unsafe { libc::prctl(libc::PR_SET_PTRACER, pid as libc::c_ulong, 0, 0, 0) },
137        "prctl:PR_SET_PTRACER",
138    )
139}
140
141/// Set this thread's name (`PR_SET_NAME`, the `comm` field visible in
142/// `/proc/<pid>/task/<tid>/comm`, capped at 15 bytes + NUL).
143///
144/// Threads created by `std::thread` inherit the process comm unless named, so a
145/// daemon that spawns several worker threads ends up with indistinguishable
146/// names. Naming each thread makes `/proc`/`ps`/strace output actionable
147/// (e.g. `coreshift_fg` vs `coreshift_fps`).
148///
149/// ### Errors
150/// - `EINVAL`/`EFAULT`: unsupported name (truncated at 15 bytes, never a
151///   failure — this is best-effort diagnostics).
152pub fn set_thread_name(name: &str) -> Result<(), CoreError> {
153    let mut buf = [0u8; 16];
154    let bytes = name.as_bytes();
155    let n = bytes.len().min(15);
156    buf[..n].copy_from_slice(&bytes[..n]);
157    syscall_ret(
158        unsafe { libc::prctl(libc::PR_SET_NAME, buf.as_ptr() as libc::c_ulong, 0, 0, 0) },
159        "prctl:PR_SET_NAME",
160    )
161}
162
163/// Duplicate `src_fd` onto `dst_fd` and close `src_fd`.
164///
165/// Equivalent to `dup2(src_fd, dst_fd); close(src_fd)`.
166///
167/// # Safety
168/// Manipulates raw file descriptors.
169pub unsafe fn redirect_fd_to(src_fd: i32, dst_fd: i32) {
170    unsafe {
171        libc::dup2(src_fd, dst_fd);
172        // CORE-M8: when src == dst, `dup2` is a no-op and `close(src_fd)`
173        // would close the very fd meant to be kept.
174        if src_fd != dst_fd {
175            libc::close(src_fd);
176        }
177    }
178}
179
180pub fn getuid() -> u32 {
181    unsafe { libc::getuid() }
182}
183pub fn getgid() -> u32 {
184    unsafe { libc::getgid() }
185}
186
187/// Drop process privileges to the given UID (`setresuid`).
188///
189/// Sets real, effective, and saved UID to `uid`.
190pub fn setuid(uid: u32) -> Result<(), CoreError> {
191    syscall_ret(unsafe { libc::setresuid(uid, uid, uid) }, "setresuid")
192}
193
194/// Drop process privileges to the given GID (`setresgid`).
195///
196/// Sets real, effective, and saved GID to `gid`.
197pub fn setgid(gid: u32) -> Result<(), CoreError> {
198    syscall_ret(unsafe { libc::setresgid(gid, gid, gid) }, "setresgid")
199}
200
201/// Close all file descriptors >= `start`.
202///
203/// Enumerates `/proc/self/fd` to avoid EBADF on sparse fd tables.
204/// Falls back to a blind 3..1024 scan if `/proc/self/fd` is unreadable.
205pub fn close_fds_from(start: i32) {
206    // Open the dir handle ourselves so we own its fd: `std::fs::read_dir` hides
207    // it, and its dirfd is listed in `/proc/self/fd`, so a naive snapshot that
208    // includes it would close it mid-iteration and again on drop
209    // (double-close → in a multithreaded daemon the fd can be reused and an
210    // unrelated socket/log fd gets closed, finding 12).
211    let dir = unsafe { libc::opendir(c"/proc/self/fd".as_ptr()) };
212    if dir.is_null() {
213        for fd in start..1024 {
214            unsafe { libc::close(fd) };
215        }
216        return;
217    }
218    let dir_fd = unsafe { libc::dirfd(dir) };
219    let mut fds = Vec::new();
220    loop {
221        // readdir is not thread-safe against a concurrent close of the fd it is
222        // reading, but this runs in a forked single-threaded child; the fd
223        // snapshot is taken before any close happens below.
224        let ent = unsafe { libc::readdir(dir) };
225        if ent.is_null() {
226            break;
227        }
228        let name = unsafe { (*ent).d_name.as_ptr() };
229        let name = unsafe { std::ffi::CStr::from_ptr(name) };
230        let Ok(name) = name.to_str() else { continue };
231        if let Ok(fd) = name.parse::<i32>()
232            && fd >= start
233            && fd != dir_fd
234        {
235            fds.push(fd);
236        }
237    }
238    // Close the dir handle first so its fd is never in the close set, then
239    // close the snapshot.
240    unsafe { libc::closedir(dir) };
241    for fd in fds {
242        unsafe { libc::close(fd) };
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn set_dumpable_round_trips() {
252        // The flag is process-wide and inherits across fork, so save/restore
253        // around the test to keep the suite's own behaviour stable.
254        let saved = unsafe { libc::prctl(libc::PR_GET_DUMPABLE, 0, 0, 0, 0) };
255        set_dumpable(false).unwrap();
256        assert_eq!(unsafe { libc::prctl(libc::PR_GET_DUMPABLE, 0, 0, 0, 0) }, 0);
257        set_dumpable(true).unwrap();
258        assert_eq!(unsafe { libc::prctl(libc::PR_GET_DUMPABLE, 0, 0, 0, 0) }, 1);
259        if saved >= 0 {
260            unsafe { libc::prctl(libc::PR_SET_DUMPABLE, saved as libc::c_ulong, 0, 0, 0) };
261        }
262    }
263
264    #[test]
265    fn set_ptracer_is_ok_or_unsupported() {
266        // PR_SET_PTRACER(0) denies all tracing. On kernels built without
267        // CONFIG_CHECKPOINT_RESTORE (notably Android, where the audit target
268        // kernel is 5.10) the prctl is unimplemented and fails EINVAL for every
269        // argument. The daemon's contract is therefore: Ok on kernels that
270        // support it, tolerated EINVAL on the rest — never a hard failure for
271        // any other reason.
272        match set_ptracer(0) {
273            Ok(()) => {}
274            Err(e) if e.raw_os_error() == Some(libc::EINVAL) => {}
275            Err(e) => panic!("PR_SET_PTRACER(0) failed unexpectedly: {e}"),
276        }
277        // Restore the default where supported so the test runner itself is not
278        // left locked down (PR_SET_PTRACER_ANY is 0xffff_ffff_ffff_ffff — not
279        // reachable via a signed i32, hence the direct prctl).
280        unsafe {
281            libc::prctl(libc::PR_SET_PTRACER, libc::PR_SET_PTRACER_ANY, 0, 0, 0);
282        };
283    }
284}