Skip to main content

proot/
access.rs

1//! How the supervisor gets the right to reach its tracees.
2//!
3//! Both modes need the kernel's ptrace access check to pass towards every
4//! tracee: the tracer for `PTRACE_SEIZE`, the no-ptrace mode for
5//! `process_vm_readv` and `/proc/<pid>/mem`.  Yama's `ptrace_scope` of 2
6//! grants that only to a holder of `CAP_SYS_PTRACE` in the tracee's user
7//! namespace, so where the plain check is refused the first tracee is moved
8//! into a user namespace of its own, which the supervisor owns from outside
9//! and therefore holds every capability in.  The namespace maps the caller's
10//! own uid and gid onto themselves and nothing else.
11//!
12//! Where neither ptrace nor a user namespace works (the common Kubernetes
13//! case), the guest-stub restricted mode (see `guest_stub`) needs no access
14//! to a guest's memory at all: a stub inside each guest process moves the
15//! memory a served syscall uses into a mailbox both sides map.  It is the
16//! fallback on x86_64 and aarch64, ruled out by `PROOT_GUEST_STUB=0`.
17//!
18//! The choice is made empirically: a child is forked and the supervisor
19//! tries the accesses it will need on it, first as it is, then inside a user
20//! namespace.  A host where neither works (Yama 3, an LSM policy, a profile
21//! that also filters `unshare`) falls back to the guest stub, or where that
22//! is unavailable or ruled out gets an error instead of a guest that hangs.
23
24use crate::sys::{errno, strerror};
25use std::ffi::CString;
26
27/// What the probe settled on.
28#[derive(Clone, Debug)]
29pub struct Access {
30    /// Whether the trace tier runs on ptrace.
31    pub ptrace: bool,
32    /// The user namespace the first tracee enters, if one is needed.
33    pub userns: Option<UserNs>,
34    /// Whether the guest-stub restricted mode is used: the supervisor does
35    /// not reach the guests' memory, a stub inside each moves it to a mailbox.
36    pub trap_mode: bool,
37}
38
39/// Everything the first tracee needs to enter its user namespace, prepared
40/// before `fork` so the child does not allocate.
41#[derive(Clone, Debug)]
42pub struct UserNs {
43    /// The line written to `/proc/self/uid_map`.
44    uid_map: CString,
45    /// The line written to `/proc/self/gid_map`.
46    gid_map: CString,
47}
48
49/// A value the probe reads back from the child's copy of the supervisor's
50/// memory, proving the read reached it.
51static MARKER: u64 = 0x7072_6f6f_745f_6e78;
52
53impl UserNs {
54    /// Prepares an identity mapping of the caller's uid and gid.
55    ///
56    /// # Returns
57    /// The prepared mapping.
58    pub fn identity() -> UserNs {
59        let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
60        UserNs {
61            uid_map: CString::new(format!("{uid} {uid} 1")).unwrap(),
62            gid_map: CString::new(format!("{gid} {gid} 1")).unwrap(),
63        }
64    }
65
66    /// Moves the calling process into a new user namespace with the prepared
67    /// mapping.  Allocation free, for use between `fork` and `execve`.
68    ///
69    /// # Returns
70    /// `Ok` once the namespace is entered and mapped.
71    ///
72    /// # Errors
73    /// The errno of the step that failed.
74    pub fn enter(&self) -> Result<(), i32> {
75        if unsafe { libc::unshare(libc::CLONE_NEWUSER) } < 0 {
76            return Err(errno());
77        }
78        write_file(c"/proc/self/setgroups", c"deny")?;
79        write_file(c"/proc/self/uid_map", &self.uid_map)?;
80        write_file(c"/proc/self/gid_map", &self.gid_map)
81    }
82}
83
84/// Writes `text` to the file at `path`.  Allocation free.
85///
86/// # Arguments
87/// * `path` - File to open for writing.
88/// * `text` - Bytes to write in one call.
89///
90/// # Errors
91/// The errno of `open` or `write`.
92fn write_file(path: &std::ffi::CStr, text: &std::ffi::CStr) -> Result<(), i32> {
93    let fd = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC) };
94    if fd < 0 {
95        return Err(errno());
96    }
97    let bytes = text.to_bytes();
98    let n = unsafe { libc::write(fd, bytes.as_ptr() as *const _, bytes.len()) };
99    let e = errno();
100    unsafe { libc::close(fd) };
101    if n == bytes.len() as isize { Ok(()) } else { Err(e) }
102}
103
104/// Outcome of one probe child.
105struct Trial {
106    /// Whether the supervisor could read the child's memory, or the errno.
107    memory: Result<(), i32>,
108    /// Whether the child could be ptraced, or its errno.
109    ptrace: Result<(), i32>,
110}
111
112/// Decides how the supervisor reaches its tracees.
113///
114/// `PROOT_USERNS` forces the user namespace (`1`) or rules it out (`0`).
115///
116/// # Arguments
117/// * `ptrace_wanted` - Whether the ptrace tier is available as far as the
118///   syscall filter is concerned; a host that refuses it only through the
119///   access check falls back to the no-ptrace mode when a user namespace
120///   does not help.
121/// * `stub` - Whether the guest-stub restricted mode may be used: `Some(false)`
122///   as the fallback when no combination reaches a child, `Some(true)` in
123///   any case, `None` never.  Ignored where the stub does not exist
124///   (neither x86_64 nor aarch64).
125///
126/// # Returns
127/// The mode and, when needed, the user namespace to enter.
128///
129/// # Errors
130/// A message naming what was refused, when no combination reaches a child
131/// and the guest-stub restricted mode is unavailable or ruled out.
132pub fn probe(ptrace_wanted: bool, stub: Option<bool>) -> Result<Access, String> {
133    let stub = stub.filter(|_| cfg!(any(target_arch = "x86_64", target_arch = "aarch64")));
134    let trapped = Access {
135        ptrace: false,
136        userns: None,
137        trap_mode: true,
138    };
139    if stub == Some(true) {
140        return Ok(trapped);
141    }
142    let forced = std::env::var_os("PROOT_USERNS").map(|v| v != "0");
143    let userns = UserNs::identity();
144    let mut plans: Vec<(bool, bool)> = Vec::new();
145    for ptrace in [ptrace_wanted, false] {
146        for ns in [false, true] {
147            if forced.is_some_and(|f| f != ns) || plans.contains(&(ptrace, ns)) {
148                continue;
149            }
150            plans.push((ptrace, ns));
151        }
152    }
153    let mut plain: Option<Trial> = None;
154    let mut namespaced: Option<Trial> = None;
155    for (ptrace, ns) in plans {
156        let slot = if ns { &mut namespaced } else { &mut plain };
157        if slot.is_none() {
158            *slot = Some(trial(ns.then_some(&userns), ptrace_wanted));
159        }
160        let t = slot.as_ref().unwrap();
161        if t.memory.is_ok() && (!ptrace || t.ptrace.is_ok()) {
162            return Ok(Access {
163                ptrace,
164                userns: ns.then_some(userns),
165                trap_mode: false,
166            });
167        }
168    }
169    if stub.is_some() {
170        return Ok(trapped);
171    }
172    Err(refusal(plain.as_ref(), namespaced.as_ref()))
173}
174
175/// Forks a child that waits, tries the supervisor's accesses on it, and
176/// kills it.
177///
178/// # Arguments
179/// * `userns` - User namespace the child enters first, if any.
180/// * `try_ptrace` - Whether to try `PTRACE_SEIZE` too.
181///
182/// # Returns
183/// The result of each access; a child that could not enter its namespace
184/// reports that errno for both.
185fn trial(userns: Option<&UserNs>, try_ptrace: bool) -> Trial {
186    let mut ready = [0i32; 2];
187    let mut go = [0i32; 2];
188    if unsafe { libc::pipe2(ready.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
189        let e = errno();
190        return Trial {
191            memory: Err(e),
192            ptrace: Err(e),
193        };
194    }
195    if unsafe { libc::pipe2(go.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
196        let e = errno();
197        unsafe {
198            libc::close(ready[0]);
199            libc::close(ready[1]);
200        }
201        return Trial {
202            memory: Err(e),
203            ptrace: Err(e),
204        };
205    }
206    let pid = unsafe { libc::fork() };
207    if pid == 0 {
208        unsafe {
209            libc::close(ready[0]);
210            libc::close(go[1]);
211            let status: i32 = match userns.map(UserNs::enter) {
212                Some(Err(e)) => e,
213                _ => 0,
214            };
215            libc::write(ready[1], &status as *const i32 as *const _, 4);
216            let mut byte = 0u8;
217            libc::read(go[0], &mut byte as *mut u8 as *mut _, 1);
218            libc::_exit(0);
219        }
220    }
221    unsafe {
222        libc::close(ready[1]);
223        libc::close(go[0]);
224    }
225    let result = if pid < 0 {
226        let e = errno();
227        Trial {
228            memory: Err(e),
229            ptrace: Err(e),
230        }
231    } else {
232        let mut status = 0i32;
233        let n = unsafe { libc::read(ready[0], &mut status as *mut i32 as *mut _, 4) };
234        if n != 4 || status != 0 {
235            let e = if n == 4 { status } else { libc::ECHILD };
236            Trial {
237                memory: Err(e),
238                ptrace: Err(e),
239            }
240        } else {
241            Trial {
242                memory: read_marker(pid),
243                ptrace: if try_ptrace { seize(pid) } else { Err(libc::ENOSYS) },
244            }
245        }
246    };
247    unsafe {
248        libc::close(ready[0]);
249        libc::close(go[1]);
250        if pid > 0 {
251            libc::kill(pid, libc::SIGKILL);
252            let mut st = 0;
253            while libc::waitpid(pid, &mut st, libc::__WALL) < 0 && errno() == libc::EINTR {}
254        }
255    }
256    result
257}
258
259/// Reads [`MARKER`] from the child the way the notification handlers will:
260/// `process_vm_readv`, or `/proc/<pid>/mem` where that is filtered.
261///
262/// # Arguments
263/// * `pid` - The probe child, a copy of this address space.
264///
265/// # Errors
266/// The errno of `/proc/<pid>/mem` when both refuse.
267fn read_marker(pid: i32) -> Result<(), i32> {
268    let mut value = 0u64;
269    let addr = &MARKER as *const u64 as u64;
270    let local = libc::iovec {
271        iov_base: &mut value as *mut u64 as *mut _,
272        iov_len: 8,
273    };
274    let remote = libc::iovec {
275        iov_base: addr as *mut _,
276        iov_len: 8,
277    };
278    if unsafe { libc::process_vm_readv(pid, &local, 1, &remote, 1, 0) } == 8 && value == MARKER {
279        return Ok(());
280    }
281    let path = CString::new(format!("/proc/{pid}/mem")).unwrap();
282    let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
283    if fd < 0 {
284        return Err(errno());
285    }
286    value = 0;
287    let n = unsafe { libc::pread64(fd, &mut value as *mut u64 as *mut _, 8, addr as i64) };
288    let e = errno();
289    unsafe { libc::close(fd) };
290    if n == 8 && value == MARKER { Ok(()) } else { Err(e) }
291}
292
293/// Seizes the probe child; it is killed right after, which ends the trace.
294///
295/// # Arguments
296/// * `pid` - The probe child.
297///
298/// # Errors
299/// The errno of `PTRACE_SEIZE`.
300fn seize(pid: i32) -> Result<(), i32> {
301    if unsafe { libc::ptrace(libc::PTRACE_SEIZE, pid, 0usize, 0usize) } < 0 {
302        Err(errno())
303    } else {
304        Ok(())
305    }
306}
307
308/// Builds the message for a host where no plan reaches a child.
309///
310/// # Arguments
311/// * `plain` - The trial without a user namespace, if it ran.
312/// * `namespaced` - The trial inside a user namespace, if it ran.
313///
314/// # Returns
315/// A message naming the refusals and the Yama setting.
316fn refusal(plain: Option<&Trial>, namespaced: Option<&Trial>) -> String {
317    let scope = std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope")
318        .map(|s| format!("kernel.yama.ptrace_scope is {}", s.trim()))
319        .unwrap_or_else(|_| "Yama is not present".into());
320    let mut msg = String::from("cannot reach the guest's memory: ");
321    if let Some(t) = plain {
322        let e = t.memory.err().unwrap_or(libc::EPERM);
323        msg.push_str(&format!(
324            "process_vm_readv and /proc/<pid>/mem are refused for a child ({})",
325            strerror(e)
326        ));
327    }
328    match namespaced {
329        Some(t) => {
330            let e = t.memory.err().unwrap_or(libc::EPERM);
331            msg.push_str(&format!(
332                "{}a user namespace did not help ({})",
333                if plain.is_some() { ", and " } else { "" },
334                strerror(e)
335            ));
336        }
337        None if std::env::var_os("PROOT_USERNS").is_some_and(|v| v == "0") => {
338            msg.push_str(", and PROOT_USERNS=0 rules out a user namespace")
339        }
340        None => {}
341    }
342    msg.push_str(&format!(
343        "; {scope}, and proot-next needs either CAP_SYS_PTRACE, a ptrace_scope of 0 or 1, or unprivileged user namespaces"
344    ));
345    if cfg!(any(target_arch = "x86_64", target_arch = "aarch64")) {
346        msg.push_str(" (PROOT_GUEST_STUB=0 rules out the guest-stub mode, which needs none of them)");
347    }
348    msg
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn probe_reaches_a_child_or_explains_why_not() {
357        match probe(true, None) {
358            Ok(a) => {
359                assert!(!a.trap_mode);
360                if a.userns.is_none() {
361                    assert!(read_marker(std::process::id() as i32).is_ok());
362                }
363            }
364            Err(msg) => assert!(msg.contains("ptrace_scope") || msg.contains("Yama")),
365        }
366    }
367
368    /// The guest stub is taken where nothing else reaches a child, anywhere
369    /// when forced, and never when ruled out.
370    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
371    #[test]
372    fn the_guest_stub_is_the_last_resort_when_asked_for() {
373        let a = probe(true, Some(false)).expect("the guest stub needs no access");
374        assert_eq!(a.trap_mode, probe(true, None).is_err());
375        assert!(probe(true, Some(true)).unwrap().trap_mode);
376    }
377
378    #[test]
379    fn identity_mapping_names_the_callers_ids() {
380        let ns = UserNs::identity();
381        let uid = unsafe { libc::getuid() };
382        assert_eq!(ns.uid_map.to_bytes(), format!("{uid} {uid} 1").as_bytes());
383    }
384}