Skip to main content

proot/syscall/
direct.rs

1//! Handlers for the syscalls that take a ptrace stop when ptrace is
2//! available, served through user notification when it is not.
3//!
4//! A notification cannot change registers, only answer the syscall, let it
5//! continue, or install a descriptor into the tracee.  Three tricks cover
6//! the former ptrace tier:
7//!
8//! * `chdir`, `fchdir` and `chroot` are answered without running: the
9//!   working directory is virtual (`procs`), every path handler resolves
10//!   relative paths against it, and the kernel's own cwd of every tracee
11//!   stays pinned to the alias directory.
12//! * `connect` and `bind` are performed by the supervisor on a socket of
13//!   the same type and flags, which then replaces the tracee's descriptor
14//!   through `ADDFD` with `SETFD`.
15//! * `execve`, `inotify_add_watch` and `O_PATH` opens must run in the tracee
16//!   with a different path.  The path string is overwritten in place,
17//!   through `/proc/<pid>/mem`, with a name of one or two bytes that
18//!   resolves inside the alias directory, which the kernel cwd makes
19//!   reachable from any process: `e` is the exec trampoline (see
20//!   `exec::shim`) and a transient one-character symlink stands for any
21//!   other host path.  A symlink to be reached by a bare name, which no
22//!   `<alias>/<name>` fits, is reached through a hard link to it instead,
23//!   the same inode.  The syscall then continues.  Once the task has
24//!   consumed the argument, the original bytes are put back, but only when
25//!   the memory still holds exactly what was written: a buffer may have
26//!   been freed or reused by then, and must not be touched if so.
27//!
28//! `clone` and its relatives are notified only to snapshot the parent's
29//! state for the child, see `procs`.
30//!
31//! In the guest-stub restricted mode (see `guest_stub`) the strings these
32//! handlers rewrite are the stub's copies in the mailbox, with room for any
33//! host path, so the full host path is written and nothing is put back.  An
34//! exec is planned from the staged copy and answered with the mailbox
35//! descriptor installed for the trampoline, which the stub then execs itself.
36
37use crate::exec::nested::EnvSource;
38use crate::exec::shim::Trapped;
39use crate::exec::{self, ExecPlan, shim};
40use crate::fs::canon::RETRY;
41use crate::fs::translate::{guest_dirfd_path, guest_fd_path, read_link, strip_deleted};
42use crate::guest_stub::layout;
43use crate::note;
44use crate::seccomp::notify::{addfd, addfd_at, reply_continue};
45use crate::state::Ctx;
46use crate::sys::{Result, errno};
47use crate::syscall::notify::{Outcome, Req};
48use crate::syscall::nr::*;
49use std::ffi::{CString, OsStr};
50use std::os::unix::ffi::OsStrExt;
51use std::os::unix::fs::DirBuilderExt;
52use std::path::{Path, PathBuf};
53use std::sync::Mutex;
54use std::time::{Duration, Instant};
55
56/// `CLONE_VFORK`: the child shares the parent's memory until it execs.
57const CLONE_VFORK: u64 = 0x4000;
58/// `CLONE_THREAD`: the new task shares the process, nothing to inherit.
59const CLONE_THREAD: u64 = 0x10000;
60/// Longest wait for a continued syscall to return before restoring anyway.
61const RESTORE_TIMEOUT: Duration = Duration::from_secs(2);
62/// CPU time a resumed task must have run for its syscall to be past the
63/// argument copy and the path walk, with a wide margin over the microseconds
64/// those take.
65const SETTLED_CPU: u64 = 100_000;
66
67/// Characters available for transient alias names; `e` and `r` are the
68/// trampoline and its records.
69const ALIAS_CHARS: &[u8] = b"0123456789abcdfghijklmnopqstuvwxyzABCDFGHIJKLMNOPQSTUVWXYZ";
70
71/// Which names are in use, and where the next search starts: names are
72/// handed out round-robin so a name just released is not re-pointed while
73/// the kernel may still be walking through it.
74static ALIASES: Mutex<([bool; ALIAS_CHARS.len()], usize)> = Mutex::new(([false; ALIAS_CHARS.len()], 0));
75
76/// Directories holding hard links to symlinks, one per mount reached so far.
77///
78/// `linkat` refuses a link across mounts, and the alias directory usually
79/// sits on a tmpfs of its own, so a symlink elsewhere is linked from a
80/// directory created on its mount.
81static STAGING: Mutex<Vec<PathBuf>> = Mutex::new(Vec::new());
82
83/// Routes a former trace-tier syscall to its handler; `None` for any other.
84///
85/// # Arguments
86/// * `r` - The notification.
87/// * `nr` - Canonical syscall number.
88pub fn dispatch(r: &Req, nr: i64) -> Option<Result<Outcome>> {
89    Some(match nr {
90        EXECVE | EXECVEAT if r.ctx.trap_mode() => do_execve_trapped(r, nr == EXECVEAT),
91        EXECVE => do_execve(r, false),
92        EXECVEAT => do_execve(r, true),
93        CHDIR => r.path(0).and_then(|p| set_cwd(r, libc::AT_FDCWD, &p)),
94        FCHDIR => guest_fd_path(r.fs(), r.pid, r.arg(0) as i32).and_then(|p| set_cwd(r, libc::AT_FDCWD, &p)),
95        CHROOT => do_chroot(r),
96        CONNECT => do_sockaddr(r, r.arg(0) as i32, r.arg(1), r.arg(2) as usize, true),
97        BIND => do_sockaddr(r, r.arg(0) as i32, r.arg(1), r.arg(2) as usize, false),
98        INOTIFY_ADD_WATCH => do_inotify_add_watch(r),
99        CLONE | FORK | VFORK | CLONE3 => do_clone(r, nr),
100        _ => return None,
101    })
102}
103
104/// Lets the trampoline's own syscalls through untranslated.
105///
106/// Between the rewritten exec and the trampoline's exec of the real target,
107/// the process opens and unlinks its record, and carries out the setup of a
108/// nested launch, with host paths.  The mark is verified against
109/// `/proc/<pid>/exe` so a program whose rewritten exec failed is not left
110/// untranslated.  Serves both modes: with ptrace the trampoline's notified
111/// syscalls come here and its traced ones to `syscall::traced`.
112///
113/// # Arguments
114/// * `r` - The notification.
115/// * `nr` - Canonical syscall number.
116///
117/// # Returns
118/// `Some(Continue)` for the trampoline, `None` otherwise.
119pub fn shim_passthrough(r: &Req, nr: i64) -> Option<Outcome> {
120    if matches!(nr, EXECVE | EXECVEAT) || r.ctx.trap_mode() {
121        return None;
122    }
123    let tgid = r.tgid();
124    r.ctx.procs.shim_pending(tgid)?;
125    if shim::running(r.pid) {
126        return Some(Outcome::Continue);
127    }
128    r.ctx.procs.shim_leave(tgid);
129    None
130}
131
132/// `socketcall(call, args)` from an i386 tracee: `bind` (2) and `connect` (3).
133///
134/// # Arguments
135/// * `r` - The notification.
136pub fn do_socketcall(r: &Req) -> Result<Outcome> {
137    let call = r.arg(0);
138    if call != 2 && call != 3 {
139        return Ok(Outcome::Continue);
140    }
141    let raw = r.read(r.arg(1), 12)?;
142    let word = |i: usize| u32::from_ne_bytes(raw[i * 4..i * 4 + 4].try_into().unwrap()) as u64;
143    do_sockaddr(r, word(0) as i32, word(1), word(2) as usize, call == 3)
144}
145
146/// `execve`/`execveat` through the trampoline.
147///
148/// The plan is checked the way the kernel would check it, so a program the
149/// kernel would refuse is refused here and the caller survives with the
150/// errno.  The record is written, the process marked, and the path string
151/// replaced by `e`.  An `execveat` of a descriptor (`AT_EMPTY_PATH`) has
152/// that descriptor replaced by one on the trampoline instead; a relative
153/// `execveat` path has its `dirfd` replaced by the alias directory so `e`
154/// resolves there.  A `vfork` child's path string belongs to its parent and
155/// is restored there later.
156///
157/// A notification from a process already marked, and verified to be running
158/// the trampoline, is the trampoline's exec of the real target: a host path
159/// the kernel resolves as is, let through once the program is recorded.
160///
161/// # Arguments
162/// * `r` - The notification.
163/// * `is_at` - `execveat` rather than `execve`.
164fn do_execve(r: &Req, is_at: bool) -> Result<Outcome> {
165    let tgid = r.tgid();
166    if let Some(p) = r.ctx.procs.shim_pending(tgid) {
167        r.ctx.procs.shim_leave(tgid);
168        if shim::running(r.pid) {
169            if let Some((parent, addr, orig, written)) = &p.restore {
170                restore_if_intact(r, *parent as u32, *addr, orig, written);
171            }
172            exec::finish_pending(r.ctx, tgid, p);
173            return Ok(Outcome::Continue);
174        }
175    }
176    let (dirfd, path_idx, argv_idx, at_flags) = if is_at {
177        (r.arg(0) as i32, 1, 2, r.arg(4) as i32)
178    } else {
179        (libc::AT_FDCWD, 0, 1, 0)
180    };
181    let user_path = r.path(path_idx)?;
182    let argv = exec::read_argv(r.ctx, r.pid, r.arg(argv_idx), r.abi.ptr_size())?;
183    let env = EnvSource::Address {
184        envp: r.arg(argv_idx + 1),
185        ptr_size: r.abi.ptr_size(),
186    };
187    let empty_path = is_at && user_path.is_empty();
188    let plan = if empty_path {
189        if at_flags & libc::AT_EMPTY_PATH == 0 {
190            return Err(libc::ENOENT);
191        }
192        let guest = guest_dirfd_path(r.fs(), r.pid, dirfd)?;
193        exec::plan(r.ctx, r.pid, libc::AT_FDCWD, &guest, argv, true, env)?
194    } else {
195        let deref = at_flags & libc::AT_SYMLINK_NOFOLLOW == 0;
196        exec::plan(r.ctx, r.pid, dirfd, &user_path, argv, deref, env)?
197    };
198    if !is_at && !plan.changed {
199        r.ctx.set_exe(tgid, plan.guest_prog, plan.host_prog);
200        return Ok(Outcome::Continue);
201    }
202    if plan.trampoline.is_none() {
203        check_executable(&plan)?;
204    }
205    r.valid()?;
206    exec::debug_exec(r.pid, &plan);
207    shim::write_record(tgid, &plan, None)?;
208    let vfork_parent = r.ctx.tracee(tgid).vfork_parent;
209    let mut orig = user_path.clone();
210    orig.push(0);
211    let mut pending = exec::pending_of(&plan);
212    pending.restore = vfork_parent
213        .filter(|_| !empty_path)
214        .map(|parent| (parent, r.arg(path_idx), orig, b"e\0".to_vec()));
215    r.ctx.procs.shim_enter(tgid, pending);
216    let undo = |e: i32| {
217        r.ctx.procs.shim_leave(tgid);
218        if let Some(p) = shim::record_path(tgid) {
219            let _ = std::fs::remove_file(p);
220        }
221        e
222    };
223    let Some(shim_path) = shim::path() else {
224        return Err(undo(libc::ENOENT));
225    };
226    if empty_path {
227        let fd =
228            open_host(shim_path.as_os_str().as_bytes(), libc::O_RDONLY | libc::O_CLOEXEC).map_err(undo)?;
229        addfd_at(r.listener, r.id, fd, dirfd, true).map_err(undo)?;
230        return Ok(Outcome::Continue);
231    }
232    r.ctx
233        .mem
234        .write_force(r.pid, r.arg(path_idx), b"e\0")
235        .map_err(undo)?;
236    if is_at && dirfd != libc::AT_FDCWD && user_path.first() != Some(&b'/') {
237        let dir = shim_path
238            .parent()
239            .map(|d| d.as_os_str().as_bytes().to_vec())
240            .unwrap_or_default();
241        let fd = open_host(&dir, libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC).map_err(undo)?;
242        addfd_at(r.listener, r.id, fd, dirfd, true).map_err(undo)?;
243    }
244    Ok(Outcome::Continue)
245}
246
247/// `execve`/`execveat` in the guest-stub restricted mode.
248///
249/// The stub staged the path and the vectors, or only `argv[0]` when they
250/// did not fit, which is enough for anything but a nested launch.  The plan
251/// is checked as [`do_execve`] checks it, plus what the trampoline's loader
252/// cannot run; the mailbox is installed into the caller, the record
253/// written, and the answer is the mailbox's descriptor number, on which
254/// the stub execs the trampoline.
255///
256/// # Arguments
257/// * `r` - The notification.
258/// * `is_at` - `execveat` rather than `execve`.
259///
260/// # Returns
261/// The mailbox's descriptor number in the caller, as the call's result.
262///
263/// # Errors
264/// The errno the kernel would refuse the exec with, `E2BIG` for vectors too
265/// large to stage when the plan needs them, or the error of reading the
266/// arguments or installing the mailbox.
267fn do_execve_trapped(r: &Req, is_at: bool) -> Result<Outcome> {
268    let (dirfd, path_idx, argv_idx, at_flags) = if is_at {
269        (r.arg(0) as i32, 1, 2, r.arg(4) as i32)
270    } else {
271        (libc::AT_FDCWD, 0, 1, 0)
272    };
273    let truncated = r.arg(layout::EXEC_TRUNCATED_ARG) != 0;
274    let user_path = r.path(path_idx)?;
275    let argv = exec::read_argv(r.ctx, r.pid, r.arg(argv_idx), r.abi.ptr_size())?;
276    let env = EnvSource::Address {
277        envp: r.arg(argv_idx + 1),
278        ptr_size: r.abi.ptr_size(),
279    };
280    let plan = if is_at && user_path.is_empty() {
281        if at_flags & libc::AT_EMPTY_PATH == 0 {
282            return Err(libc::ENOENT);
283        }
284        let guest = guest_dirfd_path(r.fs(), r.pid, dirfd)?;
285        exec::plan(r.ctx, r.pid, libc::AT_FDCWD, &guest, argv, true, env)?
286    } else {
287        let deref = at_flags & libc::AT_SYMLINK_NOFOLLOW == 0;
288        exec::plan(r.ctx, r.pid, dirfd, &user_path, argv, deref, env)?
289    };
290    if truncated && plan.trampoline.is_some() {
291        return Err(libc::E2BIG);
292    }
293    let narrow = check_loadable(r.ctx, &plan)?;
294    r.valid()?;
295    exec::debug_exec(r.pid, &plan);
296    let mailbox = r.ctx.mailbox.ok_or(libc::ENOSYS)?;
297    let copy = unsafe { libc::fcntl(mailbox, libc::F_DUPFD_CLOEXEC, 0) };
298    if copy < 0 {
299        return Err(errno());
300    }
301    let fd = addfd(r.listener, r.id, copy, false)?;
302    commit_trapped(r.ctx, r.tgid(), &plan, fd, narrow)?;
303    Ok(Outcome::Value(Ok(fd as i64)))
304}
305
306/// Plans the first tracee's exec in the guest-stub restricted mode, which
307/// the supervisor itself sets up rather than a stub: the tracee execs the
308/// trampoline through the allow gate with the mailbox it inherited.
309///
310/// # Arguments
311/// * `ctx` - Shared supervisor state.
312/// * `pid` - The first tracee, registered in the process table.
313/// * `path` - The command as given.
314/// * `argv` - Its argument vector.
315/// * `env` - Its environment, as names and values.
316///
317/// # Errors
318/// The errno the exec would have failed with.
319pub fn plan_first_trapped(
320    ctx: &Ctx,
321    pid: i32,
322    path: &[u8],
323    argv: Vec<Vec<u8>>,
324    env: Vec<(Vec<u8>, Vec<u8>)>,
325) -> Result<()> {
326    let plan = exec::plan(
327        ctx,
328        pid as u32,
329        libc::AT_FDCWD,
330        path,
331        argv,
332        true,
333        EnvSource::Given(env),
334    )?;
335    let narrow = check_loadable(ctx, &plan)?;
336    exec::debug_exec(pid as u32, &plan);
337    commit_trapped(ctx, pid, &plan, ctx.mailbox.ok_or(libc::ENOSYS)?, narrow)
338}
339
340/// Writes the trampoline's record for a planned exec in the guest-stub mode
341/// and marks the process as about to run it.
342///
343/// # Arguments
344/// * `ctx` - Shared supervisor state.
345/// * `tgid` - The process.
346/// * `plan` - Its exec; its `user_path` becomes `AT_EXECFN` and the name.
347/// * `mailbox` - The mailbox's descriptor number in the process.
348/// * `narrow` - The program is 32-bit, for the 32-bit trampoline.
349///
350/// # Errors
351/// Errno from writing the record.
352fn commit_trapped(ctx: &Ctx, tgid: i32, plan: &ExecPlan, mailbox: i32, narrow: bool) -> Result<()> {
353    let execfn = if plan.user_path.is_empty() {
354        plan.guest_prog.clone()
355    } else {
356        plan.user_path.clone()
357    };
358    let mut name = exec::exec_name(&plan.user_path, &plan.guest_prog).to_vec();
359    name.truncate(15);
360    let trapped = Trapped {
361        mailbox,
362        execfn,
363        name,
364        narrow,
365    };
366    shim::write_record(tgid, plan, Some(&trapped))?;
367    let mut pending = exec::pending_of(plan);
368    if plan.trampoline.is_none() && plan.prefix > 1 {
369        pending.cmdline_prefix = Some(plan.argv[..plan.prefix].to_vec());
370    }
371    ctx.procs.shim_enter(tgid, pending);
372    Ok(())
373}
374
375/// Refuses, in the guest-stub mode, a plan the trampolines' loaders cannot
376/// carry out, and tells which trampoline loads the rest.  Besides what the
377/// kernel refuses (see [`check_executable`]) that is a format only
378/// `binfmt_misc` would run, and a 32-bit program where the i386 trampoline
379/// cannot run (a kernel without ia32 emulation).  A nested launch, which
380/// the trampoline sets up first, is only classified.
381///
382/// # Arguments
383/// * `ctx` - Shared supervisor state, for the diagnostic.
384/// * `plan` - The exec.
385///
386/// # Returns
387/// Whether the program is 32-bit, to be loaded by the i386 trampoline.
388///
389/// # Errors
390/// The kernel's errno, `ENOEXEC` for an unknown format, `EOPNOTSUPP` for a
391/// 32-bit program without the i386 trampoline.
392fn check_loadable(ctx: &Ctx, plan: &ExecPlan) -> Result<bool> {
393    if plan.trampoline.is_some() {
394        return Ok(!plan.exec_path.is_empty()
395            && matches!(
396                exec::elf::classify_file(&plan.exec_path),
397                Ok(exec::elf::FileKind::Elf32(_))
398            ));
399    }
400    check_executable(plan)?;
401    match exec::elf::classify_file(&plan.exec_path) {
402        Ok(exec::elf::FileKind::Elf64(_)) => Ok(false),
403        Ok(exec::elf::FileKind::Elf32(_)) if shim::has_32bit() => Ok(true),
404        Ok(exec::elf::FileKind::Elf32(_)) => {
405            note!(
406                ctx,
407                1,
408                "{}: 32-bit programs cannot run here (no i386 trampoline)",
409                String::from_utf8_lossy(&plan.guest_prog)
410            );
411            Err(libc::EOPNOTSUPP)
412        }
413        _ => Err(libc::ENOEXEC),
414    }
415}
416
417/// Answers the trampoline's [`layout::HELLO`]: the exec planned for the
418/// process has happened, so it now runs the program, in the view the plan
419/// named.
420///
421/// # Arguments
422/// * `ctx` - Shared supervisor state.
423/// * `tid` - The calling thread.
424pub fn hello(ctx: &Ctx, tid: u32) {
425    let tgid = ctx.tgid(tid as i32);
426    if let Some(p) = ctx.procs.shim_pending(tgid) {
427        ctx.procs.shim_leave(tgid);
428        exec::finish_pending(ctx, tgid, p);
429    }
430}
431
432/// Refuses a plan the kernel would refuse, with the kernel's errno.
433///
434/// A shell relies on `ENOEXEC` to run a script without `#!` itself; a
435/// trampoline that died instead would take the shell with it.
436fn check_executable(plan: &exec::ExecPlan) -> Result<()> {
437    let c = CString::new(plan.host_prog.clone()).map_err(|_| libc::ENOENT)?;
438    let mut st: libc::stat = unsafe { std::mem::zeroed() };
439    if unsafe { libc::stat(c.as_ptr(), &mut st) } < 0 {
440        return Err(errno());
441    }
442    if st.st_mode & libc::S_IFMT == libc::S_IFDIR {
443        return Err(libc::EACCES);
444    }
445    if unsafe { libc::access(c.as_ptr(), libc::X_OK) } < 0 {
446        return Err(errno());
447    }
448    if !plan.understood && !binfmt_misc_matches(&plan.host_prog) {
449        return Err(libc::ENOEXEC);
450    }
451    Ok(())
452}
453
454/// Whether a registered `binfmt_misc` handler would run a file we do not
455/// recognize, by the same magic or extension test the kernel applies.
456///
457/// # Arguments
458/// * `host` - Host path of the file.
459fn binfmt_misc_matches(host: &[u8]) -> bool {
460    let Ok(entries) = std::fs::read_dir("/proc/sys/fs/binfmt_misc") else {
461        return false;
462    };
463    let head = std::fs::read(std::ffi::OsStr::from_bytes(host))
464        .map(|mut d| {
465            d.truncate(256);
466            d
467        })
468        .unwrap_or_default();
469    let name = host.rsplit(|&b| b == b'/').next().unwrap_or(host);
470    for entry in entries.flatten() {
471        if entry.file_name() == "register" || entry.file_name() == "status" {
472            continue;
473        }
474        let Ok(text) = std::fs::read_to_string(entry.path()) else {
475            continue;
476        };
477        if !text.starts_with("enabled") {
478            continue;
479        }
480        let field = |key: &str| {
481            text.lines()
482                .find_map(|l| l.strip_prefix(key))
483                .map(|v| v.trim().to_string())
484        };
485        if let Some(ext) = field("extension ") {
486            if name.ends_with(format!(".{ext}").as_bytes()) {
487                return true;
488            }
489            continue;
490        }
491        let (Some(magic), offset) = (
492            field("magic "),
493            field("offset ")
494                .and_then(|o| o.parse::<usize>().ok())
495                .unwrap_or(0),
496        ) else {
497            continue;
498        };
499        let unhex = |h: &str| -> Vec<u8> {
500            h.as_bytes()
501                .chunks(2)
502                .filter_map(|c| u8::from_str_radix(std::str::from_utf8(c).ok()?, 16).ok())
503                .collect()
504        };
505        let magic = unhex(&magic);
506        let mask = field("mask ")
507            .map(|m| unhex(&m))
508            .unwrap_or_else(|| vec![0xff; magic.len()]);
509        if head.len() < offset + magic.len() {
510            continue;
511        }
512        if magic
513            .iter()
514            .zip(mask.iter().chain(std::iter::repeat(&0xff)))
515            .zip(&head[offset..])
516            .all(|((m, k), b)| b & k == m & k)
517        {
518            return true;
519        }
520    }
521    false
522}
523
524/// Opens a host path for injection.
525fn open_host(host: &[u8], flags: i32) -> Result<i32> {
526    let c = CString::new(host.to_vec()).map_err(|_| libc::EINVAL)?;
527    let fd = unsafe { libc::open(c.as_ptr(), flags) };
528    if fd < 0 { Err(errno()) } else { Ok(fd) }
529}
530
531/// Moves the virtual working directory to `path`, with `chdir`'s own checks.
532fn set_cwd(r: &Req, dirfd: i32, path: &[u8]) -> Result<Outcome> {
533    let guest = retry(|| {
534        let resolved = r.translate(dirfd, path, true)?;
535        let mode = resolved.mode_nofollow()?;
536        if mode & libc::S_IFMT == libc::S_IFLNK && resolved.pinned() {
537            return Err(RETRY);
538        }
539        if mode & libc::S_IFMT != libc::S_IFDIR {
540            return Err(libc::ENOTDIR);
541        }
542        let (dfd, c, nofollow) = resolved.at(false)?;
543        let flags = if nofollow { libc::AT_SYMLINK_NOFOLLOW } else { 0 };
544        if unsafe { libc::syscall(libc::SYS_faccessat2, dfd, c.as_ptr(), libc::X_OK, flags) } < 0 {
545            return Err(errno());
546        }
547        Ok(resolved.guest)
548    })?;
549    r.valid()?;
550    r.ctx.procs.set_cwd(r.pid as i32, guest);
551    Ok(Outcome::Value(Ok(0)))
552}
553
554/// Runs a lookup again while it finds a symlink swapped in since the walk.
555fn retry<T>(mut f: impl FnMut() -> Result<T>) -> Result<T> {
556    for _ in 0..3 {
557        match f() {
558            Err(RETRY) => continue,
559            r => return r,
560        }
561    }
562    Err(libc::ELOOP)
563}
564
565/// `chroot`: the guest root is a no-op, anything else is refused as it would
566/// be for an unprivileged process.
567fn do_chroot(r: &Req) -> Result<Outcome> {
568    let resolved = r.translate(libc::AT_FDCWD, &r.path(0)?, true)?;
569    if resolved.guest == b"/" {
570        return Ok(Outcome::Value(Ok(0)));
571    }
572    Err(libc::EPERM)
573}
574
575/// `connect`/`bind` on an `AF_UNIX` path, done on a replacement socket.
576///
577/// The tracee's socket cannot be reached (`pidfd_getfd` is not allowed in a
578/// default container), so a new socket of the same type, with the same
579/// `O_NONBLOCK` and `O_CLOEXEC`, is connected or bound by the supervisor
580/// and installed over the tracee's descriptor number.  The address names
581/// the socket through its pinned parent, `/proc/self/fd/<parent>/<name>`,
582/// since the socket calls have no `dirfd` form: the parents cannot be
583/// redirected, while the kernel resolves the final name itself, so a
584/// symlink the guest swaps in there during a `connect` is followed.
585/// Options set on the old socket before this call, and a `bind` made before
586/// a `connect`, do not carry over.  A descriptor that is not a unix socket
587/// is left to the kernel, which reports the mismatch.
588///
589/// # Arguments
590/// * `r` - The notification.
591/// * `sockfd` - The tracee's socket.
592/// * `addr` - Address of the `sockaddr` in the tracee.
593/// * `len` - Its length.
594/// * `connect` - `connect` (follow a final symlink) rather than `bind`.
595fn do_sockaddr(r: &Req, sockfd: i32, addr: u64, len: usize, connect: bool) -> Result<Outcome> {
596    if addr == 0 || len <= 2 || len > 110 {
597        return Ok(Outcome::Continue);
598    }
599    let sa = r.read(addr, len)?;
600    if u16::from_ne_bytes([sa[0], sa[1]]) != libc::AF_UNIX as u16 || sa[2] == 0 {
601        return Ok(Outcome::Continue);
602    }
603    let end = sa[2..].iter().position(|&b| b == 0).map(|p| p + 2).unwrap_or(len);
604    let path = sa[2..end].to_vec();
605    let resolved = r.translate(libc::AT_FDCWD, &path, connect)?;
606    if resolved.host == path {
607        return Ok(Outcome::Continue);
608    }
609    if !connect {
610        r.check_writable(&resolved)?;
611    }
612    let host = resolved.parent_path()?.into_bytes();
613    if host.len() >= 108 {
614        return Err(libc::ENAMETOOLONG);
615    }
616    let Some((kind, flags)) = socket_description(r.pid, sockfd) else {
617        return Ok(Outcome::Continue);
618    };
619    let nonblock = flags & libc::O_NONBLOCK != 0;
620    let cloexec = flags & libc::O_CLOEXEC != 0;
621    let new = unsafe {
622        libc::socket(
623            libc::AF_UNIX,
624            kind | libc::SOCK_CLOEXEC | if nonblock { libc::SOCK_NONBLOCK } else { 0 },
625            0,
626        )
627    };
628    if new < 0 {
629        return Err(errno());
630    }
631    let mut sun: libc::sockaddr_un = unsafe { std::mem::zeroed() };
632    sun.sun_family = libc::AF_UNIX as u16;
633    for (i, b) in host.iter().enumerate() {
634        sun.sun_path[i] = *b as libc::c_char;
635    }
636    let sun_len = (2 + host.len() + 1) as libc::socklen_t;
637    let sun_ptr = &sun as *const libc::sockaddr_un as *const libc::sockaddr;
638    let rc = unsafe {
639        if connect {
640            libc::connect(new, sun_ptr, sun_len)
641        } else {
642            libc::bind(new, sun_ptr, sun_len)
643        }
644    };
645    if rc < 0 {
646        let e = errno();
647        unsafe { libc::close(new) };
648        return Err(e);
649    }
650    note!(
651        r.ctx,
652        2,
653        "pid {} {} fd {} -> {}",
654        r.pid,
655        if connect { "connect" } else { "bind" },
656        sockfd,
657        String::from_utf8_lossy(&resolved.host)
658    );
659    r.valid().inspect_err(|_| unsafe {
660        libc::close(new);
661    })?;
662    addfd_at(r.listener, r.id, new, sockfd, cloexec)?;
663    Ok(Outcome::Value(Ok(0)))
664}
665
666/// Type and descriptor flags of a tracee's unix socket.
667///
668/// The inode from `/proc/<pid>/fd/N` is looked up in `/proc/net/unix` for
669/// the socket type; `/proc/<pid>/fdinfo/N` gives the flags.
670///
671/// # Arguments
672/// * `tid` - Tracee thread id.
673/// * `fd` - Descriptor number in the tracee.
674///
675/// # Returns
676/// `(SOCK_* type, O_* flags)`, or `None` when it is not a unix socket.
677fn socket_description(tid: u32, fd: i32) -> Option<(i32, i32)> {
678    let link = read_link(format!("/proc/{tid}/fd/{fd}").as_bytes()).ok()?;
679    let inode = link.strip_prefix(b"socket:[")?.strip_suffix(b"]")?;
680    let inode = std::str::from_utf8(inode).ok()?;
681    let table = std::fs::read_to_string("/proc/net/unix").ok()?;
682    let kind = table.lines().skip(1).find_map(|l| {
683        let f: Vec<&str> = l.split_whitespace().collect();
684        if f.len() >= 7 && f[6] == inode {
685            i32::from_str_radix(f[4], 16).ok()
686        } else {
687            None
688        }
689    })?;
690    let info = std::fs::read_to_string(format!("/proc/{tid}/fdinfo/{fd}")).ok()?;
691    let flags = info
692        .lines()
693        .find_map(|l| l.strip_prefix("flags:"))
694        .and_then(|v| i32::from_str_radix(v.trim(), 8).ok())?;
695    Some((kind, flags))
696}
697
698/// `inotify_add_watch`: the watch goes on the tracee's own descriptor, so the
699/// path is rewritten to a transient alias, which the tracee then walks.
700fn do_inotify_add_watch(r: &Req) -> Result<Outcome> {
701    const IN_DONT_FOLLOW: u64 = 0x0200_0000;
702    let path = r.path(1)?;
703    let dont_follow = r.arg(2) & IN_DONT_FOLLOW != 0;
704    let resolved = r.translate(libc::AT_FDCWD, &path, !dont_follow)?;
705    if resolved.host == path {
706        return Ok(Outcome::Continue);
707    }
708    let form = if dont_follow
709        && resolved
710            .mode_nofollow()
711            .is_ok_and(|m| m & libc::S_IFMT == libc::S_IFLNK)
712    {
713        Form::Dir
714    } else {
715        Form::File
716    };
717    let mut orig = path.clone();
718    orig.push(0);
719    transient_rewrite(r, r.arg(1), &orig, &resolved.host, form)
720}
721
722/// An `O_PATH` open without ptrace.
723///
724/// Such descriptors cannot be injected, so directories and readable files
725/// are opened read-only instead, which serves the same purposes (`openat`
726/// relative to it, `fstat`, `/proc/self/fd/N`), through the pinned parent.
727/// `O_DIRECTORY` on anything but a directory is refused with `ENOTDIR`
728/// first, as the kernel does: `mv` probes its destination that way and
729/// takes a descriptor for a directory to move into.
730/// Anything else, a socket, a device, an unreadable file or a symlink opened
731/// with `O_NOFOLLOW`, goes through a transient alias so the kernel opens it
732/// in the tracee, which walks the host path itself.
733///
734/// # Arguments
735/// * `r` - The notification.
736/// * `dirfd` - Base of a relative `path`.
737/// * `path` - Path as the tracee passed it.
738/// * `path_addr` - Where that string lives in the tracee.
739/// * `flags` - Host-form open flags, `O_PATH` included.
740pub fn do_open_path(r: &Req, dirfd: i32, path: &[u8], path_addr: u64, flags: i64) -> Result<Outcome> {
741    let nofollow = flags & libc::O_NOFOLLOW as i64 != 0;
742    let resolved = r.translate(dirfd, path, !nofollow)?;
743    if r.ctx.trap_mode() {
744        return rewrite_staged(r, path_addr, &resolved.host);
745    }
746    let cloexec = flags & libc::O_CLOEXEC as i64 != 0;
747    let kind = resolved.mode_nofollow()? & libc::S_IFMT;
748    if flags & libc::O_DIRECTORY as i64 != 0 && kind != libc::S_IFDIR {
749        return Err(libc::ENOTDIR);
750    }
751    let (dfd, c, nofollow) = resolved.at(true)?;
752    let base = libc::O_RDONLY | libc::O_CLOEXEC | if nofollow { libc::O_NOFOLLOW } else { 0 };
753    let direct = match kind {
754        libc::S_IFDIR => Some(base | libc::O_DIRECTORY),
755        libc::S_IFREG => Some(base),
756        _ => None,
757    };
758    if let Some(open_flags) = direct {
759        r.valid()?;
760        let fd = unsafe { libc::openat(dfd, c.as_ptr(), open_flags) };
761        if fd >= 0 {
762            return Ok(Outcome::Fd { fd, cloexec });
763        }
764        if errno() != libc::EACCES {
765            return Err(errno());
766        }
767    }
768    if dirfd != libc::AT_FDCWD && path.first() != Some(&b'/') {
769        if kernel_walks_alike(r.pid, dirfd, path, &resolved.host) {
770            return Ok(Outcome::Continue);
771        }
772        return Err(libc::EACCES);
773    }
774    let form = if nofollow { Form::Dir } else { Form::File };
775    let mut orig = path.to_vec();
776    orig.push(0);
777    transient_rewrite(r, path_addr, &orig, &resolved.host, form)
778}
779
780/// Whether the kernel, walking `path` from the tracee's `dirfd` by itself,
781/// reaches `host`.
782///
783/// The descriptor is a host directory already, so a path that joins onto it
784/// with nothing to translate on the way (no symlink among its components,
785/// which canonicalization would have resolved) needs no rewrite.
786///
787/// # Arguments
788/// * `pid` - Tracee thread id.
789/// * `dirfd` - A directory descriptor of the tracee.
790/// * `path` - Path relative to it, as the tracee passed it.
791/// * `host` - What translation resolved it to.
792fn kernel_walks_alike(pid: u32, dirfd: i32, path: &[u8], host: &[u8]) -> bool {
793    let Ok(dir) = read_link(format!("/proc/{pid}/fd/{dirfd}").as_bytes()) else {
794        return false;
795    };
796    let mut joined = strip_deleted(dir);
797    if joined.first() != Some(&b'/') {
798        return false;
799    }
800    if joined != b"/" {
801        joined.push(b'/');
802    }
803    joined.extend_from_slice(path);
804    joined == host
805}
806
807/// `clone`/`fork`/`vfork`/`clone3`: refuse a new namespace, snapshot the
808/// parent for the child without ptrace, then continue.
809///
810/// `clone3` arrives here in both modes, since its flags live in memory where
811/// the filter cannot see them.
812///
813/// # Arguments
814/// * `r` - The notification.
815/// * `nr` - Canonical syscall number.
816pub fn do_clone(r: &Req, nr: i64) -> Result<Outcome> {
817    let flags = match nr {
818        CLONE => r.arg(0),
819        CLONE3 => {
820            let raw = r.read(r.arg(0), 8)?;
821            u64::from_ne_bytes(raw.try_into().unwrap())
822        }
823        VFORK => CLONE_VFORK,
824        _ => 0,
825    };
826    if flags & crate::syscall::CLONE_NEWNS_FLAGS != 0 {
827        return Err(libc::EPERM);
828    }
829    if r.ctx.no_ptrace && flags & CLONE_THREAD == 0 {
830        r.ctx.procs.forked(r.pid as i32, flags & CLONE_VFORK != 0);
831    }
832    Ok(Outcome::Continue)
833}
834
835/// How a transient alias stands for its target.
836#[derive(Clone, Copy, PartialEq, Eq)]
837enum Form {
838    /// The alias is a symlink to the target; the rewritten path is the alias name.
839    File,
840    /// The alias is a symlink to the target's parent directory; the rewritten
841    /// path is `<alias>/<basename>`, for syscalls that must not follow the
842    /// final component.  When that does not fit, a hard link to the target
843    /// stands in, see [`place_alias`].
844    Dir,
845}
846
847/// A transient alias in place: the bytes that replace the tracee's string,
848/// and the hard link to remove once the syscall has consumed them.
849struct Placed {
850    repl: Vec<u8>,
851    staged: Option<PathBuf>,
852}
853
854/// Rewrites a path string in the tracee to a transient alias, continues the
855/// syscall, and puts the string back once the syscall has consumed it.
856///
857/// # Arguments
858/// * `r` - The notification.
859/// * `addr` - Address of the string in the tracee.
860/// * `orig` - The bytes at `addr` that may be overwritten (the string and its NUL).
861/// * `host` - Host path the alias stands for.
862/// * `form` - Alias form.
863///
864/// # Returns
865/// `Outcome::Replied`: the notification was answered here.
866///
867/// # Errors
868/// `ENAMETOOLONG` when no alias fits, or the errno of creating it.
869fn transient_rewrite(r: &Req, addr: u64, orig: &[u8], host: &[u8], form: Form) -> Result<Outcome> {
870    if r.ctx.trap_mode() {
871        return rewrite_staged(r, addr, host);
872    }
873    let dir = shim::dir_bytes().ok_or(libc::ENOENT)?;
874    let name = acquire_alias().ok_or(libc::EAGAIN)?;
875    let placed = match place_alias(&dir, name, orig.len(), host, form, &staging_candidates(r.ctx)) {
876        Ok(placed) => placed,
877        Err(e) => {
878            release_alias(name);
879            return Err(e);
880        }
881    };
882    let fail = |e: i32| {
883        unstage(&placed);
884        release_alias(name);
885        e
886    };
887    let mut written = placed.repl.clone();
888    written.push(0);
889    r.valid().map_err(fail)?;
890    let cpu_before = cpu_time(r.pid);
891    r.ctx.mem.write_force(r.pid, addr, &written).map_err(fail)?;
892    note!(
893        r.ctx,
894        2,
895        "pid {} rewrote {} -> {} for {}",
896        r.pid,
897        String::from_utf8_lossy(&orig[..orig.len().saturating_sub(1)]),
898        String::from_utf8_lossy(&placed.repl),
899        String::from_utf8_lossy(host)
900    );
901    reply_continue(r.listener, r.id);
902    wait_syscall_consumed(r, cpu_before);
903    restore_if_intact(r, r.pid, addr, orig, &written);
904    unstage(&placed);
905    release_alias(name);
906    Ok(Outcome::Replied)
907}
908
909/// Places the alias `name` for `host` so that its path fits in `room` bytes,
910/// the tracee's string and its NUL.
911///
912/// A symlink in the alias directory comes first: to the target itself, or
913/// to its parent with `<name>/<basename>` for a final component that must
914/// be reached without following it.  When that does not fit, the final
915/// component is reached through a hard link to it, which is the same inode:
916/// as `<name>` in the alias directory when that is on the target's mount,
917/// else as `<name>/<name>` through a staging directory on that mount.
918///
919/// # Arguments
920/// * `dir` - The alias directory.
921/// * `name` - The alias name held by the caller.
922/// * `room` - Bytes available in the tracee for the path and its NUL.
923/// * `host` - Host path the alias stands for.
924/// * `form` - Alias form.
925/// * `candidates` - Where a staging directory may be created, in order.
926///
927/// # Errors
928/// `ENAMETOOLONG` when no placement fits, or the errno of creating the symlink.
929fn place_alias(
930    dir: &[u8],
931    name: u8,
932    room: usize,
933    host: &[u8],
934    form: Form,
935    candidates: &[PathBuf],
936) -> Result<Placed> {
937    let link = Path::new(OsStr::from_bytes(dir)).join(OsStr::from_bytes(&[name]));
938    let _ = std::fs::remove_file(&link);
939    let (target, repl): (Vec<u8>, Vec<u8>) = match form {
940        Form::File => (host.to_vec(), vec![name]),
941        Form::Dir => {
942            let cut = host.iter().rposition(|&b| b == b'/').unwrap_or(0);
943            let parent = if cut == 0 {
944                b"/".to_vec()
945            } else {
946                host[..cut].to_vec()
947            };
948            let mut p = vec![name, b'/'];
949            p.extend_from_slice(&host[cut + 1..]);
950            (parent, p)
951        }
952    };
953    if repl.len() < room {
954        std::os::unix::fs::symlink(OsStr::from_bytes(&target), &link).map_err(io_errno)?;
955        return Ok(Placed { repl, staged: None });
956    }
957    if form != Form::Dir || room < 2 {
958        return Err(libc::ENAMETOOLONG);
959    }
960    if link_nofollow(host, &link).is_ok() {
961        return Ok(Placed {
962            repl: vec![name],
963            staged: Some(link),
964        });
965    }
966    if room < 4 {
967        return Err(libc::ENAMETOOLONG);
968    }
969    let staged = stage_hard_link(host, name, candidates).ok_or(libc::ENAMETOOLONG)?;
970    let stage_dir = staged.parent().ok_or(libc::ENAMETOOLONG)?;
971    if let Err(e) = std::os::unix::fs::symlink(stage_dir, &link) {
972        let _ = std::fs::remove_file(&staged);
973        return Err(io_errno(e));
974    }
975    Ok(Placed {
976        repl: vec![name, b'/', name],
977        staged: Some(staged),
978    })
979}
980
981/// Removes the hard link an alias was placed through, once the syscall has
982/// consumed the rewritten path; a descriptor opened through it keeps the inode.
983///
984/// # Arguments
985/// * `placed` - The alias.
986fn unstage(placed: &Placed) {
987    if let Some(p) = &placed.staged {
988        let _ = std::fs::remove_file(p);
989    }
990}
991
992/// The errno of an I/O error, `EIO` for one without.
993///
994/// # Arguments
995/// * `e` - The error.
996fn io_errno(e: std::io::Error) -> i32 {
997    e.raw_os_error().unwrap_or(libc::EIO)
998}
999
1000/// `linkat` without `AT_SYMLINK_FOLLOW`: a hard link to `host` itself, a
1001/// symlink included.
1002///
1003/// # Arguments
1004/// * `host` - Existing path.
1005/// * `at` - Path of the new link.
1006///
1007/// # Errors
1008/// The errno of `linkat`.
1009fn link_nofollow(host: &[u8], at: &Path) -> Result<()> {
1010    let src = CString::new(host.to_vec()).map_err(|_| libc::EINVAL)?;
1011    let dst = CString::new(at.as_os_str().as_bytes().to_vec()).map_err(|_| libc::EINVAL)?;
1012    if unsafe { libc::linkat(libc::AT_FDCWD, src.as_ptr(), libc::AT_FDCWD, dst.as_ptr(), 0) } < 0 {
1013        return Err(errno());
1014    }
1015    Ok(())
1016}
1017
1018/// Makes a hard link named `name` to `host` in a staging directory on its
1019/// mount, creating one under the first of `candidates` that allows it.
1020///
1021/// # Arguments
1022/// * `host` - Existing path.
1023/// * `name` - The alias name held by the caller.
1024/// * `candidates` - Where a staging directory may be created, in order.
1025///
1026/// # Returns
1027/// The hard link's path, or `None` when no directory on that mount can hold
1028/// one: a read-only mount, no writable candidate on it, or a symlink of
1029/// another owner under `fs.protected_hardlinks`.
1030fn stage_hard_link(host: &[u8], name: u8, candidates: &[PathBuf]) -> Option<PathBuf> {
1031    let link_in = |d: &Path| {
1032        let p = d.join(OsStr::from_bytes(&[name]));
1033        let _ = std::fs::remove_file(&p);
1034        link_nofollow(host, &p).ok().map(|()| p)
1035    };
1036    let mut staging = STAGING.lock().unwrap();
1037    if let Some(p) = staging.iter().find_map(|d| link_in(d)) {
1038        return Some(p);
1039    }
1040    for base in candidates {
1041        let d = base.join(format!("proot-next-{}-stage", std::process::id()));
1042        if std::fs::DirBuilder::new().mode(0o700).create(&d).is_err() {
1043            continue;
1044        }
1045        if let Some(p) = link_in(&d) {
1046            staging.push(d);
1047            return Some(p);
1048        }
1049        let _ = std::fs::remove_dir(&d);
1050    }
1051    None
1052}
1053
1054/// Where a staging directory may be created, in order: the user's cache
1055/// directory, beside the rootfs, and the temporary directory.
1056///
1057/// # Arguments
1058/// * `ctx` - Supervisor state.
1059fn staging_candidates(ctx: &Ctx) -> Vec<PathBuf> {
1060    let mut v = Vec::new();
1061    if let Some(c) = std::env::var_os("XDG_CACHE_HOME").filter(|c| !c.is_empty()) {
1062        v.push(PathBuf::from(c));
1063    } else if let Some(h) = std::env::var_os("HOME") {
1064        v.push(PathBuf::from(h).join(".cache"));
1065    }
1066    if let Some(parent) = Path::new(OsStr::from_bytes(&ctx.config.root)).parent() {
1067        v.push(parent.to_path_buf());
1068    }
1069    v.push(std::env::temp_dir());
1070    v
1071}
1072
1073/// Removes the staging directories, at exit.
1074pub fn remove_staging() {
1075    for d in STAGING.lock().unwrap().drain(..) {
1076        let _ = std::fs::remove_dir_all(d);
1077    }
1078}
1079
1080/// Whether `host` lies in the alias directory or a staging directory.
1081///
1082/// # Arguments
1083/// * `host` - A host path.
1084fn under_alias(host: &[u8]) -> bool {
1085    let within = |d: &[u8]| host.len() > d.len() && host.starts_with(d) && host[d.len()] == b'/';
1086    shim::dir_bytes().is_some_and(|d| within(&d))
1087        || STAGING
1088            .lock()
1089            .unwrap()
1090            .iter()
1091            .any(|d| within(d.as_os_str().as_bytes()))
1092}
1093
1094/// `readlinkat` with an empty path on a descriptor opened through a hard
1095/// link alias: the answer comes from the descriptor's own inode, reopened
1096/// through `/proc`, since the link it was opened by is gone by then and its
1097/// name may stand for something else.
1098///
1099/// # Arguments
1100/// * `r` - The notification.
1101/// * `dirfd` - The descriptor.
1102/// * `path` - The path argument.
1103///
1104/// # Returns
1105/// `None` when the call is not such a read, which the caller serves by path.
1106pub fn readlink_alias_fd(r: &Req, dirfd: i32, path: &[u8]) -> Option<Result<Vec<u8>>> {
1107    if !r.ctx.no_ptrace || !path.is_empty() || dirfd < 0 {
1108        return None;
1109    }
1110    let proc_link = format!("/proc/{}/fd/{}", r.pid, dirfd);
1111    let host = strip_deleted(read_link(proc_link.as_bytes()).ok()?);
1112    if !under_alias(&host) {
1113        return None;
1114    }
1115    Some(readlink_reopened(&proc_link))
1116}
1117
1118/// Reads the target of the symlink a `/proc/<pid>/fd/N` link leads to, by
1119/// reopening it `O_PATH` here, which lands on that inode whether or not its
1120/// name still exists.
1121///
1122/// # Arguments
1123/// * `proc_link` - The `/proc` link.
1124///
1125/// # Errors
1126/// The errno of the reopen, or `ENOENT` where the inode is no symlink, as
1127/// the kernel answers an empty path on such a descriptor.
1128fn readlink_reopened(proc_link: &str) -> Result<Vec<u8>> {
1129    let c = CString::new(proc_link).map_err(|_| libc::EINVAL)?;
1130    let fd = unsafe { libc::open(c.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) };
1131    if fd < 0 {
1132        return Err(errno());
1133    }
1134    let mut buf = vec![0u8; libc::PATH_MAX as usize];
1135    let n = unsafe { libc::readlinkat(fd, c"".as_ptr(), buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
1136    let e = errno();
1137    unsafe { libc::close(fd) };
1138    if n < 0 {
1139        return Err(if e == libc::EINVAL { libc::ENOENT } else { e });
1140    }
1141    buf.truncate(n as usize);
1142    Ok(buf)
1143}
1144
1145/// Replaces a path the guest stub staged with its host path and lets the
1146/// syscall continue on it.
1147///
1148/// # Arguments
1149/// * `r` - The notification.
1150/// * `addr` - Address of the staged path, a [`layout::PATH_AREA`] of the mailbox.
1151/// * `host` - The host path.
1152///
1153/// # Errors
1154/// `ENAMETOOLONG` for a host path that does not fit, `EFAULT` for an
1155/// address outside the mailbox.
1156fn rewrite_staged(r: &Req, addr: u64, host: &[u8]) -> Result<Outcome> {
1157    if host.len() >= layout::PATH_AREA {
1158        return Err(libc::ENAMETOOLONG);
1159    }
1160    let mut bytes = host.to_vec();
1161    bytes.push(0);
1162    r.valid()?;
1163    r.ctx.mem.write(r.pid, addr, &bytes)?;
1164    note!(
1165        r.ctx,
1166        2,
1167        "pid {} rewrote the staged path to {}",
1168        r.pid,
1169        String::from_utf8_lossy(host)
1170    );
1171    Ok(Outcome::Continue)
1172}
1173
1174/// Puts `orig` back at `addr` in process `pid` if the memory still holds
1175/// exactly the rewrite, i.e. `written` followed by the rest of `orig`.
1176///
1177/// A buffer that was freed or reused since the rewrite no longer matches
1178/// and is left alone: writing into it would corrupt whatever lives there
1179/// now (a reused stack frame, an allocator's free list).
1180///
1181/// # Arguments
1182/// * `r` - The notification, for logging.
1183/// * `pid` - Process whose memory holds the string.
1184/// * `addr` - Address of the string.
1185/// * `orig` - The original bytes.
1186/// * `written` - The bytes written over the front of them.
1187fn restore_if_intact(r: &Req, pid: u32, addr: u64, orig: &[u8], written: &[u8]) {
1188    let mut cur = vec![0u8; orig.len()];
1189    let intact = r.ctx.mem.read(pid, addr, &mut cur) == Ok(orig.len())
1190        && cur[..written.len()] == *written
1191        && cur[written.len()..] == orig[written.len()..];
1192    if intact {
1193        let _ = r.ctx.mem.write_force(pid, addr, orig);
1194    } else {
1195        note!(
1196            r.ctx,
1197            2,
1198            "pid {pid} rewritten string at {addr:#x} not restored: memory reused"
1199        );
1200    }
1201}
1202
1203/// Waits until the continued syscall has consumed its rewritten argument.
1204///
1205/// The kernel copies a path in, and walks it, as the first thing it does
1206/// after the task resumes, so the string may be restored once the task has
1207/// spent [`SETTLED_CPU`] of CPU time since the reply (`/proc/<tid>/schedstat`),
1208/// or sooner when it is seen blocked in a different syscall, or outside any
1209/// syscall, or gone.  `/proc/<tid>/syscall` alone cannot tell: it reads
1210/// `running` for a task on a CPU whether it is about to copy the argument or
1211/// long past it.  A syscall that blocks inside itself has consumed the
1212/// argument too, so the wait gives up after [`RESTORE_TIMEOUT`].  Without
1213/// scheduler statistics the wait is a fixed two milliseconds.
1214///
1215/// # Arguments
1216/// * `r` - The notification, for the thread id and the syscall to look for.
1217/// * `cpu_before` - The task's run time before the reply, from [`cpu_time`].
1218fn wait_syscall_consumed(r: &Req, cpu_before: Option<u64>) {
1219    let path = format!("/proc/{}/syscall", r.pid);
1220    let start = Instant::now();
1221    let mut polls = 0u32;
1222    while start.elapsed() < RESTORE_TIMEOUT {
1223        match (cpu_before, cpu_time(r.pid)) {
1224            (Some(before), Some(now)) if now.saturating_sub(before) >= SETTLED_CPU => return,
1225            (Some(_), None) => return,
1226            (None, _) => {
1227                std::thread::sleep(Duration::from_millis(2));
1228                return;
1229            }
1230            _ => {}
1231        }
1232        let Ok(s) = std::fs::read_to_string(&path) else {
1233            return;
1234        };
1235        let mut it = s.split_whitespace();
1236        let first = it.next().unwrap_or("");
1237        if first != "running" {
1238            let in_it = first.parse::<i64>().ok() == Some(r.raw_nr as i64)
1239                && (0..6).all(|i| {
1240                    it.next()
1241                        .and_then(|a| u64::from_str_radix(a.trim_start_matches("0x"), 16).ok())
1242                        == Some(r.args[i])
1243                });
1244            if !in_it {
1245                return;
1246            }
1247        }
1248        polls += 1;
1249        if polls < 50 {
1250            std::thread::yield_now();
1251        } else {
1252            std::thread::sleep(Duration::from_micros(200.min(50 * polls as u64)));
1253        }
1254    }
1255}
1256
1257/// CPU time thread `tid` has run, in nanoseconds, from `/proc/<tid>/schedstat`.
1258///
1259/// # Arguments
1260/// * `tid` - Thread id.
1261fn cpu_time(tid: u32) -> Option<u64> {
1262    let s = std::fs::read_to_string(format!("/proc/{tid}/schedstat")).ok()?;
1263    s.split_whitespace().next()?.parse().ok()
1264}
1265
1266/// Takes a free one-character alias name, the least recently handed out.
1267fn acquire_alias() -> Option<u8> {
1268    let mut g = ALIASES.lock().unwrap();
1269    let (used, next) = &mut *g;
1270    let n = ALIAS_CHARS.len();
1271    let i = (0..n).map(|k| (*next + k) % n).find(|&i| !used[i])?;
1272    used[i] = true;
1273    *next = (i + 1) % n;
1274    Some(ALIAS_CHARS[i])
1275}
1276
1277/// Returns an alias name to the pool.  The symlink stays until the name
1278/// comes round again.
1279fn release_alias(name: u8) {
1280    if let Some(i) = ALIAS_CHARS.iter().position(|&c| c == name) {
1281        ALIASES.lock().unwrap().0[i] = false;
1282    }
1283}
1284
1285/// Whether ptrace can be used from here.
1286///
1287/// `PTRACE_PEEKUSER` on a process that is not a tracee fails with `ESRCH`
1288/// once the kernel reaches the request; a seccomp policy that filters the
1289/// syscall answers with its own errno before that.
1290pub fn ptrace_usable() -> bool {
1291    let rc = unsafe { libc::ptrace(libc::PTRACE_PEEKUSER, std::process::id() as i32, 0usize, 0usize) };
1292    rc < 0 && errno() == libc::ESRCH
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297    use super::*;
1298
1299    #[test]
1300    fn alias_names_are_short_and_rotate() {
1301        let a = acquire_alias().unwrap();
1302        let b = acquire_alias().unwrap();
1303        assert_ne!(a, b);
1304        assert!(a != b'e' && a != b'r' && b != b'e' && b != b'r');
1305        release_alias(a);
1306        release_alias(b);
1307        let c = acquire_alias().unwrap();
1308        assert_ne!(c, a);
1309        assert_ne!(c, b);
1310        release_alias(c);
1311    }
1312
1313    #[test]
1314    fn describes_own_unix_socket() {
1315        let fd = unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_DGRAM | libc::SOCK_NONBLOCK, 0) };
1316        assert!(fd >= 0);
1317        let (kind, flags) = socket_description(std::process::id(), fd).expect("own socket is described");
1318        assert_eq!(kind, libc::SOCK_DGRAM);
1319        assert!(flags & libc::O_NONBLOCK != 0);
1320        assert!(flags & libc::O_CLOEXEC == 0);
1321        unsafe { libc::close(fd) };
1322        assert!(socket_description(std::process::id(), fd).is_none());
1323    }
1324
1325    #[test]
1326    fn ptrace_probe_does_not_panic() {
1327        let _ = ptrace_usable();
1328    }
1329
1330    fn scratch(name: &str) -> PathBuf {
1331        let d = std::env::temp_dir().join(format!("proot-direct-{name}-{}", std::process::id()));
1332        let _ = std::fs::remove_dir_all(&d);
1333        std::fs::create_dir_all(&d).unwrap();
1334        d
1335    }
1336
1337    #[test]
1338    fn hard_link_alias_is_the_symlink_itself() {
1339        use std::os::unix::fs::MetadataExt;
1340        let d = scratch("hardlink");
1341        let link = d.join("link");
1342        std::os::unix::fs::symlink("target", &link).unwrap();
1343        let alias_dir = d.join("alias");
1344        std::fs::create_dir(&alias_dir).unwrap();
1345        let (dir, host) = (alias_dir.as_os_str().as_bytes(), link.as_os_str().as_bytes());
1346        let name = acquire_alias().unwrap();
1347        let want = std::fs::symlink_metadata(&link).unwrap();
1348        let placed = place_alias(dir, name, b"link\0".len(), host, Form::Dir, &[]).unwrap();
1349        assert_eq!(placed.repl, vec![name]);
1350        let got = std::fs::symlink_metadata(alias_dir.join(OsStr::from_bytes(&[name]))).unwrap();
1351        assert!(got.file_type().is_symlink());
1352        assert_eq!((got.dev(), got.ino()), (want.dev(), want.ino()));
1353        assert_eq!(std::fs::symlink_metadata(&link).unwrap().nlink(), 2);
1354        unstage(&placed);
1355        assert_eq!(std::fs::symlink_metadata(&link).unwrap().nlink(), 1);
1356        assert_eq!(
1357            place_alias(dir, name, b"\0".len(), host, Form::Dir, &[]).err(),
1358            Some(libc::ENAMETOOLONG)
1359        );
1360        let fits = place_alias(dir, name, b"./link\0".len(), host, Form::Dir, &[]).unwrap();
1361        assert!(fits.staged.is_none());
1362        assert_eq!(fits.repl, [&[name, b'/'][..], b"link"].concat());
1363        release_alias(name);
1364        std::fs::remove_dir_all(d).unwrap();
1365    }
1366
1367    #[test]
1368    fn staging_directory_holds_a_link_on_the_symlinks_mount() {
1369        use std::os::unix::fs::MetadataExt;
1370        let d = scratch("staging");
1371        let link = d.join("link");
1372        std::os::unix::fs::symlink("target", &link).unwrap();
1373        let host = link.as_os_str().as_bytes();
1374        let name = acquire_alias().unwrap();
1375        let staged =
1376            stage_hard_link(host, name, &[d.join("missing"), d.clone()]).expect("a staging directory");
1377        let stage_dir = staged.parent().unwrap().to_path_buf();
1378        assert_eq!(stage_dir.parent(), Some(d.as_path()));
1379        let (want, got) = (
1380            std::fs::symlink_metadata(&link).unwrap(),
1381            std::fs::symlink_metadata(&staged).unwrap(),
1382        );
1383        assert_eq!((got.dev(), got.ino()), (want.dev(), want.ino()));
1384        assert!(under_alias(staged.as_os_str().as_bytes()));
1385        assert!(!under_alias(host));
1386        assert_eq!(stage_hard_link(host, name, &[]), Some(staged.clone()));
1387        remove_staging();
1388        assert!(!stage_dir.exists());
1389        release_alias(name);
1390        std::fs::remove_dir_all(d).unwrap();
1391    }
1392
1393    #[test]
1394    fn kernel_walks_a_single_component_from_a_directory_descriptor() {
1395        let d = scratch("walk");
1396        std::os::unix::fs::symlink("target", d.join("link")).unwrap();
1397        let c = CString::new(d.as_os_str().as_bytes()).unwrap();
1398        let fd = unsafe { libc::open(c.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC) };
1399        assert!(fd >= 0);
1400        let host = d.join("link");
1401        let host = host.as_os_str().as_bytes();
1402        let pid = std::process::id();
1403        assert!(kernel_walks_alike(pid, fd, b"link", host));
1404        assert!(!kernel_walks_alike(pid, fd, b"./link", host));
1405        assert!(!kernel_walks_alike(pid, fd, b"link", b"/elsewhere/link"));
1406        assert!(!kernel_walks_alike(pid, -1, b"link", host));
1407        unsafe { libc::close(fd) };
1408        std::fs::remove_dir_all(d).unwrap();
1409    }
1410}