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}