Skip to main content

capsule_runtime/
security.rs

1//! Process-level hardening against the `/proc/<pid>/environ` side channel.
2//!
3//! `build_shell_env` (see `shell.rs`) sanitizes the environment array handed to each
4//! spawned subprocess, but that only covers the array `Command` passes at `execve`
5//! time. It does nothing about a bash child reading its own parent's raw kernel-recorded
6//! environment via `/proc/$PPID/environ` — a channel entirely outside `Command`'s
7//! control. Marking the runtime process non-dumpable closes that channel: the kernel's
8//! `ptrace_may_access` check (which gates `/proc/pid/environ` reads) fails for a
9//! non-dumpable target even when the reader shares the same UID.
10
11/// Marks the current process non-dumpable so no same-UID process — including its own
12/// shell descendants — can read this process's `/proc/<pid>/environ`.
13///
14/// Must be called as the first statement of `main()` in every binary that links this
15/// crate and can reach `execute_shell`, before any argument parsing or subprocess-capable
16/// code runs. A future binary that links `capsule-runtime` and reaches `execute_shell`
17/// inherits the same obligation.
18///
19/// No-op on non-Linux targets: `/proc/<pid>/environ` and `prctl(2)` don't exist there,
20/// so there is no side channel to close.
21#[cfg(target_os = "linux")]
22#[allow(unsafe_code)]
23pub fn harden_process_dumpable() -> Result<(), String> {
24    // SAFETY: PR_SET_DUMPABLE takes a single int argument and has no pointer/lifetime
25    // requirements; the extra 0 args are ignored by prctl's variadic C signature.
26    let result = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };
27    if result == 0 {
28        Ok(())
29    } else {
30        Err(format!(
31            "prctl(PR_SET_DUMPABLE, 0) failed: {}",
32            std::io::Error::last_os_error()
33        ))
34    }
35}
36
37#[cfg(not(target_os = "linux"))]
38pub fn harden_process_dumpable() -> Result<(), String> {
39    Ok(())
40}
41
42#[cfg(all(test, target_os = "linux"))]
43mod tests {
44    use super::*;
45
46    #[test]
47    #[allow(unsafe_code)]
48    fn harden_process_dumpable_clears_dumpable_flag() {
49        harden_process_dumpable().expect("prctl(PR_SET_DUMPABLE) should succeed");
50
51        // SAFETY: PR_GET_DUMPABLE takes no further arguments; reading back our own
52        // process's flag is side-effect-free.
53        let flag = unsafe { libc::prctl(libc::PR_GET_DUMPABLE, 0, 0, 0, 0) };
54        assert_eq!(flag, 0, "process should report non-dumpable after hardening");
55    }
56}
57
58#[cfg(all(test, not(target_os = "linux")))]
59mod non_linux_tests {
60    use super::*;
61
62    #[test]
63    fn harden_process_dumpable_is_noop_ok() {
64        assert!(harden_process_dumpable().is_ok());
65    }
66}