Skip to main content

proot/syscall/
notify.rs

1//! Handlers for syscalls answered through seccomp user notification.
2//!
3//! Each handler reads its path arguments out of the tracee, translates them
4//! into the guest filesystem view, performs the real syscall in the
5//! supervisor, and answers with a value, an error, or an injected
6//! descriptor.  The kernel never sees an untranslated guest path for the
7//! syscalls routed here.
8//!
9//! The syscall is made on what the walk resolved, not on a host path string
10//! the kernel would walk again: the `*at` form relative to the pinned parent
11//! directory with `O_NOFOLLOW`/`AT_SYMLINK_NOFOLLOW`, since the walk already
12//! followed a final symlink when asked to.  A symlink found at that point was
13//! swapped in by the guest after the walk; the handler then walks again, a
14//! few times, so the link is resolved inside the guest namespace, and gives
15//! up with `ELOOP`.  Syscalls without a `dirfd` form act on the object's
16//! `/proc/self/fd/<n>`, which the kernel resolves to the open inode.
17//!
18//! Every notification is answered exactly once.  After tracee memory has been
19//! read and before anything irreversible happens, the notification id is
20//! re-validated; a stale id (the tracee died or was interrupted by a signal)
21//! is dropped silently.
22
23use crate::abi::{Abi, Variant};
24use crate::fs::Fs;
25use crate::fs::canon::{RETRY, Resolved, fd_path, readlinkat};
26use crate::fs::translate::{detranslate_path, guest_cwd, guest_dirfd_path, read_link, translate};
27use crate::note;
28use crate::seccomp::notify::{reply, reply_continue, reply_fd, still_valid};
29use crate::state::{Ctx, View};
30use crate::sys::{Errno, Result, SeccompNotif, check};
31use crate::syscall::direct;
32use crate::syscall::nr::*;
33use std::cell::OnceCell;
34use std::ffi::CString;
35use std::io::Read;
36use std::os::fd::FromRawFd;
37use std::os::unix::ffi::OsStrExt;
38use std::sync::Arc;
39
40/// Sentinel errno meaning "the notification is stale, do not answer".
41const DROPPED: Errno = -1;
42
43/// Walks of one call after a swapped-in symlink was found where the walk
44/// had followed none, before the call fails with `ELOOP`.
45const RETRIES: u32 = 3;
46
47/// Largest extended attribute value or list we shuttle through.
48const XATTR_MAX: usize = 65536;
49
50/// `open(2)` flags that imply the caller intends to modify the file.
51///
52/// `O_TMPFILE` is deliberately absent: its value includes the `O_DIRECTORY`
53/// bit, so testing for it would flag every `opendir`, and it always comes
54/// with `O_WRONLY` or `O_RDWR` anyway.
55const WRITE_INTENT: i64 =
56    (libc::O_WRONLY | libc::O_RDWR | libc::O_CREAT | libc::O_TRUNC | libc::O_APPEND) as i64;
57
58/// `fchmodat2(2)` is 452 on every architecture; the libc crate lacks it for aarch64.
59const SYS_FCHMODAT2_HOST: libc::c_long = 452;
60
61/// `IN_DONT_FOLLOW` for `inotify_add_watch(2)`.
62const IN_DONT_FOLLOW: u64 = 0x0200_0000;
63
64/// How a handler wants the notification answered.
65pub(crate) enum Outcome {
66    /// Plain return value or errno.
67    Value(Result<i64>),
68    /// Install this supervisor descriptor into the tracee and return its number.
69    Fd { fd: i32, cloexec: bool },
70    /// Let the kernel run the syscall untouched.
71    Continue,
72    /// The handler answered the notification itself.
73    Replied,
74}
75
76/// One notification plus the shared state needed to serve it.
77pub(crate) struct Req<'a> {
78    pub ctx: &'a Ctx,
79    pub listener: i32,
80    pub id: u64,
81    pub pid: u32,
82    pub args: [u64; 6],
83    /// Syscall number as the tracee issued it, in its own ABI.
84    pub raw_nr: i32,
85    /// ABI the request came through; 32-bit pointers are already zero-extended.
86    pub abi: Abi,
87    /// How an i386 request differs from the x86_64 number it was mapped to.
88    pub variant: Variant,
89    /// The caller's view, looked up once per request.
90    view: OnceCell<Arc<View>>,
91}
92
93impl<'a> Req<'a> {
94    pub fn arg(&self, i: usize) -> u64 {
95        self.args[i]
96    }
97
98    /// The view the calling process lives in.
99    pub fn view(&self) -> &View {
100        self.view.get_or_init(|| self.ctx.view(self.pid as i32))
101    }
102
103    /// The calling process's filesystem view.
104    pub fn fs(&self) -> &Fs {
105        &self.view().fs
106    }
107
108    fn arg_i64(&self, i: usize) -> i64 {
109        self.args[i] as i64
110    }
111
112    fn dirfd(&self, i: usize) -> i32 {
113        self.args[i] as i32
114    }
115
116    /// Thread-group id of the calling thread.
117    pub fn tgid(&self) -> i32 {
118        self.ctx.tgid(self.pid as i32)
119    }
120
121    /// Reads the NUL-terminated path in argument `i`, rewriting `/proc/self`
122    /// and `/proc/thread-self` to the tracee's own entries since the
123    /// supervisor would otherwise see its own.
124    pub fn path(&self, i: usize) -> Result<Vec<u8>> {
125        let raw = self.ctx.mem.read_cstr(self.pid, self.arg(i))?;
126        Ok(rewrite_proc_self(&raw, self.pid))
127    }
128
129    /// Translates `path` relative to `dirfd` into the object to act on.
130    pub fn translate(&self, dirfd: i32, path: &[u8], deref_final: bool) -> Result<Resolved> {
131        let resolved = translate(self.fs(), self.pid, dirfd, path, deref_final)?;
132        note!(
133            self.ctx,
134            2,
135            "pid {} {} -> {}",
136            self.pid,
137            String::from_utf8_lossy(path),
138            String::from_utf8_lossy(&resolved.host)
139        );
140        Ok(resolved)
141    }
142
143    /// Translates like [`Req::translate`], except that an empty path names `dirfd` itself.
144    ///
145    /// `AT_EMPTY_PATH` asks for that, and `readlinkat` always takes it; for
146    /// `AT_FDCWD` it is the tracee's working directory.
147    ///
148    /// # Arguments
149    /// * `dirfd` - Base descriptor for a relative path, or `libc::AT_FDCWD`.
150    /// * `path` - Path bytes as read from the tracee, without NUL.
151    /// * `deref_final` - Follow a symlink in the final component.
152    ///
153    /// # Errors
154    /// As [`Req::translate`], or the errno of reading the descriptor's path.
155    pub fn translate_or_dirfd(&self, dirfd: i32, path: &[u8], deref_final: bool) -> Result<Resolved> {
156        if !path.is_empty() {
157            return self.translate(dirfd, path, deref_final);
158        }
159        let guest = guest_dirfd_path(self.fs(), self.pid, dirfd)?;
160        self.translate(libc::AT_FDCWD, &guest, deref_final)
161    }
162
163    /// Re-validates the notification; `Err(DROPPED)` when the tracee is gone.
164    pub fn valid(&self) -> Result<()> {
165        if still_valid(self.listener, self.id) {
166            Ok(())
167        } else {
168            Err(DROPPED)
169        }
170    }
171
172    /// Refuses with `EROFS` when the resolved path lies under a read-only binding.
173    pub fn check_writable(&self, resolved: &Resolved) -> Result<()> {
174        match self.fs().binding_for_guest(&resolved.guest) {
175            Some(b) if b.read_only => Err(libc::EROFS),
176            _ => Ok(()),
177        }
178    }
179
180    pub fn write(&self, addr: u64, data: &[u8]) -> Result<()> {
181        self.ctx.mem.write(self.pid, addr, data)
182    }
183
184    pub fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>> {
185        let mut buf = vec![0u8; len];
186        let n = self.ctx.mem.read(self.pid, addr, &mut buf)?;
187        if n != len {
188            return Err(libc::EFAULT);
189        }
190        Ok(buf)
191    }
192}
193
194/// Serves one notification.  Always answers exactly once, unless the
195/// notification turned out to be stale.
196///
197/// # Arguments
198/// * `ctx` - Shared supervisor state.
199/// * `listener` - Listener descriptor to answer on.
200/// * `req` - The notification as received.
201pub fn handle(ctx: &Ctx, listener: i32, req: &SeccompNotif) {
202    let Some(abi) = Abi::from_audit(req.data.arch) else {
203        reply(listener, req.id, Err(libc::ENOSYS));
204        return;
205    };
206    let raw_nr = req.data.nr as i64;
207    let hello = match abi {
208        Abi::X86_64 | Abi::Aarch64 => crate::guest_stub::layout::HELLO64,
209        Abi::I386 | Abi::Arm => crate::guest_stub::layout::HELLO32,
210    };
211    if ctx.trap_mode() && raw_nr == hello && req.data.args[0] == crate::guest_stub::layout::HELLO_MARK {
212        direct::hello(ctx, req.pid);
213        reply(listener, req.id, Ok(0));
214        return;
215    }
216    let (nr, variant) = if ctx.no_ptrace && abi.socketcall_nr() == Some(raw_nr) {
217        (SOCKETCALL, Variant::Same)
218    } else {
219        match abi.to_canonical(raw_nr) {
220            Some(x) => x,
221            None => {
222                // Not a syscall we serve for this ABI: let the kernel run it.
223                reply_continue(listener, req.id);
224                return;
225            }
226        }
227    };
228    let r = Req {
229        ctx,
230        listener,
231        id: req.id,
232        pid: req.pid,
233        args: req.data.args,
234        raw_nr: req.data.nr,
235        abi,
236        variant,
237        view: OnceCell::new(),
238    };
239    match dispatch(&r, nr) {
240        Ok(Outcome::Value(v)) => reply(listener, req.id, v),
241        Ok(Outcome::Fd { fd, cloexec }) => reply_fd(listener, req.id, fd, cloexec),
242        Ok(Outcome::Continue) => reply_continue(listener, req.id),
243        Ok(Outcome::Replied) => {}
244        Err(DROPPED) => {}
245        Err(e) => {
246            note!(
247                ctx,
248                2,
249                "pid {} {}: {}",
250                req.pid,
251                name(nr),
252                crate::sys::strerror(e)
253            );
254            reply(listener, req.id, Err(e));
255        }
256    }
257}
258
259/// Routes a syscall number to its handler.
260fn dispatch(r: &Req, nr: i64) -> Result<Outcome> {
261    if let Some(o) = direct::shim_passthrough(r, nr) {
262        return Ok(o);
263    }
264    if r.ctx.no_ptrace {
265        if nr == SOCKETCALL {
266            return direct::do_socketcall(r);
267        }
268        if let Some(o) = direct::dispatch(r, nr) {
269            return o;
270        }
271    }
272    match nr {
273        OPEN => do_open(
274            r,
275            libc::AT_FDCWD,
276            &r.path(0)?,
277            r.arg(0),
278            r.arg_i64(1),
279            r.arg_i64(2),
280        ),
281        CREAT => {
282            let flags = (libc::O_CREAT | libc::O_WRONLY | libc::O_TRUNC) as i64;
283            do_open(r, libc::AT_FDCWD, &r.path(0)?, r.arg(0), flags, r.arg_i64(1))
284        }
285        OPENAT => do_open(r, r.dirfd(0), &r.path(1)?, r.arg(1), r.arg_i64(2), r.arg_i64(3)),
286        OPENAT2 => do_openat2(r),
287        STAT => do_stat(r, libc::AT_FDCWD, &r.path(0)?, r.arg(1), 0),
288        LSTAT => do_stat(
289            r,
290            libc::AT_FDCWD,
291            &r.path(0)?,
292            r.arg(1),
293            libc::AT_SYMLINK_NOFOLLOW as i64,
294        ),
295        NEWFSTATAT => do_stat(r, r.dirfd(0), &r.path(1)?, r.arg(2), r.arg_i64(3)),
296        STATX => do_statx(r),
297        STATFS => do_statfs(r),
298        ACCESS => do_access(r, libc::AT_FDCWD, &r.path(0)?, r.arg_i64(1), 0),
299        FACCESSAT => do_access(r, r.dirfd(0), &r.path(1)?, r.arg_i64(2), 0),
300        FACCESSAT2 => do_access(r, r.dirfd(0), &r.path(1)?, r.arg_i64(2), r.arg_i64(3)),
301        READLINK => do_readlink(r, libc::AT_FDCWD, &r.path(0)?, r.arg(1), r.arg(2) as usize),
302        READLINKAT => do_readlink(r, r.dirfd(0), &r.path(1)?, r.arg(2), r.arg(3) as usize),
303        GETCWD => do_getcwd(r),
304        UNAME => do_uname(r),
305        // -0: the tracee is root as far as it can tell, with a virtual identity it can drop.
306        // A nested view without -0 inside a launch with it is left to the kernel.
307        GETUID | GETGID | GETEUID | GETEGID | GETRESUID | GETRESGID | SETUID | SETGID | SETREUID
308        | SETREGID | SETRESUID | SETRESGID | SETFSUID | SETFSGID | SETGROUPS | GETGROUPS | CAPSET
309        | FCHOWN
310            if !r.view().fake_root =>
311        {
312            Ok(Outcome::Continue)
313        }
314        GETUID | GETGID | GETEUID | GETEGID | GETRESUID | GETRESGID | SETUID | SETGID | SETREUID
315        | SETREGID | SETRESUID | SETRESGID | SETFSUID | SETFSGID => do_identity(r, nr),
316        SETGROUPS => do_setgroups(r),
317        GETGROUPS => do_getgroups(r),
318        CAPSET => Ok(Outcome::Value(Ok(0))),
319        FCHOWN => do_fchown(r),
320        MKDIR => do_mkdir(r, libc::AT_FDCWD, &r.path(0)?, r.arg_i64(1)),
321        MKDIRAT => do_mkdir(r, r.dirfd(0), &r.path(1)?, r.arg_i64(2)),
322        RMDIR => do_unlink(r, libc::AT_FDCWD, &r.path(0)?, libc::AT_REMOVEDIR as i64),
323        UNLINK => do_unlink(r, libc::AT_FDCWD, &r.path(0)?, 0),
324        UNLINKAT => do_unlink(r, r.dirfd(0), &r.path(1)?, r.arg_i64(2)),
325        RENAME => do_rename(r, libc::AT_FDCWD, &r.path(0)?, libc::AT_FDCWD, &r.path(1)?, 0),
326        RENAMEAT => do_rename(r, r.dirfd(0), &r.path(1)?, r.dirfd(2), &r.path(3)?, 0),
327        RENAMEAT2 => do_rename(r, r.dirfd(0), &r.path(1)?, r.dirfd(2), &r.path(3)?, r.arg_i64(4)),
328        LINK => do_link(r, libc::AT_FDCWD, &r.path(0)?, libc::AT_FDCWD, &r.path(1)?, 0),
329        LINKAT => do_link(r, r.dirfd(0), &r.path(1)?, r.dirfd(2), &r.path(3)?, r.arg_i64(4)),
330        SYMLINK => do_symlink(r, &r.path(0)?, libc::AT_FDCWD, &r.path(1)?),
331        SYMLINKAT => do_symlink(r, &r.path(0)?, r.dirfd(1), &r.path(2)?),
332        CHMOD => do_chmod(r, libc::AT_FDCWD, &r.path(0)?, r.arg_i64(1), 0),
333        FCHMODAT => do_chmod(r, r.dirfd(0), &r.path(1)?, r.arg_i64(2), 0),
334        FCHMODAT2 => do_chmod(r, r.dirfd(0), &r.path(1)?, r.arg_i64(2), r.arg_i64(3)),
335        CHOWN => do_chown(r, libc::AT_FDCWD, &r.path(0)?, r.arg_i64(1), r.arg_i64(2), 0),
336        LCHOWN => {
337            let flags = libc::AT_SYMLINK_NOFOLLOW as i64;
338            do_chown(r, libc::AT_FDCWD, &r.path(0)?, r.arg_i64(1), r.arg_i64(2), flags)
339        }
340        FCHOWNAT => do_chown(
341            r,
342            r.dirfd(0),
343            &r.path(1)?,
344            r.arg_i64(2),
345            r.arg_i64(3),
346            r.arg_i64(4),
347        ),
348        TRUNCATE => do_truncate(r),
349        MKNOD => do_mknod(r, libc::AT_FDCWD, &r.path(0)?, r.arg_i64(1), r.arg_i64(2)),
350        MKNODAT => do_mknod(r, r.dirfd(0), &r.path(1)?, r.arg_i64(2), r.arg_i64(3)),
351        UTIME => do_utime(r),
352        UTIMES => do_utimes(r, libc::AT_FDCWD, 0, 1),
353        FUTIMESAT => do_utimes(r, r.dirfd(0), 1, 2),
354        UTIMENSAT => do_utimensat(r),
355        SETXATTR | LSETXATTR => do_setxattr(r, nr == LSETXATTR),
356        GETXATTR | LGETXATTR => do_getxattr(r, nr == LGETXATTR),
357        LISTXATTR | LLISTXATTR => do_listxattr(r, nr == LLISTXATTR),
358        REMOVEXATTR | LREMOVEXATTR => do_removexattr(r, nr == LREMOVEXATTR),
359        INOTIFY_ADD_WATCH => do_inotify_add_watch(r),
360        CLONE3 => direct::do_clone(r, CLONE3),
361        PRCTL => do_prctl(r),
362        NAME_TO_HANDLE_AT => Ok(Outcome::Value(Err(libc::EOPNOTSUPP))),
363        _ => Ok(Outcome::Continue),
364    }
365}
366
367// ---- helpers ---------------------------------------------------------------
368
369/// Builds a C string from path bytes.
370fn cstr(path: &[u8]) -> Result<CString> {
371    CString::new(path.to_vec()).map_err(|_| libc::EINVAL)
372}
373
374/// Replaces a leading `/proc/self` or `/proc/thread-self` with the tracee's pid.
375fn rewrite_proc_self(path: &[u8], pid: u32) -> Vec<u8> {
376    let rules: [(&[u8], String); 2] = [
377        (b"/proc/thread-self", format!("/proc/{pid}/task/{pid}")),
378        (b"/proc/self", format!("/proc/{pid}")),
379    ];
380    for (prefix, repl) in rules {
381        if path.starts_with(prefix) && (path.len() == prefix.len() || path[prefix.len()] == b'/') {
382            let mut out = repl.into_bytes();
383            out.extend_from_slice(&path[prefix.len()..]);
384            return out;
385        }
386    }
387    path.to_vec()
388}
389
390/// Lexically cleans an absolute path (no symlink resolution).
391fn lexical_clean(path: &[u8]) -> Vec<u8> {
392    let mut parts: Vec<&[u8]> = Vec::new();
393    for comp in path.split(|&c| c == b'/') {
394        match comp {
395            b"" | b"." => {}
396            b".." => {
397                parts.pop();
398            }
399            c => parts.push(c),
400        }
401    }
402    let mut out = Vec::with_capacity(path.len() + 1);
403    for p in &parts {
404        out.push(b'/');
405        out.extend_from_slice(p);
406    }
407    if out.is_empty() {
408        out.push(b'/');
409    }
410    out
411}
412
413/// Classifies a guest path as a `/proc/<pid>/...` magic link.
414///
415/// # Returns
416/// `Some((pid, true))` for an `exe` link, `Some((pid, false))` for `cwd`,
417/// `root`, `fd/N` or `map_files/X`, `None` for anything else.
418fn proc_magic_link(guest: &[u8]) -> Option<(i32, bool)> {
419    let rest = guest.strip_prefix(b"/proc/")?;
420    let mut parts = rest.split(|&c| c == b'/');
421    let pid: i32 = std::str::from_utf8(parts.next()?).ok()?.parse().ok()?;
422    let mut tail: Vec<&[u8]> = parts.collect();
423    if tail.len() >= 3 && tail[0] == b"task" {
424        tail.drain(0..2);
425    }
426    match tail.as_slice() {
427        [b"exe"] => Some((pid, true)),
428        [b"cwd"] | [b"root"] => Some((pid, false)),
429        [b"fd", _] | [b"map_files", _] => Some((pid, false)),
430        _ => None,
431    }
432}
433
434/// Runs a handler again while it reports a swapped-in symlink ([`RETRY`]).
435fn retrying<T>(mut f: impl FnMut() -> Result<T>) -> Result<T> {
436    for _ in 0..RETRIES {
437        match f() {
438            Err(RETRY) => continue,
439            r => return r,
440        }
441    }
442    Err(libc::ELOOP)
443}
444
445/// Whether the resolved object is a FIFO; opening one may block the supervisor.
446fn is_fifo(resolved: &Resolved) -> bool {
447    resolved
448        .mode_nofollow()
449        .is_ok_and(|m| m & libc::S_IFMT == libc::S_IFIFO)
450}
451
452/// Reads all of an open descriptor without taking ownership of it.
453fn read_all(fd: i32) -> Result<Vec<u8>> {
454    let file = std::mem::ManuallyDrop::new(unsafe { std::fs::File::from_raw_fd(fd) });
455    let mut out = Vec::new();
456    (&*file)
457        .read_to_end(&mut out)
458        .map_err(|e| e.raw_os_error().unwrap_or(libc::EIO))?;
459    Ok(out)
460}
461
462/// The `AT_SYMLINK_NOFOLLOW` bit to pass for an act on a resolved path.
463fn nofollow_flag(nofollow: bool) -> i64 {
464    if nofollow {
465        libc::AT_SYMLINK_NOFOLLOW as i64
466    } else {
467        0
468    }
469}
470
471/// Reads a `struct timeval[2]` from the tracee and converts it to 64-bit timespecs.
472///
473/// An i386 caller (`Variant::Time32`) supplies 32-bit seconds and microseconds.
474fn timevals_to_timespecs(r: &Req, addr: u64) -> Result<Option<[u8; 32]>> {
475    if addr == 0 {
476        return Ok(None);
477    }
478    let (word, read_word): (usize, fn(&[u8]) -> i64) = match r.variant {
479        Variant::Time32 => (4, |b| i32::from_ne_bytes(b.try_into().unwrap()) as i64),
480        _ => (8, |b| i64::from_ne_bytes(b.try_into().unwrap())),
481    };
482    let tv = r.read(addr, 4 * word)?;
483    let mut ts = [0u8; 32];
484    for i in 0..2 {
485        let sec = read_word(&tv[i * 2 * word..i * 2 * word + word]);
486        let usec = read_word(&tv[i * 2 * word + word..i * 2 * word + 2 * word]);
487        ts[i * 16..i * 16 + 8].copy_from_slice(&sec.to_ne_bytes());
488        ts[i * 16 + 8..i * 16 + 16].copy_from_slice(&(usec * 1000).to_ne_bytes());
489    }
490    Ok(Some(ts))
491}
492
493/// Reads a `struct timespec[2]` from the tracee, widening 32-bit fields for i386 `utimensat`.
494///
495/// `UTIME_NOW`/`UTIME_OMIT` are positive values below 2^30 and survive the widening.
496fn timespecs_from_tracee(r: &Req, addr: u64) -> Result<Option<[u8; 32]>> {
497    if addr == 0 {
498        return Ok(None);
499    }
500    if r.variant != Variant::Timespec32 {
501        return Ok(Some(r.read(addr, 32)?.try_into().map_err(|_| libc::EFAULT)?));
502    }
503    let raw = r.read(addr, 16)?;
504    let mut ts = [0u8; 32];
505    for i in 0..4 {
506        let v = i32::from_ne_bytes(raw[i * 4..i * 4 + 4].try_into().unwrap()) as i64;
507        ts[i * 8..i * 8 + 8].copy_from_slice(&v.to_ne_bytes());
508    }
509    Ok(Some(ts))
510}
511
512/// Runs `utimensat(2)` on a resolved path with an optional timespec pair.
513fn host_utimensat(r: &Req, resolved: &Resolved, times: Option<&[u8; 32]>, nofollow: bool) -> Result<i64> {
514    r.check_writable(resolved)?;
515    let (dfd, c, nofollow) = resolved.at(nofollow)?;
516    let tp = times.map(|t| t.as_ptr()).unwrap_or(std::ptr::null());
517    check(unsafe { libc::syscall(libc::SYS_utimensat, dfd, c.as_ptr(), tp, nofollow_flag(nofollow)) })
518}
519
520/// `O_NOFOLLOW` when the host open must not follow a final symlink.
521///
522/// The tracee's own `O_NOFOLLOW` is dropped first: a mount point is opened
523/// through its pinned descriptor's `/proc/self/fd` link, which the flag
524/// would refuse with `ELOOP` (libpulse opens `PULSE_RUNTIME_PATH` that way
525/// before connecting).
526///
527/// # Arguments
528/// * `nofollow` - What [`Resolved::at`] reported for the path.
529///
530/// # Returns
531/// `O_NOFOLLOW` or `0`.
532fn nofollow_open_flag(nofollow: bool) -> i64 {
533    if nofollow { libc::O_NOFOLLOW as i64 } else { 0 }
534}
535
536/// Deref policy derived from `AT_SYMLINK_NOFOLLOW`.
537fn follow(flags: i64) -> bool {
538    flags & libc::AT_SYMLINK_NOFOLLOW as i64 == 0
539}
540
541/// Whether a path argument is empty with `AT_EMPTY_PATH`, i.e. targets the fd itself.
542fn targets_fd(path: &[u8], flags: i64) -> bool {
543    path.is_empty() && flags & libc::AT_EMPTY_PATH as i64 != 0
544}
545
546// ---- open family -----------------------------------------------------------
547
548/// `open`/`creat`/`openat`: open in the supervisor and inject the descriptor.
549///
550/// `path_addr` is where the path string lives in the tracee, for the
551/// in-place rewrite an `O_PATH` open needs without ptrace.
552fn do_open(r: &Req, dirfd: i32, path: &[u8], path_addr: u64, flags: i64, mode: i64) -> Result<Outcome> {
553    let flags = r.abi.open_flags_to_host(flags);
554    if flags & libc::O_PATH as i64 != 0 && r.ctx.no_ptrace {
555        return direct::do_open_path(r, dirfd, path, path_addr, flags);
556    }
557    let o_excl_create = flags & libc::O_CREAT as i64 != 0 && flags & libc::O_EXCL as i64 != 0;
558    let deref = flags & libc::O_NOFOLLOW as i64 == 0 && !o_excl_create;
559    let cloexec = flags & libc::O_CLOEXEC as i64 != 0;
560    retrying(|| {
561        let resolved = r.translate(dirfd, path, deref)?;
562        r.valid()?;
563        if flags & WRITE_INTENT != 0 {
564            r.check_writable(&resolved)?;
565        }
566        if resolved.host == path && is_fifo(&resolved) {
567            // Identity path: let the tracee block on the FIFO itself rather than us.
568            return Ok(Outcome::Continue);
569        }
570        let (dfd, c, nofollow) = resolved.at(!deref)?;
571        let host_flags =
572            (flags & !(libc::O_NOFOLLOW as i64)) | libc::O_CLOEXEC as i64 | nofollow_open_flag(nofollow);
573        let fd = match check(unsafe { libc::syscall(libc::SYS_openat, dfd, c.as_ptr(), host_flags, mode) }) {
574            // A symlink where the walk followed none: swapped in since.
575            Err(libc::ELOOP) if deref && resolved.pinned() => return Err(RETRY),
576            rc => rc? as i32,
577        };
578        if flags & WRITE_INTENT == 0 {
579            match fake_proc_file(r, &resolved.host, fd) {
580                Ok(Some(fake)) => {
581                    unsafe { libc::close(fd) };
582                    return Ok(Outcome::Fd { fd: fake, cloexec });
583                }
584                Ok(None) => {}
585                Err(e) => {
586                    unsafe { libc::close(fd) };
587                    return Err(e);
588                }
589            }
590        }
591        Ok(Outcome::Fd { fd, cloexec })
592    })
593}
594
595/// Serves an opened `/proc/<pid>/status` or `cmdline` from a rewritten copy.
596///
597/// # Arguments
598/// * `host` - Host path of the opened file, which tells which it is.
599/// * `fd` - The open descriptor, read and left at its start when no copy is made.
600///
601/// # Returns
602/// A descriptor on the copy, or `None` when the file is neither or needs no rewrite.
603fn fake_proc_file(r: &Req, host: &[u8], fd: i32) -> Result<Option<i32>> {
604    let Some(rest) = host.strip_prefix(b"/proc/") else {
605        return Ok(None);
606    };
607    let (rest, status) = match (rest.strip_suffix(b"/status"), rest.strip_suffix(b"/cmdline")) {
608        (Some(p), _) => (p, true),
609        (_, Some(p)) => (p, false),
610        _ => return Ok(None),
611    };
612    let is_num = |b: &[u8]| !b.is_empty() && b.iter().all(|c| c.is_ascii_digit());
613    let ok = match rest.splitn(3, |&c| c == b'/').collect::<Vec<_>>().as_slice() {
614        [pid] => is_num(pid),
615        [pid, b"task", tid] => is_num(pid) && is_num(tid),
616        _ => false,
617    };
618    if !ok {
619        return Ok(None);
620    }
621    let text = read_all(fd)?;
622    let fake = if status {
623        Some(fake_proc_status(&text, fake_ids_for_status(r, host))?)
624    } else {
625        fake_proc_cmdline(r.ctx, rest, &text)?
626    };
627    if fake.is_none() {
628        unsafe { libc::lseek(fd, 0, libc::SEEK_SET) };
629    }
630    Ok(fake)
631}
632
633/// Serves `/proc/<pid>/status` with our own pid hidden from `TracerPid` and,
634/// under `-0`, root in the `Uid`/`Gid` lines.
635///
636/// Programs read that field to decide whether a debugger is attached and may
637/// change behaviour or trap when it is.  A transparent supervisor must not
638/// show up there, so the file is copied into a memfd with the field zeroed.
639///
640/// # Arguments
641/// * `text` - The file as the kernel gives it.
642/// * `fake_root` - The virtual identity to show, under `-0`.
643///
644/// # Returns
645/// A descriptor on the rewritten copy.
646fn fake_proc_status(text: &[u8], fake_root: Option<crate::procs::Ids>) -> Result<i32> {
647    let me = format!("TracerPid:\t{}\n", std::process::id()).into_bytes();
648    let mut out = Vec::with_capacity(text.len());
649    for line in text.split_inclusive(|&c| c == b'\n') {
650        if line == me.as_slice() {
651            out.extend_from_slice(b"TracerPid:\t0\n");
652        } else if let Some(ids) = fake_root
653            && (line.starts_with(b"Uid:") || line.starts_with(b"Gid:"))
654        {
655            let (a, b, c, d) = if line.starts_with(b"Uid:") {
656                (ids.ruid, ids.euid, ids.suid, ids.fsuid)
657            } else {
658                (ids.rgid, ids.egid, ids.sgid, ids.fsgid)
659            };
660            out.extend_from_slice(&line[..4]);
661            out.extend_from_slice(format!("\t{a}\t{b}\t{c}\t{d}\n").as_bytes());
662        } else {
663            out.extend_from_slice(line);
664        }
665    }
666    readonly_memfd("proc-status", &out)
667}
668
669/// Serves `/proc/<pid>/cmdline` as the process was started, without the
670/// loader the exec was retargeted at.
671///
672/// The kernel's file names the dynamic loader's alias, `--argv0`, the name
673/// and the program before the real arguments, or the shim loader's alias,
674/// the program and its `AT_EXECFN` before the whole argv; `ps`, `pgrep -f`
675/// and programs reading their own command line expect the argv the tracee
676/// passed.  A process in
677/// the no-ptrace trampoline, or one that rewrote its own argv, is left as is.
678///
679/// In the guest-stub restricted mode no loader is exec'd, so the kernel's
680/// file is the vector the program was exec'd with, which is right but for a
681/// `#!` script: there the interpreter, its argument and the script recorded
682/// at the exec take the place of `argv[0]`, as the kernel would show them.
683///
684/// # Arguments
685/// * `ctx` - Shared supervisor state.
686/// * `rest` - The `<pid>` or `<pid>/task/<tid>` of the path.
687/// * `text` - The file as the kernel gives it.
688///
689/// # Returns
690/// A descriptor on the rewritten copy, or `None` when the file carries no
691/// loader prefix.
692///
693/// # Errors
694/// An errno when the copy cannot be made.
695fn fake_proc_cmdline(ctx: &Ctx, rest: &[u8], text: &[u8]) -> Result<Option<i32>> {
696    let args: Vec<&[u8]> = text.split(|&c| c == 0).collect();
697    if ctx.trap_mode() {
698        let pid: i32 = std::str::from_utf8(rest.split(|&c| c == b'/').next().unwrap_or_default())
699            .ok()
700            .and_then(|p| p.parse().ok())
701            .unwrap_or(0);
702        let Some(prefix) = ctx.procs.info(pid).cmdline_prefix.filter(|_| !text.is_empty()) else {
703            return Ok(None);
704        };
705        let out: Vec<u8> = prefix
706            .iter()
707            .map(|a| a.as_slice())
708            .chain(args.iter().skip(1).copied())
709            .collect::<Vec<_>>()
710            .join(&0u8);
711        return Ok(Some(readonly_memfd("proc-cmdline", &out)?));
712    }
713    let alias_dir = crate::exec::alias_dir();
714    let in_alias = args
715        .first()
716        .is_some_and(|a| crate::fs::binding::is_prefix(alias_dir.as_os_str().as_bytes(), a));
717    if !in_alias {
718        return Ok(None);
719    }
720    let tail: Vec<&[u8]> = match args.as_slice() {
721        [alias, ..] if crate::exec::is_loader_alias(alias) => {
722            let Some(rest) = args.get(crate::exec::LOADER_ARGS..) else {
723                return Ok(None);
724            };
725            rest.to_vec()
726        }
727        [_, b"--argv0", argv0, _, rest @ ..] => std::iter::once(*argv0).chain(rest.iter().copied()).collect(),
728        [_, prog, rest @ ..] => std::iter::once(*prog).chain(rest.iter().copied()).collect(),
729        _ => return Ok(None),
730    };
731    let out: Vec<u8> = tail.join(&0u8);
732    Ok(Some(readonly_memfd("proc-cmdline", &out)?))
733}
734
735/// A read-only descriptor on a memfd holding `content`.
736///
737/// The caller asked for a read-only file and a memfd is read-write, so the
738/// writable descriptor is re-opened read-only through `/proc/self/fd` and
739/// dropped, which keeps `F_GETFL` truthful.
740///
741/// # Arguments
742/// * `name` - Name of the memfd, for diagnostics.
743/// * `content` - Bytes the file holds.
744fn readonly_memfd(name: &str, content: &[u8]) -> Result<i32> {
745    let name = CString::new(name).unwrap();
746    let fd =
747        check(unsafe { libc::syscall(libc::SYS_memfd_create, name.as_ptr(), libc::MFD_CLOEXEC) })? as i32;
748    let mut off = 0;
749    while off < content.len() {
750        let n = unsafe { libc::write(fd, content[off..].as_ptr() as *const _, content.len() - off) };
751        if n <= 0 {
752            unsafe { libc::close(fd) };
753            return Err(crate::sys::errno());
754        }
755        off += n as usize;
756    }
757    let link = CString::new(format!("/proc/self/fd/{fd}")).unwrap();
758    let ro = unsafe { libc::open(link.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
759    unsafe { libc::close(fd) };
760    if ro < 0 {
761        return Err(crate::sys::errno());
762    }
763    Ok(ro)
764}
765
766/// `openat2`: like `openat` with a `struct open_how`.
767fn do_openat2(r: &Req) -> Result<Outcome> {
768    let path = r.path(1)?;
769    let size = (r.arg(3) as usize).min(24);
770    let mut how = [0u8; 24];
771    if size > 0 {
772        how[..size].copy_from_slice(&r.read(r.arg(2), size)?);
773    }
774    let flags = r
775        .abi
776        .open_flags_to_host(u64::from_ne_bytes(how[0..8].try_into().unwrap()) as i64);
777    if flags & libc::O_PATH as i64 != 0 {
778        // An O_PATH descriptor cannot be injected; ENOSYS makes callers fall
779        // back to openat(2), which the filter routes to the ptrace tier.
780        return Err(libc::ENOSYS);
781    }
782    let o_excl_create = flags & libc::O_CREAT as i64 != 0 && flags & libc::O_EXCL as i64 != 0;
783    let deref = flags & libc::O_NOFOLLOW as i64 == 0 && !o_excl_create;
784    retrying(|| {
785        let resolved = r.translate(r.dirfd(0), &path, deref)?;
786        r.valid()?;
787        if flags & WRITE_INTENT != 0 {
788            r.check_writable(&resolved)?;
789        }
790        let (dfd, c, nofollow) = resolved.at(!deref)?;
791        let mut how = how;
792        let host_flags = (flags & !(libc::O_NOFOLLOW as i64)) | nofollow_open_flag(nofollow);
793        how[0..8].copy_from_slice(&(host_flags as u64).to_ne_bytes());
794        if resolved.is_mount_point() {
795            let resolve = u64::from_ne_bytes(how[16..24].try_into().unwrap());
796            let keep = resolve & !(libc::RESOLVE_NO_SYMLINKS | libc::RESOLVE_NO_MAGICLINKS);
797            how[16..24].copy_from_slice(&keep.to_ne_bytes());
798        }
799        let fd = match check(unsafe {
800            libc::syscall(libc::SYS_openat2, dfd, c.as_ptr(), how.as_ptr(), 24usize)
801        }) {
802            Err(libc::ELOOP) if deref && resolved.pinned() => return Err(RETRY),
803            rc => rc? as i32,
804        };
805        Ok(Outcome::Fd {
806            fd,
807            cloexec: flags & libc::O_CLOEXEC as i64 != 0,
808        })
809    })
810}
811
812// ---- stat family -----------------------------------------------------------
813
814/// `stat`/`lstat`/`newfstatat`: fill the host's `struct stat` in the tracee
815/// (144 bytes on x86_64, 128 on aarch64), converted for i386 callers.
816fn do_stat(r: &Req, dirfd: i32, path: &[u8], buf: u64, flags: i64) -> Result<Outcome> {
817    if targets_fd(path, flags) {
818        return Ok(Outcome::Continue);
819    }
820    let st = retrying(|| {
821        let resolved = r.translate(dirfd, path, follow(flags))?;
822        r.valid()?;
823        let (dfd, c, nofollow) = resolved.at(!follow(flags))?;
824        let mut st: libc::stat = unsafe { std::mem::zeroed() };
825        let host_flags = flags & libc::AT_NO_AUTOMOUNT as i64 | nofollow_flag(nofollow);
826        check(unsafe {
827            libc::syscall(
828                libc::SYS_newfstatat,
829                dfd,
830                c.as_ptr(),
831                &mut st as *mut libc::stat,
832                host_flags,
833            )
834        })?;
835        if follow(flags) && resolved.pinned() && st.st_mode & libc::S_IFMT == libc::S_IFLNK {
836            return Err(RETRY);
837        }
838        Ok(st)
839    })?;
840    let mut st = st;
841    fake_owner(r, &mut st.st_uid, &mut st.st_gid);
842    match r.variant {
843        Variant::Stat64 => r.write(buf, &crate::abi::stat_to_stat64(&st))?,
844        Variant::ArmStat64 => r.write(buf, &crate::abi::stat_to_arm_stat64(&st))?,
845        Variant::OldStat => r.write(buf, &crate::abi::stat_to_old_stat(&st)?)?,
846        _ => r.write(buf, unsafe {
847            std::slice::from_raw_parts(
848                &st as *const libc::stat as *const u8,
849                std::mem::size_of::<libc::stat>(),
850            )
851        })?,
852    }
853    Ok(Outcome::Value(Ok(0)))
854}
855
856/// `statx`: fill a 256-byte `struct statx` in the tracee.
857fn do_statx(r: &Req) -> Result<Outcome> {
858    const STATX_TYPE: u32 = 0x1;
859    let flags = r.arg_i64(2);
860    if r.arg(1) == 0 {
861        return Ok(Outcome::Continue);
862    }
863    let path = r.path(1)?;
864    if targets_fd(&path, flags) {
865        return Ok(Outcome::Continue);
866    }
867    let mut st = retrying(|| {
868        let resolved = r.translate(r.dirfd(0), &path, follow(flags))?;
869        r.valid()?;
870        let (dfd, c, nofollow) = resolved.at(!follow(flags))?;
871        let mut st = [0u8; 256];
872        let host_flags =
873            flags & !(libc::AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW) as i64 | nofollow_flag(nofollow);
874        // The type is asked for as well, to tell a swapped-in symlink.
875        check(unsafe {
876            libc::syscall(
877                libc::SYS_statx,
878                dfd,
879                c.as_ptr(),
880                host_flags,
881                r.arg(3) as u32 | STATX_TYPE,
882                st.as_mut_ptr(),
883            )
884        })?;
885        let mode = u16::from_ne_bytes(st[28..30].try_into().unwrap()) as u32;
886        if follow(flags) && resolved.pinned() && mode & libc::S_IFMT == libc::S_IFLNK {
887            return Err(RETRY);
888        }
889        Ok(st)
890    })?;
891    let (mut uid, mut gid) = (
892        u32::from_ne_bytes(st[20..24].try_into().unwrap()),
893        u32::from_ne_bytes(st[24..28].try_into().unwrap()),
894    );
895    fake_owner(r, &mut uid, &mut gid);
896    st[20..24].copy_from_slice(&uid.to_ne_bytes());
897    st[24..28].copy_from_slice(&gid.to_ne_bytes());
898    r.write(r.arg(4), &st)?;
899    Ok(Outcome::Value(Ok(0)))
900}
901
902/// `statfs`: fill a 120-byte `struct statfs` in the tracee.
903fn do_statfs(r: &Req) -> Result<Outcome> {
904    let path = r.path(0)?;
905    let st = retrying(|| {
906        let resolved = r.translate(libc::AT_FDCWD, &path, true)?;
907        r.valid()?;
908        let (fd, _) = resolved.object(true)?;
909        let c = fd_path(&fd);
910        let mut st: libc::statfs = unsafe { std::mem::zeroed() };
911        check(unsafe { libc::syscall(libc::SYS_statfs, c.as_ptr(), &mut st as *mut libc::statfs) })?;
912        Ok(st)
913    })?;
914    match r.variant {
915        // statfs64(path, size, buf)
916        Variant::Statfs64 => r.write(r.arg(2), &crate::abi::statfs_to_statfs64(&st))?,
917        Variant::OldStatfs => r.write(r.arg(1), &crate::abi::statfs_to_old_statfs(&st))?,
918        _ => r.write(r.arg(1), unsafe {
919            std::slice::from_raw_parts(
920                &st as *const libc::statfs as *const u8,
921                std::mem::size_of::<libc::statfs>(),
922            )
923        })?,
924    }
925    Ok(Outcome::Value(Ok(0)))
926}
927
928/// `access`/`faccessat`/`faccessat2`.
929fn do_access(r: &Req, dirfd: i32, path: &[u8], mode: i64, flags: i64) -> Result<Outcome> {
930    let resolved = if flags & libc::AT_EMPTY_PATH as i64 != 0 {
931        r.translate_or_dirfd(dirfd, path, follow(flags))?
932    } else {
933        r.translate(dirfd, path, follow(flags))?
934    };
935    r.valid()?;
936    let (dfd, c, nofollow) = resolved.at(!follow(flags))?;
937    let host_flags = flags & libc::AT_EACCESS as i64 | nofollow_flag(nofollow);
938    let rc = check(unsafe { libc::syscall(libc::SYS_faccessat2, dfd, c.as_ptr(), mode, host_flags) });
939    Ok(Outcome::Value(rc))
940}
941
942// ---- links and names ---------------------------------------------------------
943
944/// `readlink`/`readlinkat`: raw target, except `/proc` magic links are detranslated.
945///
946/// An empty path names the link `dirfd` was opened on.  Where that is no
947/// link, the answer is `ENOENT`, as the kernel's is.
948fn do_readlink(r: &Req, dirfd: i32, path: &[u8], buf: u64, bufsiz: usize) -> Result<Outcome> {
949    let target = match direct::readlink_alias_fd(r, dirfd, path) {
950        Some(target) => target?,
951        None => readlink_by_path(r, dirfd, path)?,
952    };
953    let len = target.len().min(bufsiz);
954    if len > 0 {
955        r.write(buf, &target[..len])?;
956    }
957    Ok(Outcome::Value(Ok(len as i64)))
958}
959
960/// The target of the link `path` names, or `dirfd` was opened on for an
961/// empty path, read by its path.
962///
963/// # Arguments
964/// * `r` - The notification.
965/// * `dirfd` - Base descriptor for a relative path, or `libc::AT_FDCWD`.
966/// * `path` - Path bytes as read from the tracee, without NUL.
967///
968/// # Errors
969/// As the translation, or the errno of reading the link.
970fn readlink_by_path(r: &Req, dirfd: i32, path: &[u8]) -> Result<Vec<u8>> {
971    let resolved = r.translate_or_dirfd(dirfd, path, false)?;
972    r.valid()?;
973    let read = || -> Result<Vec<u8>> {
974        let target = match &resolved.at {
975            crate::fs::canon::At::Child { parent, name } => readlinkat(parent, name),
976            // A mount point's host side is never a symlink.
977            crate::fs::canon::At::Object(_) => Err(libc::EINVAL),
978            crate::fs::canon::At::Path => read_link(&resolved.host),
979        };
980        target.map_err(|e| {
981            if e == libc::EINVAL && path.is_empty() {
982                libc::ENOENT
983            } else {
984                e
985            }
986        })
987    };
988    let guest = lexical_clean(&resolved.guest);
989    let target = match proc_magic_link(&guest) {
990        Some((pid, true)) => match r.ctx.tracee(pid).exe_guest {
991            Some(exe) => exe,
992            None => detranslate_path(r.fs(), &read()?),
993        },
994        Some((pid, false)) => match r.fs().virtual_cwd(pid).filter(|_| guest.ends_with(b"/cwd")) {
995            Some(cwd) => cwd,
996            None => detranslate_path(r.fs(), &read()?),
997        },
998        None => read()?,
999    };
1000    Ok(target)
1001}
1002
1003/// `getcwd`: the tracee's cwd as a guest path, NUL-terminated.
1004fn do_getcwd(r: &Req) -> Result<Outcome> {
1005    let mut cwd = guest_cwd(r.fs(), r.pid)?;
1006    r.valid()?;
1007    cwd.push(0);
1008    if cwd.len() > r.arg(1) as usize {
1009        return Err(libc::ERANGE);
1010    }
1011    r.write(r.arg(0), &cwd)?;
1012    Ok(Outcome::Value(Ok(cwd.len() as i64)))
1013}
1014
1015/// `uname`: the host's answer with `release` and/or `nodename` replaced.
1016fn do_uname(r: &Req) -> Result<Outcome> {
1017    let mut u: libc::utsname = unsafe { std::mem::zeroed() };
1018    check(unsafe { libc::uname(&mut u) } as i64)?;
1019    let mut bytes = [0u8; 390];
1020    let src = unsafe { std::slice::from_raw_parts(&u as *const _ as *const u8, 390) };
1021    bytes.copy_from_slice(src);
1022    if let Some(rel) = &r.view().kernel_release {
1023        let n = rel.len().min(64);
1024        bytes[130..195].fill(0);
1025        bytes[130..130 + n].copy_from_slice(&rel[..n]);
1026    }
1027    if let Some(host) = &r.view().hostname {
1028        // utsname.nodename sits at offset 65.
1029        let n = host.len().min(64);
1030        bytes[65..130].fill(0);
1031        bytes[65..65 + n].copy_from_slice(&host[..n]);
1032    }
1033    r.valid()?;
1034    r.write(r.arg(0), &bytes)?;
1035    Ok(Outcome::Value(Ok(0)))
1036}
1037
1038/// `prctl(PR_SET_DUMPABLE)` and `PR_GET_DUMPABLE`.
1039///
1040/// The flag is remembered for the process and never cleared in the kernel:
1041/// a non-dumpable tracee is unreachable through `process_vm_readv`,
1042/// `/proc/<pid>/mem` and `/proc/<pid>/fd` for a supervisor without
1043/// `CAP_SYS_PTRACE`, and every path syscall it made would fail with `EACCES`.
1044/// Other options reach the handler only through a filter without the
1045/// argument check and run natively.
1046///
1047/// # Returns
1048/// `0` for a set of `0` or `1`, the remembered flag for a get.
1049///
1050/// # Errors
1051/// `EINVAL` for a value the kernel refuses.
1052fn do_prctl(r: &Req) -> Result<Outcome> {
1053    let tgid = r.tgid();
1054    match r.arg(0) as i32 {
1055        libc::PR_SET_DUMPABLE => match r.arg(1) {
1056            v @ (0 | 1) => {
1057                r.ctx.set_dumpable(tgid, v == 1);
1058                Ok(Outcome::Value(Ok(0)))
1059            }
1060            _ => Ok(Outcome::Value(Err(libc::EINVAL))),
1061        },
1062        libc::PR_GET_DUMPABLE => Ok(Outcome::Value(Ok(r.ctx.dumpable(tgid) as i64))),
1063        _ => Ok(Outcome::Continue),
1064    }
1065}
1066
1067/// The identity syscalls under `-0`, over the process's virtual [`crate::procs::Ids`].
1068///
1069/// Getters report the virtual ids.  Setters follow the kernel's rules: with a
1070/// virtual effective id of 0 any value is accepted, otherwise only the real,
1071/// effective and saved ids may be chosen; a 32-bit `-1` leaves a field alone.
1072fn do_identity(r: &Req, nr: i64) -> Result<Outcome> {
1073    let tgid = r.tgid();
1074    let mut ids = r.ctx.ids(tgid);
1075    let id16 = r.variant == Variant::Id16;
1076    let write_three = |a: u32, b: u32, c: u32| -> Result<()> {
1077        for (i, v) in [a, b, c].into_iter().enumerate() {
1078            if r.arg(i) != 0 {
1079                if id16 {
1080                    r.write(r.arg(i), &(v as u16).to_ne_bytes())?
1081                } else {
1082                    r.write(r.arg(i), &v.to_ne_bytes())?
1083                }
1084            }
1085        }
1086        Ok(())
1087    };
1088    let arg = |i: usize| r.arg(i) as u32;
1089    let keep = |v: u32| v == u32::MAX;
1090    let allowed = |v: u32, (a, b, c): (u32, u32, u32)| v == a || v == b || v == c;
1091    let uids = (ids.ruid, ids.euid, ids.suid);
1092    let gids = (ids.rgid, ids.egid, ids.sgid);
1093    let root = ids.euid == 0;
1094    let rc: i64 = match nr {
1095        GETUID => ids.ruid as i64,
1096        GETEUID => ids.euid as i64,
1097        GETGID => ids.rgid as i64,
1098        GETEGID => ids.egid as i64,
1099        GETRESUID => {
1100            write_three(ids.ruid, ids.euid, ids.suid)?;
1101            0
1102        }
1103        GETRESGID => {
1104            write_three(ids.rgid, ids.egid, ids.sgid)?;
1105            0
1106        }
1107        SETUID => {
1108            let v = arg(0);
1109            if root {
1110                (ids.ruid, ids.euid, ids.suid) = (v, v, v);
1111            } else if allowed(v, uids) {
1112                ids.euid = v;
1113            } else {
1114                return Err(libc::EPERM);
1115            }
1116            ids.fsuid = ids.euid;
1117            0
1118        }
1119        SETGID => {
1120            let v = arg(0);
1121            if root {
1122                (ids.rgid, ids.egid, ids.sgid) = (v, v, v);
1123            } else if allowed(v, gids) {
1124                ids.egid = v;
1125            } else {
1126                return Err(libc::EPERM);
1127            }
1128            ids.fsgid = ids.egid;
1129            0
1130        }
1131        SETREUID | SETRESUID => {
1132            let (nr_, ne, ns) = if nr == SETREUID {
1133                (arg(0), arg(1), u32::MAX)
1134            } else {
1135                (arg(0), arg(1), arg(2))
1136            };
1137            for v in [nr_, ne, ns] {
1138                if !keep(v) && !root && !allowed(v, uids) {
1139                    return Err(libc::EPERM);
1140                }
1141            }
1142            if !keep(nr_) {
1143                ids.ruid = nr_;
1144            }
1145            if !keep(ne) {
1146                ids.euid = ne;
1147            }
1148            if !keep(ns) {
1149                ids.suid = ns;
1150            } else if nr == SETREUID && (!keep(nr_) || (!keep(ne) && ne != uids.0)) {
1151                ids.suid = ids.euid;
1152            }
1153            ids.fsuid = ids.euid;
1154            0
1155        }
1156        SETREGID | SETRESGID => {
1157            let (ng, ne, ns) = if nr == SETREGID {
1158                (arg(0), arg(1), u32::MAX)
1159            } else {
1160                (arg(0), arg(1), arg(2))
1161            };
1162            for v in [ng, ne, ns] {
1163                if !keep(v) && !root && !allowed(v, gids) {
1164                    return Err(libc::EPERM);
1165                }
1166            }
1167            if !keep(ng) {
1168                ids.rgid = ng;
1169            }
1170            if !keep(ne) {
1171                ids.egid = ne;
1172            }
1173            if !keep(ns) {
1174                ids.sgid = ns;
1175            } else if nr == SETREGID && (!keep(ng) || (!keep(ne) && ne != gids.0)) {
1176                ids.sgid = ids.egid;
1177            }
1178            ids.fsgid = ids.egid;
1179            0
1180        }
1181        SETFSUID => {
1182            let prev = ids.fsuid;
1183            let v = arg(0);
1184            if root || allowed(v, uids) || v == ids.fsuid {
1185                ids.fsuid = v;
1186            }
1187            prev as i64
1188        }
1189        SETFSGID => {
1190            let prev = ids.fsgid;
1191            let v = arg(0);
1192            if root || allowed(v, gids) || v == ids.fsgid {
1193                ids.fsgid = v;
1194            }
1195            prev as i64
1196        }
1197        _ => return Ok(Outcome::Continue),
1198    };
1199    r.ctx.set_ids(tgid, ids);
1200    Ok(Outcome::Value(Ok(rc)))
1201}
1202
1203/// `setgroups(size, list)` under `-0`: recorded for `getgroups`, never applied.
1204fn do_setgroups(r: &Req) -> Result<Outcome> {
1205    let tgid = r.tgid();
1206    if r.ctx.ids(tgid).euid != 0 {
1207        return Err(libc::EPERM);
1208    }
1209    let n = (r.arg(0) as usize).min(65536);
1210    let width = if r.variant == Variant::Id16 { 2 } else { 4 };
1211    let raw = if n > 0 {
1212        r.read(r.arg(1), n * width)?
1213    } else {
1214        Vec::new()
1215    };
1216    let groups: Vec<u32> = raw
1217        .chunks_exact(width)
1218        .map(|c| {
1219            if width == 2 {
1220                u16::from_ne_bytes([c[0], c[1]]) as u32
1221            } else {
1222                u32::from_ne_bytes(c.try_into().unwrap())
1223            }
1224        })
1225        .collect();
1226    r.ctx.set_groups(tgid, groups);
1227    Ok(Outcome::Value(Ok(0)))
1228}
1229
1230/// `getgroups(size, list)` under `-0`: the recorded list, or the kernel's when none was set.
1231fn do_getgroups(r: &Req) -> Result<Outcome> {
1232    let tgid = r.tgid();
1233    let Some(groups) = r.ctx.groups(tgid) else {
1234        return Ok(Outcome::Continue);
1235    };
1236    let size = r.arg(0) as usize;
1237    if size == 0 {
1238        return Ok(Outcome::Value(Ok(groups.len() as i64)));
1239    }
1240    if size < groups.len() {
1241        return Err(libc::EINVAL);
1242    }
1243    let mut out = Vec::with_capacity(groups.len() * 4);
1244    for g in &groups {
1245        if r.variant == Variant::Id16 {
1246            out.extend_from_slice(&(*g as u16).to_ne_bytes())
1247        } else {
1248            out.extend_from_slice(&g.to_ne_bytes())
1249        }
1250    }
1251    if !out.is_empty() {
1252        r.write(r.arg(1), &out)?;
1253    }
1254    Ok(Outcome::Value(Ok(groups.len() as i64)))
1255}
1256
1257/// `fchown` under `-0`: through the descriptor's `/proc` link; a refusal is forgiven.
1258fn do_fchown(r: &Req) -> Result<Outcome> {
1259    let c = cstr(format!("/proc/{}/fd/{}", r.pid, r.arg(0) as i32).as_bytes())?;
1260    let rc = check(unsafe {
1261        libc::syscall(
1262            libc::SYS_fchownat,
1263            libc::AT_FDCWD,
1264            c.as_ptr(),
1265            r.arg_i64(1),
1266            r.arg_i64(2),
1267            0,
1268        )
1269    });
1270    Ok(Outcome::Value(forgive_as_root(r, rc)))
1271}
1272
1273/// Under `-0`, an ownership or device-node refusal reads as success, like PRoot's fake_id0.
1274fn forgive_as_root(r: &Req, rc: Result<i64>) -> Result<i64> {
1275    match rc {
1276        Err(libc::EPERM) | Err(libc::EACCES) if r.view().fake_root => Ok(0),
1277        other => other,
1278    }
1279}
1280
1281/// Under `-0`, files owned by the supervisor's own ids belong to root.
1282fn fake_owner(r: &Req, uid: &mut u32, gid: &mut u32) {
1283    if r.view().fake_root {
1284        if *uid == unsafe { libc::getuid() } {
1285            *uid = 0;
1286        }
1287        if *gid == unsafe { libc::getgid() } {
1288            *gid = 0;
1289        }
1290    }
1291}
1292
1293/// Under `-0`, the virtual identity to show in a `/proc/<pid>/status` being opened.
1294fn fake_ids_for_status(r: &Req, host: &[u8]) -> Option<crate::procs::Ids> {
1295    if !r.view().fake_root {
1296        return None;
1297    }
1298    let pid: i32 = host
1299        .strip_prefix(b"/proc/")?
1300        .split(|&c| c == b'/')
1301        .next()
1302        .and_then(|p| std::str::from_utf8(p).ok()?.parse().ok())?;
1303    Some(r.ctx.ids(pid))
1304}
1305
1306/// `mkdir`/`mkdirat`.
1307fn do_mkdir(r: &Req, dirfd: i32, path: &[u8], mode: i64) -> Result<Outcome> {
1308    let resolved = r.translate(dirfd, path, false)?;
1309    r.valid()?;
1310    r.check_writable(&resolved)?;
1311    let (dfd, c, _) = resolved.at(true)?;
1312    let rc = check(unsafe { libc::syscall(libc::SYS_mkdirat, dfd, c.as_ptr(), mode) });
1313    r.fs().flush_cache();
1314    Ok(Outcome::Value(rc))
1315}
1316
1317/// `unlink`/`unlinkat`/`rmdir`.  A mount point of the view is busy, as a
1318/// mount point is.
1319fn do_unlink(r: &Req, dirfd: i32, path: &[u8], flags: i64) -> Result<Outcome> {
1320    let resolved = r.translate(dirfd, path, false)?;
1321    r.valid()?;
1322    r.check_writable(&resolved)?;
1323    if resolved.is_mount_point() {
1324        return Err(libc::EBUSY);
1325    }
1326    let (dfd, c, _) = resolved.at(true)?;
1327    let rc = check(unsafe { libc::syscall(libc::SYS_unlinkat, dfd, c.as_ptr(), flags) });
1328    r.fs().flush_cache();
1329    Ok(Outcome::Value(rc))
1330}
1331
1332/// `rename`/`renameat`/`renameat2`.  A mount point of the view is busy, as a
1333/// mount point is.
1334fn do_rename(r: &Req, olddirfd: i32, old: &[u8], newdirfd: i32, new: &[u8], flags: i64) -> Result<Outcome> {
1335    let from = r.translate(olddirfd, old, false)?;
1336    let to = r.translate(newdirfd, new, false)?;
1337    r.valid()?;
1338    r.check_writable(&from)?;
1339    r.check_writable(&to)?;
1340    if from.is_mount_point() || to.is_mount_point() {
1341        return Err(libc::EBUSY);
1342    }
1343    let (dfo, co, _) = from.at(true)?;
1344    let (dfn, cn, _) = to.at(true)?;
1345    let rc = check(unsafe { libc::syscall(libc::SYS_renameat2, dfo, co.as_ptr(), dfn, cn.as_ptr(), flags) });
1346    r.fs().flush_cache();
1347    Ok(Outcome::Value(rc))
1348}
1349
1350/// `link`/`linkat`.  An empty old path with `AT_EMPTY_PATH` links the tracee's
1351/// descriptor via its `/proc` entry.  The walk followed the old side when
1352/// asked to, so the kernel is never asked to follow it again.
1353fn do_link(r: &Req, olddirfd: i32, old: &[u8], newdirfd: i32, new: &[u8], flags: i64) -> Result<Outcome> {
1354    let from = if targets_fd(old, flags) {
1355        None
1356    } else {
1357        Some(r.translate(olddirfd, old, flags & libc::AT_SYMLINK_FOLLOW as i64 != 0)?)
1358    };
1359    let to = r.translate(newdirfd, new, false)?;
1360    r.valid()?;
1361    r.check_writable(&to)?;
1362    let (dfo, co, host_flags) = match &from {
1363        None => (
1364            libc::AT_FDCWD,
1365            cstr(format!("/proc/{}/fd/{}", r.pid, olddirfd).as_bytes())?,
1366            libc::AT_SYMLINK_FOLLOW as i64,
1367        ),
1368        Some(from) => {
1369            let (dfd, c, _) = from.at(true)?;
1370            // A mount point needs following through its /proc/self/fd link, and a
1371            // /proc fd link the caller follows (an O_TMPFILE published) to its object.
1372            let follow = if from.is_mount_point()
1373                || (flags & libc::AT_SYMLINK_FOLLOW as i64 != 0 && from.is_proc_fd_link())
1374            {
1375                libc::AT_SYMLINK_FOLLOW as i64
1376            } else {
1377                0
1378            };
1379            (dfd, c, follow)
1380        }
1381    };
1382    let (dfn, cn, _) = to.at(true)?;
1383    let rc =
1384        check(unsafe { libc::syscall(libc::SYS_linkat, dfo, co.as_ptr(), dfn, cn.as_ptr(), host_flags) });
1385    r.fs().flush_cache();
1386    Ok(Outcome::Value(rc))
1387}
1388
1389/// `symlink`/`symlinkat`: the target is stored verbatim, only the link path is translated.
1390fn do_symlink(r: &Req, target: &[u8], newdirfd: i32, linkpath: &[u8]) -> Result<Outcome> {
1391    let resolved = r.translate(newdirfd, linkpath, false)?;
1392    r.valid()?;
1393    r.check_writable(&resolved)?;
1394    let (dfd, cl, _) = resolved.at(true)?;
1395    let ct = cstr(target)?;
1396    let rc = check(unsafe { libc::syscall(libc::SYS_symlinkat, ct.as_ptr(), dfd, cl.as_ptr()) });
1397    r.fs().flush_cache();
1398    Ok(Outcome::Value(rc))
1399}
1400
1401// ---- attributes --------------------------------------------------------------
1402
1403/// `chmod`/`fchmodat`/`fchmodat2`, on the object's `/proc/self/fd` link.
1404///
1405/// A symlink itself has no mode to change: `EOPNOTSUPP`, as `fchmodat2`
1406/// says of `AT_SYMLINK_NOFOLLOW` on one.
1407fn do_chmod(r: &Req, dirfd: i32, path: &[u8], mode: i64, flags: i64) -> Result<Outcome> {
1408    let rc = retrying(|| {
1409        let resolved = if flags & libc::AT_EMPTY_PATH as i64 != 0 {
1410            r.translate_or_dirfd(dirfd, path, follow(flags))?
1411        } else {
1412            r.translate(dirfd, path, follow(flags))?
1413        };
1414        r.valid()?;
1415        r.check_writable(&resolved)?;
1416        let (fd, st_mode) = resolved.object(follow(flags))?;
1417        if st_mode & libc::S_IFMT == libc::S_IFLNK {
1418            return Ok(Err(libc::EOPNOTSUPP));
1419        }
1420        let c = fd_path(&fd);
1421        Ok(check(unsafe {
1422            libc::syscall(SYS_FCHMODAT2_HOST, libc::AT_FDCWD, c.as_ptr(), mode, 0)
1423        }))
1424    })?;
1425    Ok(Outcome::Value(rc))
1426}
1427
1428/// `chown`/`lchown`/`fchownat`.
1429fn do_chown(r: &Req, dirfd: i32, path: &[u8], uid: i64, gid: i64, flags: i64) -> Result<Outcome> {
1430    if targets_fd(path, flags) {
1431        return Ok(Outcome::Continue);
1432    }
1433    let resolved = r.translate(dirfd, path, follow(flags))?;
1434    r.valid()?;
1435    r.check_writable(&resolved)?;
1436    let (dfd, c, nofollow) = resolved.at(!follow(flags))?;
1437    let rc = check(unsafe {
1438        libc::syscall(
1439            libc::SYS_fchownat,
1440            dfd,
1441            c.as_ptr(),
1442            uid,
1443            gid,
1444            nofollow_flag(nofollow),
1445        )
1446    });
1447    Ok(Outcome::Value(forgive_as_root(r, rc)))
1448}
1449
1450/// `truncate`, on the object's `/proc/self/fd` link.
1451fn do_truncate(r: &Req) -> Result<Outcome> {
1452    let path = r.path(0)?;
1453    // truncate64(path, len_lo, len_hi) on i386; ARM pads to an even register pair.
1454    let len = match r.variant {
1455        Variant::Truncate64 => (r.arg(1) | (r.arg(2) << 32)) as i64,
1456        Variant::Truncate64Arm => (r.arg(2) | (r.arg(3) << 32)) as i64,
1457        _ => r.arg_i64(1),
1458    };
1459    let rc = retrying(|| {
1460        let resolved = r.translate(libc::AT_FDCWD, &path, true)?;
1461        r.valid()?;
1462        r.check_writable(&resolved)?;
1463        let (fd, _) = resolved.object(true)?;
1464        let c = fd_path(&fd);
1465        Ok(check(unsafe {
1466            libc::syscall(libc::SYS_truncate, c.as_ptr(), len)
1467        }))
1468    })?;
1469    Ok(Outcome::Value(rc))
1470}
1471
1472/// `mknod`/`mknodat`.
1473fn do_mknod(r: &Req, dirfd: i32, path: &[u8], mode: i64, dev: i64) -> Result<Outcome> {
1474    let resolved = r.translate(dirfd, path, false)?;
1475    r.valid()?;
1476    r.check_writable(&resolved)?;
1477    let (dfd, c, _) = resolved.at(true)?;
1478    let rc = check(unsafe { libc::syscall(libc::SYS_mknodat, dfd, c.as_ptr(), mode, dev) });
1479    Ok(Outcome::Value(forgive_as_root(r, rc)))
1480}
1481
1482/// `utime`: `struct utimbuf` (two seconds fields) to timespecs.
1483fn do_utime(r: &Req) -> Result<Outcome> {
1484    let path = r.path(0)?;
1485    let times = if r.arg(1) == 0 {
1486        None
1487    } else {
1488        let mut ts = [0u8; 32];
1489        if r.variant == Variant::Time32 {
1490            let ub = r.read(r.arg(1), 8)?;
1491            for (dst, src) in [(0usize, 0usize), (16, 4)] {
1492                let v = i32::from_ne_bytes(ub[src..src + 4].try_into().unwrap()) as i64;
1493                ts[dst..dst + 8].copy_from_slice(&v.to_ne_bytes());
1494            }
1495        } else {
1496            let ub = r.read(r.arg(1), 16)?;
1497            ts[0..8].copy_from_slice(&ub[0..8]);
1498            ts[16..24].copy_from_slice(&ub[8..16]);
1499        }
1500        Some(ts)
1501    };
1502    let resolved = r.translate(libc::AT_FDCWD, &path, true)?;
1503    r.valid()?;
1504    Ok(Outcome::Value(host_utimensat(
1505        r,
1506        &resolved,
1507        times.as_ref(),
1508        false,
1509    )))
1510}
1511
1512/// `utimes`/`futimesat`: `struct timeval[2]` to timespecs.
1513fn do_utimes(r: &Req, dirfd: i32, path_idx: usize, times_idx: usize) -> Result<Outcome> {
1514    let path = r.path(path_idx)?;
1515    let times = timevals_to_timespecs(r, r.arg(times_idx))?;
1516    let resolved = r.translate(dirfd, &path, true)?;
1517    r.valid()?;
1518    Ok(Outcome::Value(host_utimensat(
1519        r,
1520        &resolved,
1521        times.as_ref(),
1522        false,
1523    )))
1524}
1525
1526/// `utimensat`.  A NULL path targets the descriptor itself and is passed through.
1527fn do_utimensat(r: &Req) -> Result<Outcome> {
1528    if r.arg(1) == 0 {
1529        return Ok(Outcome::Continue);
1530    }
1531    let flags = r.arg_i64(3);
1532    let path = r.path(1)?;
1533    if targets_fd(&path, flags) {
1534        return Ok(Outcome::Continue);
1535    }
1536    let times = timespecs_from_tracee(r, r.arg(2))?;
1537    let resolved = r.translate(r.dirfd(0), &path, follow(flags))?;
1538    r.valid()?;
1539    Ok(Outcome::Value(host_utimensat(
1540        r,
1541        &resolved,
1542        times.as_ref(),
1543        !follow(flags),
1544    )))
1545}
1546
1547/// The path an extended-attribute syscall acts on.
1548///
1549/// The `l` variants act on the name as it is now, through the pinned parent
1550/// (`/proc/self/fd/<parent>/<name>`), since a symlink is what they are for.
1551/// The others act on the object's own `/proc/self/fd` link, which the
1552/// kernel resolves to the inode, once the object is seen not to be a symlink
1553/// swapped in after the walk.
1554///
1555/// # Returns
1556/// The path, and the descriptor that keeps it valid.
1557fn xattr_target(resolved: &Resolved, nofollow: bool) -> Result<(CString, Option<Arc<std::os::fd::OwnedFd>>)> {
1558    if nofollow && !resolved.is_mount_point() {
1559        return Ok((resolved.parent_path()?, None));
1560    }
1561    let (fd, _) = resolved.object(true)?;
1562    Ok((fd_path(&fd), Some(fd)))
1563}
1564
1565/// `setxattr`/`lsetxattr`.
1566fn do_setxattr(r: &Req, nofollow: bool) -> Result<Outcome> {
1567    let path = r.path(0)?;
1568    let name = cstr(&r.ctx.mem.read_cstr(r.pid, r.arg(1))?)?;
1569    let size = r.arg(3) as usize;
1570    if size > XATTR_MAX {
1571        return Err(libc::E2BIG);
1572    }
1573    let value = r.read(r.arg(2), size)?;
1574    let rc = retrying(|| {
1575        let resolved = r.translate(libc::AT_FDCWD, &path, !nofollow)?;
1576        r.valid()?;
1577        r.check_writable(&resolved)?;
1578        let (c, _keep) = xattr_target(&resolved, nofollow)?;
1579        let nr = if nofollow && !resolved.is_mount_point() {
1580            libc::SYS_lsetxattr
1581        } else {
1582            libc::SYS_setxattr
1583        };
1584        Ok(check(unsafe {
1585            libc::syscall(nr, c.as_ptr(), name.as_ptr(), value.as_ptr(), size, r.arg_i64(4))
1586        }))
1587    })?;
1588    Ok(Outcome::Value(rc))
1589}
1590
1591/// `getxattr`/`lgetxattr`.
1592fn do_getxattr(r: &Req, nofollow: bool) -> Result<Outcome> {
1593    let path = r.path(0)?;
1594    let name = cstr(&r.ctx.mem.read_cstr(r.pid, r.arg(1))?)?;
1595    let size = (r.arg(3) as usize).min(XATTR_MAX);
1596    let mut buf = vec![0u8; size];
1597    let rc = retrying(|| {
1598        let resolved = r.translate(libc::AT_FDCWD, &path, !nofollow)?;
1599        r.valid()?;
1600        let (c, _keep) = xattr_target(&resolved, nofollow)?;
1601        let nr = if nofollow && !resolved.is_mount_point() {
1602            libc::SYS_lgetxattr
1603        } else {
1604            libc::SYS_getxattr
1605        };
1606        check(unsafe { libc::syscall(nr, c.as_ptr(), name.as_ptr(), buf.as_mut_ptr(), size) })
1607    })?;
1608    if size > 0 && rc > 0 {
1609        r.write(r.arg(2), &buf[..rc as usize])?;
1610    }
1611    Ok(Outcome::Value(Ok(rc)))
1612}
1613
1614/// `listxattr`/`llistxattr`.
1615fn do_listxattr(r: &Req, nofollow: bool) -> Result<Outcome> {
1616    let path = r.path(0)?;
1617    let size = (r.arg(2) as usize).min(XATTR_MAX);
1618    let mut buf = vec![0u8; size];
1619    let rc = retrying(|| {
1620        let resolved = r.translate(libc::AT_FDCWD, &path, !nofollow)?;
1621        r.valid()?;
1622        let (c, _keep) = xattr_target(&resolved, nofollow)?;
1623        let nr = if nofollow && !resolved.is_mount_point() {
1624            libc::SYS_llistxattr
1625        } else {
1626            libc::SYS_listxattr
1627        };
1628        check(unsafe { libc::syscall(nr, c.as_ptr(), buf.as_mut_ptr(), size) })
1629    })?;
1630    if size > 0 && rc > 0 {
1631        r.write(r.arg(1), &buf[..rc as usize])?;
1632    }
1633    Ok(Outcome::Value(Ok(rc)))
1634}
1635
1636/// `removexattr`/`lremovexattr`.
1637fn do_removexattr(r: &Req, nofollow: bool) -> Result<Outcome> {
1638    let path = r.path(0)?;
1639    let name = cstr(&r.ctx.mem.read_cstr(r.pid, r.arg(1))?)?;
1640    let rc = retrying(|| {
1641        let resolved = r.translate(libc::AT_FDCWD, &path, !nofollow)?;
1642        r.valid()?;
1643        r.check_writable(&resolved)?;
1644        let (c, _keep) = xattr_target(&resolved, nofollow)?;
1645        let nr = if nofollow && !resolved.is_mount_point() {
1646            libc::SYS_lremovexattr
1647        } else {
1648            libc::SYS_removexattr
1649        };
1650        Ok(check(unsafe { libc::syscall(nr, c.as_ptr(), name.as_ptr()) }))
1651    })?;
1652    Ok(Outcome::Value(rc))
1653}
1654
1655/// `inotify_add_watch`.
1656///
1657/// The watch must be added to the tracee's own inotify descriptor, which the
1658/// supervisor cannot reach without `pidfd_getfd`.  Paths that translate to
1659/// themselves are passed through; translated paths fail with `ENOENT` so
1660/// watchers fall back to polling.  Moving this to the ptrace tier would fix it.
1661fn do_inotify_add_watch(r: &Req) -> Result<Outcome> {
1662    let path = r.path(1)?;
1663    let deref = r.arg(2) & IN_DONT_FOLLOW == 0;
1664    let resolved = r.translate(libc::AT_FDCWD, &path, deref)?;
1665    r.valid()?;
1666    if resolved.host == path {
1667        return Ok(Outcome::Continue);
1668    }
1669    note!(
1670        r.ctx,
1671        1,
1672        "pid {} inotify_add_watch on translated path {} unsupported",
1673        r.pid,
1674        String::from_utf8_lossy(&path)
1675    );
1676    Ok(Outcome::Value(Err(libc::ENOENT)))
1677}
1678
1679#[cfg(test)]
1680mod tests {
1681    use super::*;
1682
1683    #[test]
1684    fn rewrites_proc_self() {
1685        assert_eq!(rewrite_proc_self(b"/proc/self/exe", 42), b"/proc/42/exe".to_vec());
1686        assert_eq!(rewrite_proc_self(b"/proc/selfish", 42), b"/proc/selfish".to_vec());
1687        assert_eq!(
1688            rewrite_proc_self(b"/proc/thread-self/stat", 7),
1689            b"/proc/7/task/7/stat".to_vec()
1690        );
1691    }
1692
1693    #[test]
1694    fn classifies_magic_links() {
1695        assert_eq!(proc_magic_link(b"/proc/12/exe"), Some((12, true)));
1696        assert_eq!(proc_magic_link(b"/proc/12/task/13/exe"), Some((12, true)));
1697        assert_eq!(proc_magic_link(b"/proc/12/fd/3"), Some((12, false)));
1698        assert_eq!(proc_magic_link(b"/proc/12/cwd"), Some((12, false)));
1699        assert_eq!(proc_magic_link(b"/proc/12/status"), None);
1700        assert_eq!(proc_magic_link(b"/etc/passwd"), None);
1701    }
1702
1703    #[test]
1704    fn cleans_lexically() {
1705        assert_eq!(lexical_clean(b"/a//b/./../c"), b"/a/c".to_vec());
1706        assert_eq!(lexical_clean(b"/.."), b"/".to_vec());
1707    }
1708}