Skip to main content

proot/
procs.rs

1//! Per-process bookkeeping shared by every handler: the program a process
2//! runs, the identity it believes it has under `-0` and, without ptrace, the
3//! working directory it believes it is in.
4//!
5//! With ptrace the tracer registers every child at its fork event and
6//! forgets it at exit.  Without ptrace (see `launch`) nothing announces a new
7//! process, so the table is lazy: a process is discovered at its first
8//! notification and inherits the state of its parent, read from `/proc`.  To
9//! make that inheritance exact when the parent changed state between the
10//! fork and the child's first syscall, `clone` is notified too and a snapshot
11//! of the parent is queued for the child; a parent that exits first leaves
12//! its snapshots on an orphan list.  Exits are observed through pidfds by a
13//! reaper thread so the table does not grow without bound.
14//!
15//! Notifications carry thread ids; the thread-to-process map is cached and
16//! re-read from `/proc` once a second, which keeps a reused id from naming
17//! the wrong process for longer than that.
18
19use crate::state::View;
20use crate::sys::{Result, errno};
21use std::collections::{HashMap, VecDeque};
22use std::sync::atomic::{AtomicUsize, Ordering};
23use std::sync::{Arc, Mutex, RwLock};
24use std::time::{Duration, Instant};
25
26/// How long a cached thread-to-process mapping is trusted.
27const TID_TTL: Duration = Duration::from_secs(1);
28/// How long an orphaned fork snapshot stays claimable.
29const ORPHAN_TTL: Duration = Duration::from_secs(10);
30/// Longest parent chain walked when discovering a process.
31const MAX_ANCESTRY: usize = 64;
32
33/// The identity a process believes it has under `-0`.
34///
35/// Starts as root; `setuid` and friends move it with the kernel's rules
36/// (anything goes while the effective id is 0, otherwise only among the
37/// real, effective and saved ids).  Inherited across fork, kept across exec.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
39pub struct Ids {
40    pub ruid: u32,
41    pub euid: u32,
42    pub suid: u32,
43    pub fsuid: u32,
44    pub rgid: u32,
45    pub egid: u32,
46    pub sgid: u32,
47    pub fsgid: u32,
48}
49
50/// What the supervisor remembers about one tracee process.
51#[derive(Clone, Debug, Default)]
52pub struct TraceeInfo {
53    /// Virtual identity under `-0`; `None` until the process changes it.
54    pub ids: Option<Ids>,
55    /// Supplementary groups set with `setgroups` under `-0`.
56    pub groups: Option<Vec<u32>>,
57    /// Canonical guest path of the running executable, for `/proc/self/exe`.
58    pub exe_guest: Option<Vec<u8>>,
59    /// Host path actually executed, for diagnostics.
60    pub exe_host: Option<Vec<u8>>,
61    /// Working directory as a canonical guest path, kept here without ptrace
62    /// (the kernel's own cwd is then pinned to the supervisor's alias directory).
63    pub cwd: Option<Vec<u8>>,
64    /// The process this one was `vfork`ed from and still shares memory with,
65    /// until it execs; a rewritten exec path then has to be restored there.
66    pub vfork_parent: Option<i32>,
67    /// The view this process lives in; `None` for the root view.
68    pub view: Option<Arc<View>>,
69    /// Set when the process cleared its dumpable flag with `prctl`, which the
70    /// supervisor answers without doing; an exec resets it as the kernel
71    /// resets the flag.
72    pub undumpable: bool,
73    /// Arguments that stand in `/proc/<pid>/cmdline` before the kernel's
74    /// `argv[1..]`: the interpreter, its argument and the script of a `#!`
75    /// program the guest-stub trampoline loaded, where the kernel's file
76    /// keeps the vector the script was exec'd with.
77    pub cmdline_prefix: Option<Vec<Vec<u8>>>,
78    /// Set between a rewritten exec and the trampoline's exec of the real
79    /// target.  Inherited by a child the trampoline forks, which then carries
80    /// out the exec itself.
81    pub pending: Option<PendingExec>,
82}
83
84/// The program a process is about to run through the exec shim.
85#[derive(Clone, Debug)]
86pub struct PendingExec {
87    /// Canonical guest path of the program.
88    pub guest: Vec<u8>,
89    /// Host path handed to the kernel.
90    pub host: Vec<u8>,
91    /// The path string rewritten in a `vfork` parent's memory: its address,
92    /// the original bytes and the bytes written over them.
93    pub restore: Option<(i32, u64, Vec<u8>, Vec<u8>)>,
94    /// The view the process enters at the real exec, with its working
95    /// directory there (a nested proot or bwrap launch).
96    pub enter: Option<(Arc<View>, Vec<u8>)>,
97    /// What becomes [`TraceeInfo::cmdline_prefix`].
98    pub cmdline_prefix: Option<Vec<Vec<u8>>>,
99}
100
101/// The reaper's epoll descriptor and the pidfd it watches for each process.
102#[derive(Debug)]
103struct Reaper {
104    epoll: i32,
105    pidfds: HashMap<i32, i32>,
106}
107
108/// The process table.
109#[derive(Debug)]
110pub struct ProcTable {
111    /// True without ptrace: processes are discovered at their first syscall.
112    pub lazy: bool,
113    procs: RwLock<HashMap<i32, TraceeInfo>>,
114    tids: RwLock<HashMap<i32, (i32, Instant)>>,
115    /// State given to a process whose ancestry leads nowhere known.
116    defaults: RwLock<TraceeInfo>,
117    /// Snapshots taken at `clone`, waiting for the child, keyed by parent.
118    forks: Mutex<HashMap<i32, VecDeque<TraceeInfo>>>,
119    /// Snapshots whose parent exited before the child was seen.
120    orphans: Mutex<VecDeque<(Instant, TraceeInfo)>>,
121    /// At least the number of processes with a pending exec, so the check
122    /// on every notification costs nothing while there is none.
123    shim: AtomicUsize,
124    reaper: Mutex<Option<Reaper>>,
125}
126
127impl ProcTable {
128    /// Creates an empty table.
129    ///
130    /// # Arguments
131    /// * `lazy` - Discover processes at their first syscall (no ptrace).
132    pub fn new(lazy: bool) -> ProcTable {
133        ProcTable {
134            lazy,
135            procs: RwLock::new(HashMap::new()),
136            tids: RwLock::new(HashMap::new()),
137            defaults: RwLock::new(TraceeInfo::default()),
138            forks: Mutex::new(HashMap::new()),
139            orphans: Mutex::new(VecDeque::new()),
140            shim: AtomicUsize::new(0),
141            reaper: Mutex::new(None),
142        }
143    }
144
145    /// Sets the state handed to processes with no known ancestor, and
146    /// registers `root` with it.
147    ///
148    /// # Arguments
149    /// * `root` - Pid of the first tracee.
150    /// * `info` - Its initial state.
151    pub fn register_root(&self, root: i32, info: TraceeInfo) {
152        *self.defaults.write().unwrap() = info.clone();
153        self.procs.write().unwrap().insert(root, info);
154        self.watch(root);
155    }
156
157    /// Thread-group id of thread `tid`, cached for [`TID_TTL`].
158    ///
159    /// A thread that is gone, or not visible, maps to itself.
160    ///
161    /// # Arguments
162    /// * `tid` - Thread id as a notification reports it.
163    pub fn tgid(&self, tid: i32) -> i32 {
164        if let Some((tgid, at)) = self.tids.read().unwrap().get(&tid)
165            && at.elapsed() < TID_TTL
166        {
167            return *tgid;
168        }
169        let tgid = read_status(tid).map(|(t, _)| t).unwrap_or(tid);
170        self.tids.write().unwrap().insert(tid, (tgid, Instant::now()));
171        tgid
172    }
173
174    /// A copy of what is known about the process of thread `tid`.
175    ///
176    /// # Arguments
177    /// * `tid` - Thread or process id.
178    pub fn info(&self, tid: i32) -> TraceeInfo {
179        let tgid = self.tgid(tid);
180        self.ensure(tgid);
181        self.procs.read().unwrap().get(&tgid).cloned().unwrap_or_default()
182    }
183
184    /// Changes the state of the process of thread `tid` in place.
185    ///
186    /// Without ptrace, children forked since the last change are registered
187    /// first, with the state the parent had when it forked them.
188    ///
189    /// # Arguments
190    /// * `tid` - Thread or process id.
191    /// * `f` - The change.
192    pub fn update(&self, tid: i32, f: impl FnOnce(&mut TraceeInfo)) {
193        let tgid = self.tgid(tid);
194        self.ensure(tgid);
195        if self.lazy {
196            self.adopt_children(tgid);
197        }
198        let mut p = self.procs.write().unwrap();
199        f(p.entry(tgid).or_default());
200    }
201
202    /// Registers `child` with a copy of `parent`'s state (ptrace fork event).
203    ///
204    /// # Arguments
205    /// * `parent` - Thread id of the forking thread.
206    /// * `child` - New process (or thread) id.
207    pub fn fork(&self, parent: i32, child: i32) {
208        let tgid = self.tgid(parent);
209        let mut p = self.procs.write().unwrap();
210        let info = p.get(&tgid).cloned().unwrap_or_default();
211        if info.pending.is_some() {
212            self.shim.fetch_add(1, Ordering::Relaxed);
213        }
214        p.insert(child, info);
215    }
216
217    /// The view of the process of thread `tid`, if it has one of its own.
218    ///
219    /// # Arguments
220    /// * `tid` - Thread or process id.
221    pub fn view_of(&self, tid: i32) -> Option<Arc<View>> {
222        let tgid = self.tgid(tid);
223        self.ensure(tgid);
224        self.procs.read().unwrap().get(&tgid).and_then(|t| t.view.clone())
225    }
226
227    /// Takes a snapshot of the forking process for the child to inherit (no ptrace).
228    ///
229    /// # Arguments
230    /// * `tid` - Thread calling `clone`, `fork` or `vfork`.
231    /// * `vfork` - The child shares the parent's memory until it execs.
232    pub fn forked(&self, tid: i32, vfork: bool) {
233        let tgid = self.tgid(tid);
234        let mut info = self.info(tgid);
235        info.vfork_parent = if vfork { Some(tgid) } else { None };
236        self.forks
237            .lock()
238            .unwrap()
239            .entry(tgid)
240            .or_default()
241            .push_back(info);
242    }
243
244    /// Forgets a process or thread that exited.
245    ///
246    /// Snapshots it left for unborn children move to the orphan list.
247    ///
248    /// # Arguments
249    /// * `pid` - Thread or process id.
250    pub fn forget(&self, pid: i32) {
251        self.tids
252            .write()
253            .unwrap()
254            .retain(|t, (g, _)| *t != pid && *g != pid);
255        let Some(gone) = self.procs.write().unwrap().remove(&pid) else {
256            return;
257        };
258        if gone.pending.is_some() {
259            self.shim.fetch_sub(1, Ordering::Relaxed);
260        }
261        if let Some(q) = self.forks.lock().unwrap().remove(&pid) {
262            let now = Instant::now();
263            let mut orphans = self.orphans.lock().unwrap();
264            orphans.extend(q.into_iter().map(|s| (now, s)));
265        }
266        let mut r = self.reaper.lock().unwrap();
267        if let Some(r) = r.as_mut()
268            && let Some(fd) = r.pidfds.remove(&pid)
269        {
270            unsafe { libc::close(fd) };
271        }
272    }
273
274    /// Records the executable the process of `tid` now runs.
275    pub fn set_exe(&self, tid: i32, guest: Vec<u8>, host: Vec<u8>) {
276        self.update(tid, |t| {
277            t.exe_guest = Some(guest);
278            t.exe_host = Some(host);
279            t.undumpable = false;
280        });
281    }
282
283    /// Whether the process of thread `tid` still reads as dumpable.
284    pub fn dumpable(&self, tid: i32) -> bool {
285        !self.info(tid).undumpable
286    }
287
288    /// Records the dumpable flag the process of thread `tid` set.
289    pub fn set_dumpable(&self, tid: i32, dumpable: bool) {
290        self.update(tid, |t| t.undumpable = !dumpable);
291    }
292
293    /// The recorded guest program of thread `tid`, if any.
294    pub fn exe_of(&self, tid: i32) -> Option<Vec<u8>> {
295        self.info(tid).exe_guest
296    }
297
298    /// The virtual working directory of the process of `tid` (no ptrace).
299    pub fn cwd_of(&self, tid: i32) -> Option<Vec<u8>> {
300        self.info(tid).cwd
301    }
302
303    /// Replaces the virtual working directory of the process of `tid`.
304    pub fn set_cwd(&self, tid: i32, cwd: Vec<u8>) {
305        self.update(tid, |t| t.cwd = Some(cwd));
306    }
307
308    /// The virtual identity of the process of `tid` (root until changed).
309    pub fn ids(&self, tid: i32) -> Ids {
310        self.info(tid).ids.unwrap_or_default()
311    }
312
313    /// Replaces the virtual identity of the process of `tid`.
314    pub fn set_ids(&self, tid: i32, ids: Ids) {
315        self.update(tid, |t| t.ids = Some(ids));
316    }
317
318    /// Supplementary groups of the process of `tid`, if `setgroups` was called.
319    pub fn groups(&self, tid: i32) -> Option<Vec<u32>> {
320        self.info(tid).groups
321    }
322
323    /// Records the supplementary groups of the process of `tid`.
324    pub fn set_groups(&self, tid: i32, groups: Vec<u32>) {
325        self.update(tid, |t| t.groups = Some(groups));
326    }
327
328    /// Marks process `tgid` as about to run the exec shim for `pending`.
329    pub fn shim_enter(&self, tgid: i32, pending: PendingExec) {
330        self.shim.fetch_add(1, Ordering::Relaxed);
331        self.update(tgid, |t| {
332            t.pending = Some(pending);
333        });
334    }
335
336    /// The pending exec of process `tgid`, if it is in the shim.
337    pub fn shim_pending(&self, tgid: i32) -> Option<PendingExec> {
338        if self.shim.load(Ordering::Relaxed) == 0 {
339            return None;
340        }
341        self.ensure(tgid);
342        self.procs
343            .read()
344            .unwrap()
345            .get(&tgid)
346            .and_then(|t| t.pending.clone())
347    }
348
349    /// Clears the shim mark of process `tgid`.
350    pub fn shim_leave(&self, tgid: i32) {
351        let mut p = self.procs.write().unwrap();
352        if let Some(t) = p.get_mut(&tgid)
353            && t.pending.take().is_some()
354        {
355            self.shim.fetch_sub(1, Ordering::Relaxed);
356        }
357    }
358
359    /// Makes sure process `tgid` has an entry, discovering its ancestry if needed.
360    fn ensure(&self, tgid: i32) {
361        if !self.lazy || self.procs.read().unwrap().contains_key(&tgid) {
362            return;
363        }
364        self.discover(tgid, 0);
365    }
366
367    /// Registers `tgid` with the state of its parent, walking up as needed.
368    ///
369    /// A parent that is still known contributes the snapshot it took at the
370    /// fork, or its current state.  A process whose parent is gone (reparented
371    /// to init or to the supervisor) takes the oldest orphaned snapshot, or
372    /// the defaults.
373    fn discover(&self, tgid: i32, depth: usize) {
374        let me = std::process::id() as i32;
375        let ppid = read_status(tgid).map(|(_, p)| p).unwrap_or(0);
376        let state = if ppid > 1 && ppid != me && depth < MAX_ANCESTRY {
377            if !self.procs.read().unwrap().contains_key(&ppid) {
378                self.discover(ppid, depth + 1);
379            }
380            let snapshot = self
381                .forks
382                .lock()
383                .unwrap()
384                .get_mut(&ppid)
385                .and_then(|q| q.pop_front());
386            match snapshot {
387                Some(s) => s,
388                None => {
389                    let mut s = self.procs.read().unwrap().get(&ppid).cloned().unwrap_or_default();
390                    s.vfork_parent = None;
391                    s
392                }
393            }
394        } else {
395            let mut orphans = self.orphans.lock().unwrap();
396            while let Some((at, _)) = orphans.front()
397                && at.elapsed() > ORPHAN_TTL
398            {
399                orphans.pop_front();
400            }
401            match orphans.pop_front() {
402                Some((_, s)) => s,
403                None => self.defaults.read().unwrap().clone(),
404            }
405        };
406        let pending = state.pending.is_some();
407        let inserted = self.procs.write().unwrap().try_insert_entry(tgid, state);
408        if inserted {
409            if pending {
410                self.shim.fetch_add(1, Ordering::Relaxed);
411            }
412            self.watch(tgid);
413        }
414    }
415
416    /// Registers the children `tgid` forked since its last state change.
417    ///
418    /// Only done while snapshots are pending, since finding children means
419    /// scanning `/proc`.
420    fn adopt_children(&self, tgid: i32) {
421        if self.forks.lock().unwrap().get(&tgid).is_none_or(|q| q.is_empty()) {
422            return;
423        }
424        let Ok(dir) = std::fs::read_dir("/proc") else {
425            return;
426        };
427        for entry in dir.flatten() {
428            let Some(pid) = entry.file_name().to_str().and_then(|n| n.parse::<i32>().ok()) else {
429                continue;
430            };
431            if self.procs.read().unwrap().contains_key(&pid) {
432                continue;
433            }
434            if read_status(pid).map(|(_, p)| p) != Some(tgid) {
435                continue;
436            }
437            let snapshot = self
438                .forks
439                .lock()
440                .unwrap()
441                .get_mut(&tgid)
442                .and_then(|q| q.pop_front());
443            let Some(state) = snapshot else { break };
444            let pending = state.pending.is_some();
445            if self.procs.write().unwrap().try_insert_entry(pid, state) {
446                if pending {
447                    self.shim.fetch_add(1, Ordering::Relaxed);
448                }
449                self.watch(pid);
450            }
451        }
452    }
453
454    /// Starts watching `tgid` for exit, if a reaper is running.
455    fn watch(&self, tgid: i32) {
456        let mut r = self.reaper.lock().unwrap();
457        let Some(r) = r.as_mut() else { return };
458        if r.pidfds.contains_key(&tgid) {
459            return;
460        }
461        let pidfd = unsafe { libc::syscall(libc::SYS_pidfd_open, tgid, 0) } as i32;
462        if pidfd < 0 {
463            return;
464        }
465        let mut ev = libc::epoll_event {
466            events: libc::EPOLLIN as u32,
467            u64: tgid as u32 as u64,
468        };
469        if unsafe { libc::epoll_ctl(r.epoll, libc::EPOLL_CTL_ADD, pidfd, &mut ev) } < 0 {
470            unsafe { libc::close(pidfd) };
471            return;
472        }
473        r.pidfds.insert(tgid, pidfd);
474    }
475
476    /// Starts the reaper thread, which forgets processes as they exit.
477    ///
478    /// Processes registered before the reaper started are watched from here on.
479    ///
480    /// # Arguments
481    /// * `table` - The table to reap; the thread holds a reference for its life.
482    ///
483    /// # Errors
484    /// Errno from `epoll_create1`.
485    pub fn start_reaper(table: &Arc<ProcTable>) -> Result<()> {
486        let epoll = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) };
487        if epoll < 0 {
488            return Err(errno());
489        }
490        *table.reaper.lock().unwrap() = Some(Reaper {
491            epoll,
492            pidfds: HashMap::new(),
493        });
494        let known: Vec<i32> = table.procs.read().unwrap().keys().copied().collect();
495        for pid in known {
496            table.watch(pid);
497        }
498        let t = table.clone();
499        std::thread::Builder::new()
500            .name("reaper".into())
501            .spawn(move || {
502                let mut events = [libc::epoll_event { events: 0, u64: 0 }; 64];
503                loop {
504                    let n = unsafe { libc::epoll_wait(epoll, events.as_mut_ptr(), 64, -1) };
505                    if n < 0 {
506                        if errno() == libc::EINTR {
507                            continue;
508                        }
509                        return;
510                    }
511                    for ev in &events[..n as usize] {
512                        t.forget(ev.u64 as u32 as i32);
513                    }
514                }
515            })
516            .map_err(|_| libc::EAGAIN)?;
517        Ok(())
518    }
519}
520
521/// Inserts only when the key is absent; true when inserted.
522trait TryInsert {
523    fn try_insert_entry(&mut self, key: i32, value: TraceeInfo) -> bool;
524}
525
526impl TryInsert for HashMap<i32, TraceeInfo> {
527    fn try_insert_entry(&mut self, key: i32, value: TraceeInfo) -> bool {
528        if self.contains_key(&key) {
529            return false;
530        }
531        self.insert(key, value);
532        true
533    }
534}
535
536/// `(Tgid, PPid)` of a thread from `/proc/<tid>/status`.
537///
538/// # Arguments
539/// * `tid` - Thread id.
540pub fn read_status(tid: i32) -> Option<(i32, i32)> {
541    let s = std::fs::read_to_string(format!("/proc/{tid}/status")).ok()?;
542    let (mut tgid, mut ppid) = (None, None);
543    for line in s.lines() {
544        if let Some(v) = line.strip_prefix("Tgid:") {
545            tgid = v.trim().parse().ok();
546        } else if let Some(v) = line.strip_prefix("PPid:") {
547            ppid = v.trim().parse().ok();
548        }
549        if tgid.is_some() && ppid.is_some() {
550            break;
551        }
552    }
553    Some((tgid?, ppid?))
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559
560    #[test]
561    fn eager_table_copies_on_fork_and_forgets() {
562        let me = std::process::id() as i32;
563        let t = ProcTable::new(false);
564        t.set_exe(me, b"/bin/x".to_vec(), b"/host/x".to_vec());
565        t.fork(me, 4_000_001);
566        assert_eq!(t.exe_of(4_000_001), Some(b"/bin/x".to_vec()));
567        t.forget(4_000_001);
568        assert_eq!(t.exe_of(4_000_001), None);
569        assert_eq!(t.tgid(me), me);
570    }
571
572    #[test]
573    fn lazy_table_inherits_from_the_parent_and_takes_snapshots() {
574        let me = std::process::id() as i32;
575        let (_, parent) = read_status(me).unwrap();
576        let t = ProcTable::new(true);
577        t.register_root(
578            parent,
579            TraceeInfo {
580                cwd: Some(b"/start".to_vec()),
581                ..Default::default()
582            },
583        );
584        assert_eq!(t.cwd_of(me), Some(b"/start".to_vec()));
585        t.forked(me, false);
586        t.set_cwd(me, b"/later".to_vec());
587        let snapshot = t.forks.lock().unwrap().get(&me).and_then(|q| q.front().cloned());
588        assert_eq!(snapshot.and_then(|s| s.cwd), Some(b"/start".to_vec()));
589        assert_eq!(t.cwd_of(me), Some(b"/later".to_vec()));
590        t.forget(me);
591        let orphan = t.orphans.lock().unwrap().front().cloned();
592        assert_eq!(orphan.and_then(|(_, s)| s.cwd), Some(b"/start".to_vec()));
593    }
594
595    #[test]
596    fn shim_marks_are_per_process() {
597        let t = ProcTable::new(false);
598        assert!(t.shim_pending(7).is_none());
599        t.shim_enter(
600            7,
601            PendingExec {
602                guest: b"/bin/true".to_vec(),
603                host: b"/r/bin/true".to_vec(),
604                restore: None,
605                enter: None,
606                cmdline_prefix: None,
607            },
608        );
609        assert_eq!(t.shim_pending(7).map(|p| p.guest), Some(b"/bin/true".to_vec()));
610        t.fork(7, 8);
611        assert_eq!(t.shim_pending(8).map(|p| p.host), Some(b"/r/bin/true".to_vec()));
612        t.shim_leave(7);
613        assert!(t.shim_pending(7).is_none());
614        assert!(t.shim_pending(8).is_some());
615        t.forget(8);
616        assert_eq!(t.shim.load(Ordering::Relaxed), 0);
617    }
618}