From a4139d12d8ef35d7741da0562877a944a89cac8f Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Thu, 1 Oct 2026 19:26:12 -0700 Subject: [PATCH 1/3] procfs: stat through a task's root or cwd link Since the normalizer started spelling /proc//root and cwd paths directly, the metadata handler never saw those entries: a path continuing past the link was probed by its literal spelling, which the probe refuses to resolve through a magic link, so stat of /proc/self/root/etc/passwd or /proc/self/cwd/hostname failed with ELOOP, and stat -L of the bare link fell to the same probe. Opening a path worked, which hid it: GNU cat never stats its operand, while uutils cat on riscv64 does. The handler now follows the task's own link itself, as the kernel would, and resolves the rest from there, still refusing further magic links. The bare link is the same case with nothing after it, and .. walks the real tree rather than the spelling. Signed-off-by: Cong Wang --- .../sandlock-core/src/procfs/net_metadata.rs | 58 +++++++++++++------ .../tests/integration/test_procfs.rs | 6 +- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/crates/sandlock-core/src/procfs/net_metadata.rs b/crates/sandlock-core/src/procfs/net_metadata.rs index 9770942a..3ddede59 100644 --- a/crates/sandlock-core/src/procfs/net_metadata.rs +++ b/crates/sandlock-core/src/procfs/net_metadata.rs @@ -376,6 +376,31 @@ fn probe_metadata(path: &str, dirfd: i64, pid: u32, follow: bool) -> Result Result { + let nofollow = |last: bool| if last && !follow { libc::O_NOFOLLOW } else { 0 }; + let target = CString::new(format!("/proc/{pid}/{link}")).unwrap(); + let base = crate::seccomp::notif::openat2_at( + libc::AT_FDCWD, + &target, + (libc::O_PATH | libc::O_CLOEXEC | nofollow(tail.is_empty())) as u64, + 0, + 0, + )?; + if tail.is_empty() { + return Ok(base); + } + let tail = CString::new(tail).map_err(|_| libc::EINVAL)?; + crate::seccomp::notif::openat2_at( + base.as_raw_fd(), + &tail, + (libc::O_PATH | libc::O_CLOEXEC | nofollow(true)) as u64, + 0, + 0x02, + ) +} + fn proc_task_entry(path: &str, caller_tid: i32, caller_tgid: i32) -> Option<(i32, i32, &str)> { let (task, mut rest) = path.strip_prefix("/proc/")?.split_once('/')?; let parent = match task { @@ -439,14 +464,21 @@ pub(crate) async fn handle_pinned_metadata( .processes .tgid_of(notif.pid as i32) .unwrap_or(notif.pid as i32); + let sandbox_task = |(parent, pid, _): &(i32, i32, &str)| { + ctx.processes.contains(*parent) + && ctx.processes.contains(*pid) + && (*parent == *pid || ctx.processes.tgid_of(*pid) == Some(*parent)) + }; let tracked_proc = absolute .as_deref() .and_then(Path::to_str) .and_then(|path| proc_task_entry(path, notif.pid as i32, tgid)) - .filter(|(parent, pid, _)| { - ctx.processes.contains(*parent) - && ctx.processes.contains(*pid) - && (*parent == *pid || ctx.processes.tgid_of(*pid) == Some(*parent)) + .filter(sandbox_task); + let task_link = proc_task_entry(&path, notif.pid as i32, tgid) + .filter(sandbox_task) + .and_then(|(_, pid, entry)| { + let (link, tail) = entry.split_once('/').unwrap_or((entry, "")); + matches!(link, "cwd" | "exe" | "root").then_some((pid, link, tail)) }); let held_fd = if path.is_empty() { if request.flags & libc::AT_EMPTY_PATH as u32 == 0 @@ -487,20 +519,10 @@ pub(crate) async fn handle_pinned_metadata( } let probe = match held_fd { Some((pid, fd)) => crate::seccomp::notif::open_base_dir(pid, fd as i64), - None if tracked_proc - .is_some_and(|(_, _, entry)| matches!(entry, "cwd" | "exe" | "root")) => - { - let (_, pid, entry) = tracked_proc.unwrap(); - let target = CString::new(format!("/proc/{pid}/{entry}")).unwrap(); - crate::seccomp::notif::openat2_at( - libc::AT_FDCWD, - &target, - (libc::O_PATH | libc::O_CLOEXEC | if follow { 0 } else { libc::O_NOFOLLOW }) as u64, - 0, - 0, - ) - } - None => probe_metadata(&translated, request.dirfd, notif.pid, follow), + None => match task_link { + Some((pid, link, tail)) => probe_through_task_link(pid, link, tail, follow), + None => probe_metadata(&translated, request.dirfd, notif.pid, follow), + }, }; let mut fd = match probe { Ok(fd) => fd, diff --git a/crates/sandlock-core/tests/integration/test_procfs.rs b/crates/sandlock-core/tests/integration/test_procfs.rs index e019cff0..70d90271 100644 --- a/crates/sandlock-core/tests/integration/test_procfs.rs +++ b/crates/sandlock-core/tests/integration/test_procfs.rs @@ -983,10 +983,12 @@ async fn test_root_and_cwd_magic_links_name_the_virtual_file() { "[ \"$(cat /proc/self/root/etc/hostname)\" = \"$(cat /etc/hostname)\" ] && echo root-hostname; ", "grep -c : /proc/self/root/proc/net/dev; ", "cd /etc && [ \"$(cat /proc/self/cwd/hostname)\" = \"$(cat /etc/hostname)\" ] && echo cwd-hostname; ", - "[ \"$(cat /proc/$$/root/etc/hosts)\" = \"$(cat /etc/hosts)\" ] && echo pid-root-hosts" + "[ \"$(cat /proc/$$/root/etc/hosts)\" = \"$(cat /etc/hosts)\" ] && echo pid-root-hosts; ", + "[ -d /proc/self/root ] && [ -f /proc/self/root/etc/passwd ] && [ -f /proc/self/cwd/hostname ] ", + "&& echo stat-through-links" ); let (_, out) = run_sh(&policy, script).await; - assert_eq!(out, "root-hostname\n1\ncwd-hostname\npid-root-hosts"); + assert_eq!(out, "root-hostname\n1\ncwd-hostname\npid-root-hosts\nstat-through-links"); } #[tokio::test] From 7b944989d7838d7cecaa98f32761227cca771f5a Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Thu, 1 Oct 2026 19:46:15 -0700 Subject: [PATCH 2/3] procfs: parse a task's /proc prefix once, as the kernel walks it The metadata handler parsed the same path twice: the normalized spelling for fd/N, where the root and cwd links had already been rewritten away, and the literal one for those links, which missed /proc/self/./exe or a doubled slash and so regressed them to ELOOP, and missed relative spellings through a dirfd on /proc/. One parse now serves both. It walks the /proc// prefix the way the kernel would, collapsing ., .. and empty components, over the path joined to its base but not normalized, and leaves the entry and everything after it as spelled, so the tail behind a root or cwd link still resolves against the real tree. Signed-off-by: Cong Wang --- crates/sandlock-core/src/procfs.rs | 65 ++++++++------ .../sandlock-core/src/procfs/net_metadata.rs | 85 ++++++++++++------- crates/sandlock-core/src/seccomp/notif.rs | 2 +- .../tests/integration/test_procfs.rs | 1 + 4 files changed, 96 insertions(+), 57 deletions(-) diff --git a/crates/sandlock-core/src/procfs.rs b/crates/sandlock-core/src/procfs.rs index 2b8aee3b..2200ab66 100644 --- a/crates/sandlock-core/src/procfs.rs +++ b/crates/sandlock-core/src/procfs.rs @@ -881,7 +881,44 @@ pub(crate) fn resolve_to_normalized_absolute( chroot_mounts: &[(std::path::PathBuf, std::path::PathBuf)], processes: &ProcessIndex, ) -> Option { - use std::path::{Component, Path, PathBuf}; + use std::path::{Component, PathBuf}; + + let joined = joined_absolute(pid, dirfd, path, chroot_root, chroot_mounts, processes)?; + let mut out = PathBuf::new(); + for comp in joined.components() { + match comp { + Component::Prefix(p) => out.push(p.as_os_str()), + Component::RootDir => out.push("/"), + Component::CurDir => {} + Component::ParentDir => { + // pop the last regular component; refuse to walk above + // the root (out becomes empty after popping "/"). + if !out.pop() { + return None; + } + if out.as_os_str().is_empty() { + return None; + } + } + Component::Normal(c) => out.push(c), + } + } + Some(match chroot_root { + None => through_task_root_and_cwd(out, pid, processes), + Some(_) => out, + }) +} + +/// `path` joined to the base it resolves against, without normalization. +pub(crate) fn joined_absolute( + pid: u32, + dirfd: i64, + path: &str, + chroot_root: Option<&std::path::Path>, + chroot_mounts: &[(std::path::PathBuf, std::path::PathBuf)], + processes: &ProcessIndex, +) -> Option { + use std::path::{Path, PathBuf}; // The dirfd/cwd symlink target is the *real* host directory. Under // chroot, sandlock services /proc, /etc and /dev via on-behalf opens, @@ -897,7 +934,7 @@ pub(crate) fn resolve_to_normalized_absolute( None => host, }; - let joined: PathBuf = if Path::new(path).is_absolute() { + Some(if Path::new(path).is_absolute() { PathBuf::from(path) } else if dirfd as i32 == libc::AT_FDCWD { // Under chroot the supervisor services chdir itself and the child's @@ -913,30 +950,6 @@ pub(crate) fn resolve_to_normalized_absolute( } else { let base = std::fs::read_link(format!("/proc/{}/fd/{}", pid, dirfd as i32)).ok()?; to_virtual(base).join(path) - }; - - let mut out = PathBuf::new(); - for comp in joined.components() { - match comp { - Component::Prefix(p) => out.push(p.as_os_str()), - Component::RootDir => out.push("/"), - Component::CurDir => {} - Component::ParentDir => { - // pop the last regular component; refuse to walk above - // the root (out becomes empty after popping "/"). - if !out.pop() { - return None; - } - if out.as_os_str().is_empty() { - return None; - } - } - Component::Normal(c) => out.push(c), - } - } - Some(match chroot_root { - None => through_task_root_and_cwd(out, pid, processes), - Some(_) => out, }) } diff --git a/crates/sandlock-core/src/procfs/net_metadata.rs b/crates/sandlock-core/src/procfs/net_metadata.rs index 3ddede59..87ecaaf0 100644 --- a/crates/sandlock-core/src/procfs/net_metadata.rs +++ b/crates/sandlock-core/src/procfs/net_metadata.rs @@ -372,10 +372,15 @@ fn probe_metadata(path: &str, dirfd: i64, pid: u32, follow: bool) -> Result Option<(&str, &str)> { + let (link, tail) = entry.split_once('/').unwrap_or((entry, "")); + matches!(link, "cwd" | "exe" | "root").then_some((link, tail.trim_start_matches('/'))) +} + // The task's own link is followed as the kernel would; the rest of the path // still may not cross another magic link. fn probe_through_task_link(pid: i32, link: &str, tail: &str, follow: bool) -> Result { @@ -397,27 +402,42 @@ fn probe_through_task_link(pid: i32, link: &str, tail: &str, follow: bool) -> Re &tail, (libc::O_PATH | libc::O_CLOEXEC | nofollow(true)) as u64, 0, - 0x02, + crate::seccomp::notif::RESOLVE_NO_MAGICLINKS, ) } +// Walks the /proc// prefix as the kernel would and leaves the entry, +// and everything after it, as spelled. fn proc_task_entry(path: &str, caller_tid: i32, caller_tgid: i32) -> Option<(i32, i32, &str)> { - let (task, mut rest) = path.strip_prefix("/proc/")?.split_once('/')?; + let mut prefix = Vec::new(); + let mut rest = path.strip_prefix('/')?; + let (task, thread, entry) = loop { + let (component, tail) = rest.split_once('/').unwrap_or((rest, "")); + match (prefix.as_slice(), component) { + (_, "" | ".") => {} + (_, "..") => { + prefix.pop(); + } + (["proc", task], entry) if entry != "task" => break (*task, None, rest), + (["proc", task, "task", tid], _) => break (*task, Some(*tid), rest), + _ => prefix.push(component), + } + if tail.is_empty() { + return None; + } + rest = tail; + }; + let positive = |id: &str| id.parse::().ok().filter(|id| *id > 0); let parent = match task { "self" | "thread-self" => caller_tgid, - _ => task.parse::().ok().filter(|pid| *pid > 0)?, + _ => positive(task)?, }; - let mut pid = if task == "thread-self" { - caller_tid - } else { - parent + let pid = match thread { + Some(tid) => positive(tid)?, + None if task == "thread-self" => caller_tid, + None => parent, }; - if let Some(thread) = rest.strip_prefix("task/") { - let (tid, tail) = thread.split_once('/')?; - pid = tid.parse::().ok().filter(|pid| *pid > 0)?; - rest = tail; - } - Some((parent, pid, rest)) + Some((parent, pid, entry)) } // The sealed snapshot name selects cosmetic metadata only, never access rights. @@ -464,21 +484,16 @@ pub(crate) async fn handle_pinned_metadata( .processes .tgid_of(notif.pid as i32) .unwrap_or(notif.pid as i32); - let sandbox_task = |(parent, pid, _): &(i32, i32, &str)| { - ctx.processes.contains(*parent) - && ctx.processes.contains(*pid) - && (*parent == *pid || ctx.processes.tgid_of(*pid) == Some(*parent)) - }; - let tracked_proc = absolute + let joined = + super::joined_absolute(notif.pid, request.dirfd, &path, None, &[], &ctx.processes); + let task_entry = joined .as_deref() .and_then(Path::to_str) - .and_then(|path| proc_task_entry(path, notif.pid as i32, tgid)) - .filter(sandbox_task); - let task_link = proc_task_entry(&path, notif.pid as i32, tgid) - .filter(sandbox_task) - .and_then(|(_, pid, entry)| { - let (link, tail) = entry.split_once('/').unwrap_or((entry, "")); - matches!(link, "cwd" | "exe" | "root").then_some((pid, link, tail)) + .and_then(|joined| proc_task_entry(joined, notif.pid as i32, tgid)) + .filter(|(parent, pid, _)| { + ctx.processes.contains(*parent) + && ctx.processes.contains(*pid) + && (*parent == *pid || ctx.processes.tgid_of(*pid) == Some(*parent)) }); let held_fd = if path.is_empty() { if request.flags & libc::AT_EMPTY_PATH as u32 == 0 @@ -494,7 +509,7 @@ pub(crate) async fn handle_pinned_metadata( .and_then(|p| super::own_fd_request(p, notif.pid as i32, tgid)) .map(|fd| (notif.pid, fd)) .or_else(|| { - let (_, pid, entry) = tracked_proc?; + let (_, pid, entry) = task_entry?; let fd = entry.strip_prefix("fd/")?; if !fd.bytes().all(|byte| byte.is_ascii_digit()) { return None; @@ -519,8 +534,8 @@ pub(crate) async fn handle_pinned_metadata( } let probe = match held_fd { Some((pid, fd)) => crate::seccomp::notif::open_base_dir(pid, fd as i64), - None => match task_link { - Some((pid, link, tail)) => probe_through_task_link(pid, link, tail, follow), + None => match task_entry.and_then(|(_, pid, entry)| Some((pid, task_link(entry)?))) { + Some((pid, (link, tail))) => probe_through_task_link(pid, link, tail, follow), None => probe_metadata(&translated, request.dirfd, notif.pid, follow), }, }; @@ -705,6 +720,16 @@ mod tests { Some((8, 9, "exe")) ); assert_eq!(proc_task_entry("/proc/-1/fd/1", 9, 8), None); + assert_eq!(proc_task_entry("/proc//self/./exe", 9, 8), Some((8, 8, "exe"))); + assert_eq!( + proc_task_entry("/proc/42/task/43/../../fd/1", 9, 8), + Some((42, 42, "fd/1")) + ); + assert_eq!( + proc_task_entry("/proc/self/cwd/a/../b/", 9, 8), + Some((8, 8, "cwd/a/../b/")) + ); + assert_eq!(proc_task_entry("/proc/self/", 9, 8), None); } #[test] diff --git a/crates/sandlock-core/src/seccomp/notif.rs b/crates/sandlock-core/src/seccomp/notif.rs index 7700e179..e3cdd9e4 100644 --- a/crates/sandlock-core/src/seccomp/notif.rs +++ b/crates/sandlock-core/src/seccomp/notif.rs @@ -423,7 +423,7 @@ fn is_denied_with_symlink_resolve( /// `RESOLVE_NO_MAGICLINKS` — forbid `/proc` magic-link redirection during /// on-behalf resolution while still following ordinary symlinks the way the /// child's own open would. -const RESOLVE_NO_MAGICLINKS: u64 = 0x02; +pub(crate) const RESOLVE_NO_MAGICLINKS: u64 = 0x02; const RESOLVE_NO_SYMLINKS: u64 = 0x04; const RESOLVE_BENEATH: u64 = 0x08; const RESOLVE_IN_ROOT: u64 = 0x10; diff --git a/crates/sandlock-core/tests/integration/test_procfs.rs b/crates/sandlock-core/tests/integration/test_procfs.rs index 70d90271..645b41e2 100644 --- a/crates/sandlock-core/tests/integration/test_procfs.rs +++ b/crates/sandlock-core/tests/integration/test_procfs.rs @@ -985,6 +985,7 @@ async fn test_root_and_cwd_magic_links_name_the_virtual_file() { "cd /etc && [ \"$(cat /proc/self/cwd/hostname)\" = \"$(cat /etc/hostname)\" ] && echo cwd-hostname; ", "[ \"$(cat /proc/$$/root/etc/hosts)\" = \"$(cat /etc/hosts)\" ] && echo pid-root-hosts; ", "[ -d /proc/self/root ] && [ -f /proc/self/root/etc/passwd ] && [ -f /proc/self/cwd/hostname ] ", + "&& [ -f /proc/self/./exe ] && [ -f /proc//self/root//etc/passwd ] ", "&& echo stat-through-links" ); let (_, out) = run_sh(&policy, script).await; From ee02dfd7ea170932de950a7279d1ace420b333ce Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Thu, 1 Oct 2026 20:05:03 -0700 Subject: [PATCH 3/3] procfs: move the general metadata handler out of net_metadata handle_pinned_metadata answers every path stat, access and readlink in a sandbox without a chroot, not just those under /proc/net, but it lived in net_metadata.rs because the network view introduced it. A bug in its general path, like stat through /proc/self/root, then read as a network problem. The request decoding, the probes, the /proc prefix parser and the handler move to procfs/metadata.rs unchanged. net_metadata.rs keeps the network catalog and exposes serve_pinned, which the handler asks once it has pinned the object. Signed-off-by: Cong Wang --- crates/sandlock-core/src/procfs.rs | 4 +- crates/sandlock-core/src/procfs/metadata.rs | 443 +++++++++++++++++ .../sandlock-core/src/procfs/net_metadata.rs | 463 ++---------------- 3 files changed, 488 insertions(+), 422 deletions(-) create mode 100644 crates/sandlock-core/src/procfs/metadata.rs diff --git a/crates/sandlock-core/src/procfs.rs b/crates/sandlock-core/src/procfs.rs index 2200ab66..5c3105fb 100644 --- a/crates/sandlock-core/src/procfs.rs +++ b/crates/sandlock-core/src/procfs.rs @@ -24,9 +24,11 @@ pub(crate) mod net; mod sock_diag; pub(crate) mod net_dispatch; +mod metadata; mod net_metadata; pub(crate) use net_dispatch::{handle_net_open, handle_net_directory}; -pub(crate) use net_metadata::{handle_net_metadata, handle_pinned_metadata}; +pub(crate) use metadata::handle_pinned_metadata; +pub(crate) use net_metadata::handle_net_metadata; use std::collections::HashSet; use std::ffi::CString; diff --git a/crates/sandlock-core/src/procfs/metadata.rs b/crates/sandlock-core/src/procfs/metadata.rs new file mode 100644 index 00000000..d64f717d --- /dev/null +++ b/crates/sandlock-core/src/procfs/metadata.rs @@ -0,0 +1,443 @@ +//! Path metadata calls are answered from an object pinned on the caller's +//! behalf, so no alias or racing swap reaches what the open handlers hide. + +use std::ffi::CString; +use std::os::unix::io::{AsRawFd, OwnedFd, RawFd}; +use std::path::Path; + +use super::{net_metadata, resolve_to_normalized_absolute}; +use crate::seccomp::ctx::SupervisorCtx; +use crate::seccomp::notif::{read_child_cstr, write_child_mem, NotifAction}; +use crate::sys::structs::SeccompNotif; + +#[derive(Clone, Copy)] +pub(super) enum Operation { + Stat, + Statx, + Access, + Readlink, +} + +pub(super) struct Request { + pub(super) operation: Operation, + pub(super) dirfd: i64, + pub(super) path: u64, + pub(super) output: u64, + pub(super) flags: u32, + pub(super) mode: u32, + pub(super) size: u64, +} + +impl Request { + pub(super) fn decode(notif: &SeccompNotif) -> Option { + let nr = notif.data.nr as i64; + let a = notif.data.args; + let mut request = Self { + operation: Operation::Stat, + dirfd: a[0] as i64, + path: a[1], + output: a[2], + flags: 0, + mode: 0, + size: 0, + }; + if nr == libc::SYS_fstat { + request.path = 0; + request.output = a[1]; + request.flags = libc::AT_EMPTY_PATH as u32; + } else if nr == libc::SYS_newfstatat { + request.flags = a[3] as u32; + } else if nr == libc::SYS_statx { + request.operation = Operation::Statx; + request.flags = a[2] as u32; + request.mode = a[3] as u32; + request.output = a[4]; + } else if nr == libc::SYS_faccessat || nr == crate::arch::SYS_FACCESSAT2 { + request.operation = Operation::Access; + request.mode = a[2] as u32; + if nr == crate::arch::SYS_FACCESSAT2 { + request.flags = a[3] as u32; + } + } else if nr == libc::SYS_readlinkat { + request.operation = Operation::Readlink; + request.flags = libc::AT_SYMLINK_NOFOLLOW as u32; + request.size = a[3]; + } else { + #[cfg(target_arch = "x86_64")] + { + request.dirfd = libc::AT_FDCWD as i64; + request.path = a[0]; + request.output = a[1]; + if nr == libc::SYS_lstat { + request.flags = libc::AT_SYMLINK_NOFOLLOW as u32; + } else if nr == libc::SYS_access { + request.operation = Operation::Access; + request.mode = a[1] as u32; + } else if nr == libc::SYS_readlink { + request.operation = Operation::Readlink; + request.flags = libc::AT_SYMLINK_NOFOLLOW as u32; + request.size = a[2]; + } else if nr != libc::SYS_stat { + return None; + } + } + #[cfg(not(target_arch = "x86_64"))] + return None; + } + Some(request) + } +} + +pub(super) fn write_result( + value: &T, + output: u64, + notif: &SeccompNotif, + notif_fd: RawFd, +) -> NotifAction { + let bytes = unsafe { + std::slice::from_raw_parts(value as *const T as *const u8, std::mem::size_of::()) + }; + match write_child_mem(notif_fd, notif.id, notif.pid, output, bytes) { + Ok(_) => NotifAction::ReturnValue(0), + Err(_) => NotifAction::Errno(libc::EFAULT), + } +} + +pub(super) fn errno_action() -> NotifAction { + NotifAction::Errno( + std::io::Error::last_os_error() + .raw_os_error() + .unwrap_or(libc::EIO), + ) +} + +pub(super) fn probe_metadata( + path: &str, + dirfd: i64, + pid: u32, + follow: bool, +) -> Result { + let base = if Path::new(path).is_absolute() { + None + } else { + Some(crate::seccomp::notif::open_base_dir(pid, dirfd)?) + }; + let path = CString::new(path).map_err(|_| libc::EINVAL)?; + crate::seccomp::notif::openat2_at( + base.as_ref().map_or(libc::AT_FDCWD, AsRawFd::as_raw_fd), + &path, + (libc::O_PATH | libc::O_CLOEXEC | if follow { 0 } else { libc::O_NOFOLLOW }) as u64, + 0, + crate::seccomp::notif::RESOLVE_NO_MAGICLINKS, + ) +} + +fn task_link(entry: &str) -> Option<(&str, &str)> { + let (link, tail) = entry.split_once('/').unwrap_or((entry, "")); + matches!(link, "cwd" | "exe" | "root").then_some((link, tail.trim_start_matches('/'))) +} + +// The task's own link is followed as the kernel would; the rest of the path +// still may not cross another magic link. +fn probe_through_task_link(pid: i32, link: &str, tail: &str, follow: bool) -> Result { + let nofollow = |last: bool| if last && !follow { libc::O_NOFOLLOW } else { 0 }; + let target = CString::new(format!("/proc/{pid}/{link}")).unwrap(); + let base = crate::seccomp::notif::openat2_at( + libc::AT_FDCWD, + &target, + (libc::O_PATH | libc::O_CLOEXEC | nofollow(tail.is_empty())) as u64, + 0, + 0, + )?; + if tail.is_empty() { + return Ok(base); + } + let tail = CString::new(tail).map_err(|_| libc::EINVAL)?; + crate::seccomp::notif::openat2_at( + base.as_raw_fd(), + &tail, + (libc::O_PATH | libc::O_CLOEXEC | nofollow(true)) as u64, + 0, + crate::seccomp::notif::RESOLVE_NO_MAGICLINKS, + ) +} + +// Walks the /proc// prefix as the kernel would and leaves the entry, +// and everything after it, as spelled. +fn proc_task_entry(path: &str, caller_tid: i32, caller_tgid: i32) -> Option<(i32, i32, &str)> { + let mut prefix = Vec::new(); + let mut rest = path.strip_prefix('/')?; + let (task, thread, entry) = loop { + let (component, tail) = rest.split_once('/').unwrap_or((rest, "")); + match (prefix.as_slice(), component) { + (_, "" | ".") => {} + (_, "..") => { + prefix.pop(); + } + (["proc", task], entry) if entry != "task" => break (*task, None, rest), + (["proc", task, "task", tid], _) => break (*task, Some(*tid), rest), + _ => prefix.push(component), + } + if tail.is_empty() { + return None; + } + rest = tail; + }; + let positive = |id: &str| id.parse::().ok().filter(|id| *id > 0); + let parent = match task { + "self" | "thread-self" => caller_tgid, + _ => positive(task)?, + }; + let pid = match thread { + Some(tid) => positive(tid)?, + None if task == "thread-self" => caller_tid, + None => parent, + }; + Some((parent, pid, entry)) +} + +pub(crate) async fn handle_pinned_metadata( + notif: &SeccompNotif, + ctx: &SupervisorCtx, + notif_fd: RawFd, +) -> NotifAction { + let Some(request) = Request::decode(notif) else { + return NotifAction::Continue; + }; + if notif.data.nr as i64 == libc::SYS_fstat && (request.dirfd as i32) < 0 { + return NotifAction::Errno(libc::EBADF); + } + let path = match read_child_cstr(notif_fd, notif.id, notif.pid, request.path, 4096) { + Some(path) => path, + None if request.path == 0 + && request.flags & libc::AT_EMPTY_PATH as u32 != 0 + && matches!(request.operation, Operation::Stat | Operation::Statx) => + { + String::new() + } + None => return NotifAction::Errno(libc::EFAULT), + }; + if ctx.policy.chroot_root.is_some() + && (!path.is_empty() || request.flags & libc::AT_EMPTY_PATH as u32 == 0) + { + return NotifAction::Continue; + } + let requires_directory = path.ends_with('/') || path.ends_with("/.") || path.ends_with("/.."); + let follow = request.flags & libc::AT_SYMLINK_NOFOLLOW as u32 == 0 || requires_directory; + let absolute = + resolve_to_normalized_absolute(notif.pid, request.dirfd, &path, None, &[], &ctx.processes); + let tgid = ctx + .processes + .tgid_of(notif.pid as i32) + .unwrap_or(notif.pid as i32); + let joined = super::joined_absolute(notif.pid, request.dirfd, &path, None, &[], &ctx.processes); + let task_entry = joined + .as_deref() + .and_then(Path::to_str) + .and_then(|joined| proc_task_entry(joined, notif.pid as i32, tgid)) + .filter(|(parent, pid, _)| { + ctx.processes.contains(*parent) + && ctx.processes.contains(*pid) + && (*parent == *pid || ctx.processes.tgid_of(*pid) == Some(*parent)) + }); + let held_fd = if path.is_empty() { + if request.flags & libc::AT_EMPTY_PATH as u32 == 0 + && !matches!(request.operation, Operation::Readlink) + { + return NotifAction::Errno(libc::ENOENT); + } + Some((notif.pid, request.dirfd as i32)) + } else if follow { + absolute + .as_deref() + .and_then(Path::to_str) + .and_then(|p| super::own_fd_request(p, notif.pid as i32, tgid)) + .map(|fd| (notif.pid, fd)) + .or_else(|| { + let (_, pid, entry) = task_entry?; + let fd = entry.strip_prefix("fd/")?; + if !fd.bytes().all(|byte| byte.is_ascii_digit()) { + return None; + } + Some((pid as u32, fd.parse::().ok()?)) + }) + } else { + None + }; + let mut translated = path.clone(); + for (prefix, target) in [ + ("/proc/self/", format!("/proc/{tgid}/")), + ( + "/proc/thread-self/", + format!("/proc/{tgid}/task/{}/", notif.pid), + ), + ] { + if let Some(rest) = path.strip_prefix(prefix) { + translated = format!("{target}{rest}"); + break; + } + } + let probe = match held_fd { + Some((pid, fd)) => crate::seccomp::notif::open_base_dir(pid, fd as i64), + None => match task_entry.and_then(|(_, pid, entry)| Some((pid, task_link(entry)?))) { + Some((pid, (link, tail))) => probe_through_task_link(pid, link, tail, follow), + None => probe_metadata(&translated, request.dirfd, notif.pid, follow), + }, + }; + let mut fd = match probe { + Ok(fd) => fd, + Err(errno) => return NotifAction::Errno(errno), + }; + let mut real = match std::fs::read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())) { + Ok(real) => real, + Err(_) => return errno_action(), + }; + if held_fd.is_none() { + if let Some(reprobe) = crate::seccomp::notif::reprobe_in_callers_proc( + &real, + notif.pid, + !follow, + &ctx.processes, + ) { + match reprobe { + Ok((callers, path)) => { + fd = callers; + real = path; + } + Err(errno) => return NotifAction::Errno(errno), + } + } + } + let resolved = real.to_string_lossy(); + if let Some(action) = net_metadata::serve_pinned( + &request, + &fd, + &resolved, + absolute.as_deref().and_then(Path::to_str).unwrap_or(&path), + !path.is_empty(), + requires_directory, + follow, + notif, + ctx, + notif_fd, + ) + .await + { + return action; + } + let empty = c""; + match request.operation { + Operation::Stat => { + if request.flags + & !(libc::AT_SYMLINK_NOFOLLOW | libc::AT_EMPTY_PATH | libc::AT_NO_AUTOMOUNT) as u32 + != 0 + { + return NotifAction::Errno(libc::EINVAL); + } + let mut st: libc::stat = unsafe { std::mem::zeroed() }; + if unsafe { libc::fstat(fd.as_raw_fd(), &mut st) } < 0 { + return errno_action(); + } + write_result(&st, request.output, notif, notif_fd) + } + Operation::Statx => { + let mut st: libc::statx = unsafe { std::mem::zeroed() }; + if unsafe { + libc::statx( + fd.as_raw_fd(), + empty.as_ptr(), + request.flags as i32 | libc::AT_EMPTY_PATH, + request.mode, + &mut st, + ) + } < 0 + { + return errno_action(); + } + write_result(&st, request.output, notif, notif_fd) + } + Operation::Access => { + if unsafe { + libc::syscall( + crate::arch::SYS_FACCESSAT2, + fd.as_raw_fd(), + empty.as_ptr(), + request.mode, + request.flags as i32 | libc::AT_EMPTY_PATH, + ) + } < 0 + { + return errno_action(); + } + NotifAction::ReturnValue(0) + } + Operation::Readlink => { + if request.size == 0 || request.size > i32::MAX as u64 { + return NotifAction::Errno(libc::EINVAL); + } + let mut target = vec![0u8; (request.size as usize).min(4096)]; + let count = unsafe { + libc::readlinkat( + fd.as_raw_fd(), + empty.as_ptr(), + target.as_mut_ptr().cast(), + target.len(), + ) + }; + if count < 0 { + return errno_action(); + } + if resolved == "/proc/self" { + target = tgid.to_string().into_bytes(); + } else if resolved == "/proc/thread-self" { + target = format!("{tgid}/task/{}", notif.pid).into_bytes(); + } else { + target.truncate(count as usize); + } + target.truncate(request.size as usize); + if write_child_mem(notif_fd, notif.id, notif.pid, request.output, &target).is_err() { + return NotifAction::Errno(libc::EFAULT); + } + NotifAction::ReturnValue(target.len() as i64) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn proc_task_entries_keep_peer_identity() { + assert_eq!( + proc_task_entry("/proc/42/fd/1", 9, 8), + Some((42, 42, "fd/1")) + ); + assert_eq!( + proc_task_entry("/proc/42/task/43/fd/1", 9, 8), + Some((42, 43, "fd/1")) + ); + assert_eq!( + proc_task_entry("/proc/thread-self/cwd", 9, 8), + Some((8, 9, "cwd")) + ); + assert_eq!( + proc_task_entry("/proc/self/task/9/exe", 9, 8), + Some((8, 9, "exe")) + ); + assert_eq!(proc_task_entry("/proc/-1/fd/1", 9, 8), None); + assert_eq!( + proc_task_entry("/proc//self/./exe", 9, 8), + Some((8, 8, "exe")) + ); + assert_eq!( + proc_task_entry("/proc/42/task/43/../../fd/1", 9, 8), + Some((42, 42, "fd/1")) + ); + assert_eq!( + proc_task_entry("/proc/self/cwd/a/../b/", 9, 8), + Some((8, 8, "cwd/a/../b/")) + ); + assert_eq!(proc_task_entry("/proc/self/", 9, 8), None); + } +} diff --git a/crates/sandlock-core/src/procfs/net_metadata.rs b/crates/sandlock-core/src/procfs/net_metadata.rs index 87ecaaf0..265cfb05 100644 --- a/crates/sandlock-core/src/procfs/net_metadata.rs +++ b/crates/sandlock-core/src/procfs/net_metadata.rs @@ -1,90 +1,12 @@ -use std::ffi::CString; use std::os::unix::io::{AsRawFd, OwnedFd, RawFd}; use std::path::{Path, PathBuf}; +use super::metadata::{write_result, Operation, Request}; use super::{canon_proc_namespace, net, resolve_to_normalized_absolute}; use crate::seccomp::ctx::SupervisorCtx; use crate::seccomp::notif::{read_child_cstr, write_child_mem, NotifAction}; use crate::sys::structs::SeccompNotif; -#[derive(Clone, Copy)] -enum Operation { - Stat, - Statx, - Access, - Readlink, -} - -struct Request { - operation: Operation, - dirfd: i64, - path: u64, - output: u64, - flags: u32, - mode: u32, - size: u64, -} - -impl Request { - fn decode(notif: &SeccompNotif) -> Option { - let nr = notif.data.nr as i64; - let a = notif.data.args; - let mut request = Self { - operation: Operation::Stat, - dirfd: a[0] as i64, - path: a[1], - output: a[2], - flags: 0, - mode: 0, - size: 0, - }; - if nr == libc::SYS_fstat { - request.path = 0; - request.output = a[1]; - request.flags = libc::AT_EMPTY_PATH as u32; - } else if nr == libc::SYS_newfstatat { - request.flags = a[3] as u32; - } else if nr == libc::SYS_statx { - request.operation = Operation::Statx; - request.flags = a[2] as u32; - request.mode = a[3] as u32; - request.output = a[4]; - } else if nr == libc::SYS_faccessat || nr == crate::arch::SYS_FACCESSAT2 { - request.operation = Operation::Access; - request.mode = a[2] as u32; - if nr == crate::arch::SYS_FACCESSAT2 { - request.flags = a[3] as u32; - } - } else if nr == libc::SYS_readlinkat { - request.operation = Operation::Readlink; - request.flags = libc::AT_SYMLINK_NOFOLLOW as u32; - request.size = a[3]; - } else { - #[cfg(target_arch = "x86_64")] - { - request.dirfd = libc::AT_FDCWD as i64; - request.path = a[0]; - request.output = a[1]; - if nr == libc::SYS_lstat { - request.flags = libc::AT_SYMLINK_NOFOLLOW as u32; - } else if nr == libc::SYS_access { - request.operation = Operation::Access; - request.mode = a[1] as u32; - } else if nr == libc::SYS_readlink { - request.operation = Operation::Readlink; - request.flags = libc::AT_SYMLINK_NOFOLLOW as u32; - request.size = a[2]; - } else if nr != libc::SYS_stat { - return None; - } - } - #[cfg(not(target_arch = "x86_64"))] - return None; - } - Some(request) - } -} - fn host_path(path: &Path, ctx: &SupervisorCtx) -> PathBuf { if let Some((virtual_path, host)) = ctx .policy @@ -190,16 +112,6 @@ fn metadata(entry: net::NetEntry, symlink: bool, created_at: std::time::Duration st } -fn write_result(value: &T, output: u64, notif: &SeccompNotif, notif_fd: RawFd) -> NotifAction { - let bytes = unsafe { - std::slice::from_raw_parts(value as *const T as *const u8, std::mem::size_of::()) - }; - match write_child_mem(notif_fd, notif.id, notif.pid, output, bytes) { - Ok(_) => NotifAction::ReturnValue(0), - Err(_) => NotifAction::Errno(libc::EFAULT), - } -} - pub(crate) async fn handle_net_metadata( notif: &SeccompNotif, ctx: &SupervisorCtx, @@ -352,94 +264,6 @@ async fn serve_metadata( } } -fn errno_action() -> NotifAction { - NotifAction::Errno( - std::io::Error::last_os_error() - .raw_os_error() - .unwrap_or(libc::EIO), - ) -} - -fn probe_metadata(path: &str, dirfd: i64, pid: u32, follow: bool) -> Result { - let base = if Path::new(path).is_absolute() { - None - } else { - Some(crate::seccomp::notif::open_base_dir(pid, dirfd)?) - }; - let path = CString::new(path).map_err(|_| libc::EINVAL)?; - crate::seccomp::notif::openat2_at( - base.as_ref().map_or(libc::AT_FDCWD, AsRawFd::as_raw_fd), - &path, - (libc::O_PATH | libc::O_CLOEXEC | if follow { 0 } else { libc::O_NOFOLLOW }) as u64, - 0, - crate::seccomp::notif::RESOLVE_NO_MAGICLINKS, - ) -} - -fn task_link(entry: &str) -> Option<(&str, &str)> { - let (link, tail) = entry.split_once('/').unwrap_or((entry, "")); - matches!(link, "cwd" | "exe" | "root").then_some((link, tail.trim_start_matches('/'))) -} - -// The task's own link is followed as the kernel would; the rest of the path -// still may not cross another magic link. -fn probe_through_task_link(pid: i32, link: &str, tail: &str, follow: bool) -> Result { - let nofollow = |last: bool| if last && !follow { libc::O_NOFOLLOW } else { 0 }; - let target = CString::new(format!("/proc/{pid}/{link}")).unwrap(); - let base = crate::seccomp::notif::openat2_at( - libc::AT_FDCWD, - &target, - (libc::O_PATH | libc::O_CLOEXEC | nofollow(tail.is_empty())) as u64, - 0, - 0, - )?; - if tail.is_empty() { - return Ok(base); - } - let tail = CString::new(tail).map_err(|_| libc::EINVAL)?; - crate::seccomp::notif::openat2_at( - base.as_raw_fd(), - &tail, - (libc::O_PATH | libc::O_CLOEXEC | nofollow(true)) as u64, - 0, - crate::seccomp::notif::RESOLVE_NO_MAGICLINKS, - ) -} - -// Walks the /proc// prefix as the kernel would and leaves the entry, -// and everything after it, as spelled. -fn proc_task_entry(path: &str, caller_tid: i32, caller_tgid: i32) -> Option<(i32, i32, &str)> { - let mut prefix = Vec::new(); - let mut rest = path.strip_prefix('/')?; - let (task, thread, entry) = loop { - let (component, tail) = rest.split_once('/').unwrap_or((rest, "")); - match (prefix.as_slice(), component) { - (_, "" | ".") => {} - (_, "..") => { - prefix.pop(); - } - (["proc", task], entry) if entry != "task" => break (*task, None, rest), - (["proc", task, "task", tid], _) => break (*task, Some(*tid), rest), - _ => prefix.push(component), - } - if tail.is_empty() { - return None; - } - rest = tail; - }; - let positive = |id: &str| id.parse::().ok().filter(|id| *id > 0); - let parent = match task { - "self" | "thread-self" => caller_tgid, - _ => positive(task)?, - }; - let pid = match thread { - Some(tid) => positive(tid)?, - None if task == "thread-self" => caller_tid, - None => parent, - }; - Some((parent, pid, entry)) -} - // The sealed snapshot name selects cosmetic metadata only, never access rights. fn named_net_file(path: &str) -> Option { let name = path @@ -450,122 +274,20 @@ fn named_net_file(path: &str) -> Option { .find_map(|(entry, file)| (*entry == name).then_some(*file)) } -pub(crate) async fn handle_pinned_metadata( +pub(super) async fn serve_pinned( + request: &Request, + fd: &OwnedFd, + resolved: &str, + original: &str, + check_access: bool, + requires_directory: bool, + follow: bool, notif: &SeccompNotif, ctx: &SupervisorCtx, notif_fd: RawFd, -) -> NotifAction { - let Some(request) = Request::decode(notif) else { - return NotifAction::Continue; - }; - if notif.data.nr as i64 == libc::SYS_fstat && (request.dirfd as i32) < 0 { - return NotifAction::Errno(libc::EBADF); - } - let path = match read_child_cstr(notif_fd, notif.id, notif.pid, request.path, 4096) { - Some(path) => path, - None if request.path == 0 - && request.flags & libc::AT_EMPTY_PATH as u32 != 0 - && matches!(request.operation, Operation::Stat | Operation::Statx) => - { - String::new() - } - None => return NotifAction::Errno(libc::EFAULT), - }; - if ctx.policy.chroot_root.is_some() - && (!path.is_empty() || request.flags & libc::AT_EMPTY_PATH as u32 == 0) - { - return NotifAction::Continue; - } - let requires_directory = path.ends_with('/') || path.ends_with("/.") || path.ends_with("/.."); - let follow = request.flags & libc::AT_SYMLINK_NOFOLLOW as u32 == 0 || requires_directory; - let absolute = - resolve_to_normalized_absolute(notif.pid, request.dirfd, &path, None, &[], &ctx.processes); - let tgid = ctx - .processes - .tgid_of(notif.pid as i32) - .unwrap_or(notif.pid as i32); - let joined = - super::joined_absolute(notif.pid, request.dirfd, &path, None, &[], &ctx.processes); - let task_entry = joined - .as_deref() - .and_then(Path::to_str) - .and_then(|joined| proc_task_entry(joined, notif.pid as i32, tgid)) - .filter(|(parent, pid, _)| { - ctx.processes.contains(*parent) - && ctx.processes.contains(*pid) - && (*parent == *pid || ctx.processes.tgid_of(*pid) == Some(*parent)) - }); - let held_fd = if path.is_empty() { - if request.flags & libc::AT_EMPTY_PATH as u32 == 0 - && !matches!(request.operation, Operation::Readlink) - { - return NotifAction::Errno(libc::ENOENT); - } - Some((notif.pid, request.dirfd as i32)) - } else if follow { - absolute - .as_deref() - .and_then(Path::to_str) - .and_then(|p| super::own_fd_request(p, notif.pid as i32, tgid)) - .map(|fd| (notif.pid, fd)) - .or_else(|| { - let (_, pid, entry) = task_entry?; - let fd = entry.strip_prefix("fd/")?; - if !fd.bytes().all(|byte| byte.is_ascii_digit()) { - return None; - } - Some((pid as u32, fd.parse::().ok()?)) - }) - } else { - None - }; - let mut translated = path.clone(); - for (prefix, target) in [ - ("/proc/self/", format!("/proc/{tgid}/")), - ( - "/proc/thread-self/", - format!("/proc/{tgid}/task/{}/", notif.pid), - ), - ] { - if let Some(rest) = path.strip_prefix(prefix) { - translated = format!("{target}{rest}"); - break; - } - } - let probe = match held_fd { - Some((pid, fd)) => crate::seccomp::notif::open_base_dir(pid, fd as i64), - None => match task_entry.and_then(|(_, pid, entry)| Some((pid, task_link(entry)?))) { - Some((pid, (link, tail))) => probe_through_task_link(pid, link, tail, follow), - None => probe_metadata(&translated, request.dirfd, notif.pid, follow), - }, - }; - let mut fd = match probe { - Ok(fd) => fd, - Err(errno) => return NotifAction::Errno(errno), - }; - let mut real = match std::fs::read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())) { - Ok(real) => real, - Err(_) => return errno_action(), - }; - if held_fd.is_none() { - if let Some(reprobe) = crate::seccomp::notif::reprobe_in_callers_proc( - &real, - notif.pid, - !follow, - &ctx.processes, - ) { - match reprobe { - Ok((callers, path)) => { - fd = callers; - real = path; - } - Err(errno) => return NotifAction::Errno(errno), - } - } - } - let resolved = real.to_string_lossy(); +) -> Option { if matches!(request.operation, Operation::Stat | Operation::Statx) { - if let Some(file) = named_net_file(&resolved) { + if let Some(file) = named_net_file(resolved) { let seals = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GET_SEALS) }; let required = libc::F_SEAL_SEAL | libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK; @@ -575,115 +297,45 @@ pub(crate) async fn handle_pinned_metadata( .find(|(_, entry)| *entry == file) .unwrap() .0; - return serve_metadata( - &request, - "", - &format!("/proc/net/{name}"), - requires_directory, - follow, - false, - notif, - ctx, - notif_fd, - ) - .await; + return Some( + serve_metadata( + request, + "", + &format!("/proc/net/{name}"), + requires_directory, + follow, + false, + notif, + ctx, + notif_fd, + ) + .await, + ); } } } - if net::lookup(&canon_proc_namespace(&resolved)) != net::NetEntry::Outside { - return serve_metadata( - &request, - absolute.as_deref().and_then(Path::to_str).unwrap_or(&path), - &resolved, - requires_directory, - follow, - !path.is_empty(), - notif, - ctx, - notif_fd, - ) - .await; - } - let empty = c""; - match request.operation { - Operation::Stat => { - if request.flags - & !(libc::AT_SYMLINK_NOFOLLOW | libc::AT_EMPTY_PATH | libc::AT_NO_AUTOMOUNT) as u32 - != 0 - { - return NotifAction::Errno(libc::EINVAL); - } - let mut st: libc::stat = unsafe { std::mem::zeroed() }; - if unsafe { libc::fstat(fd.as_raw_fd(), &mut st) } < 0 { - return errno_action(); - } - write_result(&st, request.output, notif, notif_fd) - } - Operation::Statx => { - let mut st: libc::statx = unsafe { std::mem::zeroed() }; - if unsafe { - libc::statx( - fd.as_raw_fd(), - empty.as_ptr(), - request.flags as i32 | libc::AT_EMPTY_PATH, - request.mode, - &mut st, - ) - } < 0 - { - return errno_action(); - } - write_result(&st, request.output, notif, notif_fd) - } - Operation::Access => { - if unsafe { - libc::syscall( - crate::arch::SYS_FACCESSAT2, - fd.as_raw_fd(), - empty.as_ptr(), - request.mode, - request.flags as i32 | libc::AT_EMPTY_PATH, - ) - } < 0 - { - return errno_action(); - } - NotifAction::ReturnValue(0) - } - Operation::Readlink => { - if request.size == 0 || request.size > i32::MAX as u64 { - return NotifAction::Errno(libc::EINVAL); - } - let mut target = vec![0u8; (request.size as usize).min(4096)]; - let count = unsafe { - libc::readlinkat( - fd.as_raw_fd(), - empty.as_ptr(), - target.as_mut_ptr().cast(), - target.len(), - ) - }; - if count < 0 { - return errno_action(); - } - if resolved == "/proc/self" { - target = tgid.to_string().into_bytes(); - } else if resolved == "/proc/thread-self" { - target = format!("{tgid}/task/{}", notif.pid).into_bytes(); - } else { - target.truncate(count as usize); - } - target.truncate(request.size as usize); - if write_child_mem(notif_fd, notif.id, notif.pid, request.output, &target).is_err() { - return NotifAction::Errno(libc::EFAULT); - } - NotifAction::ReturnValue(target.len() as i64) - } + if net::lookup(&canon_proc_namespace(resolved)) != net::NetEntry::Outside { + return Some( + serve_metadata( + request, + original, + resolved, + requires_directory, + follow, + check_access, + notif, + ctx, + notif_fd, + ) + .await, + ); } + None } #[cfg(test)] mod tests { + use super::super::metadata::probe_metadata; use super::*; #[test] @@ -701,37 +353,6 @@ mod tests { assert_eq!(named_net_file("/tmp/sandlock-proc-net-tcp (deleted)"), None); } - #[test] - fn proc_task_entries_keep_peer_identity() { - assert_eq!( - proc_task_entry("/proc/42/fd/1", 9, 8), - Some((42, 42, "fd/1")) - ); - assert_eq!( - proc_task_entry("/proc/42/task/43/fd/1", 9, 8), - Some((42, 43, "fd/1")) - ); - assert_eq!( - proc_task_entry("/proc/thread-self/cwd", 9, 8), - Some((8, 9, "cwd")) - ); - assert_eq!( - proc_task_entry("/proc/self/task/9/exe", 9, 8), - Some((8, 9, "exe")) - ); - assert_eq!(proc_task_entry("/proc/-1/fd/1", 9, 8), None); - assert_eq!(proc_task_entry("/proc//self/./exe", 9, 8), Some((8, 8, "exe"))); - assert_eq!( - proc_task_entry("/proc/42/task/43/../../fd/1", 9, 8), - Some((42, 42, "fd/1")) - ); - assert_eq!( - proc_task_entry("/proc/self/cwd/a/../b/", 9, 8), - Some((8, 8, "cwd/a/../b/")) - ); - assert_eq!(proc_task_entry("/proc/self/", 9, 8), None); - } - #[test] fn pinned_outside_alias_stays_in_closed_network_catalog() { let directory =