diff --git a/Cargo.lock b/Cargo.lock index 8b19700ea0..571d80e7b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1689,6 +1689,9 @@ dependencies = [ [[package]] name = "fspy_test_bin" version = "0.0.0" +dependencies = [ + "nix 0.30.1", +] [[package]] name = "fspy_test_utils" diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index b78b29fabd..bd2cc02759 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -19,6 +19,7 @@ use fspy_shared_unix::{ use futures_util::FutureExt; #[cfg(target_os = "linux")] use syscall_handler::SyscallHandler; +use tokio::task::spawn_blocking; use crate::{ Command, TrackedChild, @@ -124,15 +125,16 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result }); } - let child = tokio_command.spawn()?; - - drop(tokio_command); + // tokio_command.spawn blocks while executing the `pre_exec` closure. + // Run it inside spawn_blocking to avoid blocking the tokio runtime, especially the supervisor loop, + // which needs to accept incoming connections while `pre_exec` is connecting to it. + let child = spawn_blocking(move || tokio_command.spawn()).await??; let arenas_future = async move { let arenas = std::iter::once(exec_resolve_accesses); #[cfg(target_os = "linux")] let arenas = - arenas.chain(supervisor.stop().await?.into_iter().map(|handler| handler.arena)); + arenas.chain(supervisor.stop().await?.into_iter().map(|handler| handler.into_arena())); io::Result::Ok(arenas.collect::>()) }; diff --git a/crates/fspy/src/unix/syscall_handler/execve.rs b/crates/fspy/src/unix/syscall_handler/execve.rs new file mode 100644 index 0000000000..d34ac8c2df --- /dev/null +++ b/crates/fspy/src/unix/syscall_handler/execve.rs @@ -0,0 +1,24 @@ +use std::io; + +use fspy_seccomp_unotify::supervisor::handler::arg::{CStrPtr, Caller, Fd}; + +use super::SyscallHandler; + +impl SyscallHandler { + fn handle_execve(&mut self, caller: Caller, fd: Fd, path_ptr: CStrPtr) -> io::Result<()> { + // TODO: parse shebangs to track reading interpreters + self.handle_open(caller, fd, path_ptr, libc::O_RDONLY) + } + + pub(super) fn execveat( + &mut self, + caller: Caller, + (fd, path_ptr): (Fd, CStrPtr), + ) -> io::Result<()> { + self.handle_execve(caller, fd, path_ptr) + } + + pub(super) fn execve(&mut self, caller: Caller, (path_ptr,): (CStrPtr,)) -> io::Result<()> { + self.handle_execve(caller, Fd::cwd(), path_ptr) + } +} diff --git a/crates/fspy/src/unix/syscall_handler/getdents.rs b/crates/fspy/src/unix/syscall_handler/getdents.rs new file mode 100644 index 0000000000..45eec5320a --- /dev/null +++ b/crates/fspy/src/unix/syscall_handler/getdents.rs @@ -0,0 +1,16 @@ +use std::io; + +use fspy_seccomp_unotify::supervisor::handler::arg::{Caller, Fd}; + +use super::SyscallHandler; + +impl SyscallHandler { + #[cfg(target_arch = "x86_64")] + pub(super) fn getdents(&mut self, caller: Caller, (fd,): (Fd,)) -> io::Result<()> { + self.handle_open_dir(caller, fd) + } + + pub(super) fn getdents64(&mut self, caller: Caller, (fd,): (Fd,)) -> io::Result<()> { + self.handle_open_dir(caller, fd) + } +} diff --git a/crates/fspy/src/unix/syscall_handler/mod.rs b/crates/fspy/src/unix/syscall_handler/mod.rs index 81622eb322..d06580bea0 100644 --- a/crates/fspy/src/unix/syscall_handler/mod.rs +++ b/crates/fspy/src/unix/syscall_handler/mod.rs @@ -1,45 +1,76 @@ -use std::{io, os::unix::ffi::OsStrExt}; +mod execve; +mod getdents; +mod open; +mod stat; + +use std::{ + borrow::Cow, + ffi::{OsStr, c_int}, + io, + os::unix::ffi::OsStrExt, + path::{Path, PathBuf}, +}; use fspy_seccomp_unotify::{ impl_handler, - supervisor::handler::arg::{CStrPtr, Fd, Ignored}, + supervisor::handler::arg::{CStrPtr, Caller, Fd}, }; use fspy_shared::ipc::{AccessMode, NativeStr, PathAccess}; +use nix::NixPath; use crate::arena::PathAccessArena; const PATH_MAX: usize = libc::PATH_MAX as usize; -#[derive(Default, Debug)] +#[derive(Debug)] pub struct SyscallHandler { - pub(crate) arena: PathAccessArena, + arena: PathAccessArena, + path_read_buf: [u8; PATH_MAX], } -impl SyscallHandler { - fn handle_open(&mut self, path: CStrPtr) -> io::Result<()> { - path.read_with_buf::(|path| { - let Some(path) = path else { - // Ignore paths that are too long to fit in PATH_MAX - return Ok(()); - }; - self.arena - .add(PathAccess { mode: AccessMode::Read, path: NativeStr::from_bytes(path) }); - Ok(()) - })?; - Ok(()) +impl Default for SyscallHandler { + fn default() -> Self { + Self { arena: PathAccessArena::default(), path_read_buf: [0; PATH_MAX] } } +} - #[cfg(target_arch = "x86_64")] - fn open(&mut self, (path,): (CStrPtr,)) -> io::Result<()> { - self.handle_open(path) +impl SyscallHandler { + pub fn into_arena(self) -> PathAccessArena { + self.arena } - fn openat(&mut self, (_, path): (Ignored, CStrPtr)) -> io::Result<()> { - self.handle_open(path) + fn handle_open( + &mut self, + caller: Caller, + dir_fd: Fd, + path_ptr: CStrPtr, + flags: c_int, + ) -> io::Result<()> { + let Some(path_len) = path_ptr.read(caller, &mut self.path_read_buf)? else { + // Ignore paths that are too long to fit in PATH_MAX + return Ok(()); + }; + let mut path = Cow::Borrowed(Path::new(OsStr::from_bytes(&self.path_read_buf[..path_len]))); + if !path.is_absolute() { + let mut resolved_path = PathBuf::from(dir_fd.get_path(caller)?); + if !path.is_empty() { + resolved_path.push(&path); + } + path = Cow::Owned(resolved_path); + } + self.arena.add(PathAccess { + mode: match flags & libc::O_ACCMODE { + libc::O_RDWR => AccessMode::ReadWrite, + libc::O_WRONLY => AccessMode::Write, + _ => AccessMode::Read, + }, + path: NativeStr::from_bytes(path.as_os_str().as_bytes()), + }); + Ok(()) } - fn getdents64(&mut self, (fd,): (Fd,)) -> io::Result<()> { - let path = fd.get_path()?; + fn handle_open_dir(&mut self, caller: Caller, fd: Fd) -> io::Result<()> { + let path = fd.get_path(caller)?; self.arena.add(PathAccess { mode: AccessMode::ReadDir, path: NativeStr::from_bytes(path.as_bytes()), @@ -50,7 +81,19 @@ impl SyscallHandler { impl_handler!( SyscallHandler: + #[cfg(target_arch = "x86_64")] open, openat, + openat2, + + #[cfg(target_arch = "x86_64")] getdents, getdents64, + + #[cfg(target_arch = "x86_64")] stat, + #[cfg(target_arch = "x86_64")] lstat, + #[cfg(target_arch = "x86_64")] newfstatat, + #[cfg(target_arch = "aarch64")] fstatat, + + execve, + execveat, ); diff --git a/crates/fspy/src/unix/syscall_handler/open.rs b/crates/fspy/src/unix/syscall_handler/open.rs new file mode 100644 index 0000000000..be7ae157ed --- /dev/null +++ b/crates/fspy/src/unix/syscall_handler/open.rs @@ -0,0 +1,35 @@ +use std::{ffi::c_int, io}; + +use fspy_seccomp_unotify::supervisor::handler::arg::{CStrPtr, Caller, Fd, Ptr}; + +use super::SyscallHandler; + +impl SyscallHandler { + #[cfg(target_arch = "x86_64")] + pub(super) fn open( + &mut self, + caller: Caller, + (path, flags): (CStrPtr, c_int), + ) -> io::Result<()> { + self.handle_open(caller, Fd::cwd(), path, flags) + } + + pub(super) fn openat( + &mut self, + caller: Caller, + (dir_fd, path, flags): (Fd, CStrPtr, c_int), + ) -> io::Result<()> { + self.handle_open(caller, dir_fd, path, flags) + } + + pub(super) fn openat2( + &mut self, + caller: Caller, + // open_how is a pointer to struct `open_how`, but we only care about flags here, so use `Ptr` + (dir_fd, path, open_how): (Fd, CStrPtr, Ptr), + ) -> io::Result<()> { + // SAFETY: open_how is a valid pointer to struct `open_how` in the target process, which has `flags` as the first field of type `u64` + let flags = unsafe { open_how.read(caller) }?; + self.handle_open(caller, dir_fd, path, c_int::try_from(flags).unwrap_or(libc::O_RDWR)) + } +} diff --git a/crates/fspy/src/unix/syscall_handler/stat.rs b/crates/fspy/src/unix/syscall_handler/stat.rs new file mode 100644 index 0000000000..ae7da89381 --- /dev/null +++ b/crates/fspy/src/unix/syscall_handler/stat.rs @@ -0,0 +1,35 @@ +use std::io; + +use fspy_seccomp_unotify::supervisor::handler::arg::{CStrPtr, Caller, Fd}; + +use super::SyscallHandler; + +impl SyscallHandler { + #[cfg(target_arch = "x86_64")] + pub(super) fn stat(&mut self, caller: Caller, (path,): (CStrPtr,)) -> io::Result<()> { + self.handle_open(caller, Fd::cwd(), path, libc::O_RDONLY) + } + + #[cfg(target_arch = "x86_64")] + pub(super) fn lstat(&mut self, caller: Caller, (path,): (CStrPtr,)) -> io::Result<()> { + self.handle_open(caller, Fd::cwd(), path, libc::O_RDONLY) + } + + #[cfg(target_arch = "aarch64")] + pub(super) fn fstatat( + &mut self, + caller: Caller, + (dir_fd, path_ptr): (Fd, CStrPtr), + ) -> io::Result<()> { + self.handle_open(caller, dir_fd, path_ptr, libc::O_RDONLY) + } + + #[cfg(target_arch = "x86_64")] + pub(super) fn newfstatat( + &mut self, + caller: Caller, + (dir_fd, path_ptr): (Fd, CStrPtr), + ) -> io::Result<()> { + self.handle_open(caller, dir_fd, path_ptr, libc::O_RDONLY) + } +} diff --git a/crates/fspy/tests/static_executable.rs b/crates/fspy/tests/static_executable.rs index 24ebb54fc9..4d3709c4bf 100644 --- a/crates/fspy/tests/static_executable.rs +++ b/crates/fspy/tests/static_executable.rs @@ -35,8 +35,11 @@ fn test_bin_path() -> &'static Path { TEST_BIN_PATH.as_path() } -async fn track_test_bin(args: &[&str]) -> PathAccessIterable { +async fn track_test_bin(args: &[&str], cwd: Option<&str>) -> PathAccessIterable { let mut cmd = fspy::Spy::global().unwrap().new_command(test_bin_path()); + if let Some(cwd) = cwd { + cmd.current_dir(cwd); + }; cmd.args(args); let mut tracked_child = cmd.spawn().await.unwrap(); @@ -48,6 +51,60 @@ async fn track_test_bin(args: &[&str]) -> PathAccessIterable { #[tokio::test] async fn open_read() { - let accesses = track_test_bin(&["open_read", "/hello"]).await; + let accesses = track_test_bin(&["open_read", "/hello"], None).await; + assert_contains(&accesses, Path::new("/hello"), fspy::AccessMode::Read); +} + +#[tokio::test] +async fn open_write() { + let accesses = track_test_bin(&["open_write", "/hello"], None).await; + assert_contains(&accesses, Path::new("/hello"), fspy::AccessMode::Write); +} + +#[tokio::test] +async fn open_readwrite() { + let accesses = track_test_bin(&["open_readwrite", "/hello"], None).await; + assert_contains(&accesses, Path::new("/hello"), fspy::AccessMode::ReadWrite); +} + +#[tokio::test] +async fn openat2_read() { + let accesses = track_test_bin(&["openat2_read", "/hello"], None).await; + assert_contains(&accesses, Path::new("/hello"), fspy::AccessMode::Read); +} + +#[tokio::test] +async fn openat2_write() { + let accesses = track_test_bin(&["openat2_write", "/hello"], None).await; + assert_contains(&accesses, Path::new("/hello"), fspy::AccessMode::Write); +} + +#[tokio::test] +async fn openat2_readwrite() { + let accesses = track_test_bin(&["openat2_readwrite", "/hello"], None).await; + assert_contains(&accesses, Path::new("/hello"), fspy::AccessMode::ReadWrite); +} + +#[tokio::test] +async fn open_relative() { + let accesses = track_test_bin(&["open_read", "hello"], Some("/home")).await; + assert_contains(&accesses, Path::new("/home/hello"), fspy::AccessMode::Read); +} + +#[tokio::test] +async fn readdir() { + let accesses = track_test_bin(&["readdir", "/home"], None).await; + assert_contains(&accesses, Path::new("/home"), fspy::AccessMode::ReadDir); +} + +#[tokio::test] +async fn stat() { + let accesses = track_test_bin(&["stat", "/hello"], None).await; + assert_contains(&accesses, Path::new("/hello"), fspy::AccessMode::Read); +} + +#[tokio::test] +async fn execve() { + let accesses = track_test_bin(&["execve", "/hello"], None).await; assert_contains(&accesses, Path::new("/hello"), fspy::AccessMode::Read); } diff --git a/crates/fspy_seccomp_unotify/src/supervisor/handler/arg.rs b/crates/fspy_seccomp_unotify/src/supervisor/handler/arg.rs index 5703695eec..f4431f02ee 100644 --- a/crates/fspy_seccomp_unotify/src/supervisor/handler/arg.rs +++ b/crates/fspy_seccomp_unotify/src/supervisor/handler/arg.rs @@ -1,138 +1,188 @@ use std::{ - ffi::OsString, - io, - mem::{MaybeUninit, transmute}, + ffi::{OsString, c_int}, + io::{self, IoSliceMut, Read}, + marker::PhantomData, + mem::MaybeUninit, os::{fd::RawFd, raw::c_void}, }; -use bytes::BufMut; use libc::{pid_t, seccomp_notif}; -use tokio::io::ReadBuf; +use nix::sys::uio::{RemoteIoVec, process_vm_readv}; pub trait FromSyscallArg: Sized { - fn from_syscall_arg(pid: u32, arg: u64) -> io::Result; + fn from_syscall_arg(arg: u64) -> io::Result; +} +/// Represents the caller of a syscall. Needed to read memory from the caller's address space. +#[derive(Debug, Clone, Copy)] +pub struct Caller<'a> { + pid: pid_t, + _marker: std::marker::PhantomData<&'a ()>, } -#[derive(Debug)] +impl<'a> Caller<'a> { + /// Creates a `Caller` for the given pid with a local lifetime. + #[doc(hidden)] // only exposed for `impl_handler` macro + pub fn with_pid) -> R>(pid: pid_t, f: F) -> R { + f(Self { pid, _marker: std::marker::PhantomData }) + } + + pub fn read_vm(self, starting_addr: usize) -> ProcessVmReader<'a> { + ProcessVmReader { caller: self, current_addr: starting_addr } + } +} + +pub struct ProcessVmReader<'a> { + caller: Caller<'a>, + current_addr: usize, +} + +impl<'a> io::Read for ProcessVmReader<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let buf_len = buf.len(); + let read_len = process_vm_readv( + nix::unistd::Pid::from_raw(self.caller.pid), + &mut [IoSliceMut::new(buf)], + &[RemoteIoVec { base: self.current_addr, len: buf_len }], + )?; + self.current_addr = self.current_addr.checked_add(read_len).ok_or_else(|| { + io::Error::new(io::ErrorKind::Other, "address overflow while reading remote process") + })?; + Ok(read_len) + } +} + +#[derive(Debug, Clone, Copy)] pub struct CStrPtr { - pid: pid_t, - remote_ptr: *mut c_void, + remote_ptr: usize, } impl CStrPtr { // Reads the C string from the remote process into the provided buffer. - // Returns whether the read was successful or not because the buffer was filled before a null-terminator was found. - pub fn read(&self, buf: &mut B) -> io::Result { - loop { - let chunk = buf.chunk_mut(); - if chunk.len() == 0 { - return Ok(false); + // Returns: + /// - `Ok(Some(n))` if a null-terminator was found at position n of the buffer, + /// - `Ok(None)` if the buffer was filled without encountering a null-terminator. + /// - `Err(UnexpectedEof)` if Eof was reached without encountering a null-terminator. + /// - `Err(other_err)` on other errors from reading the remote process memory. + pub fn read(&self, caller: Caller<'_>, buf: &mut [u8]) -> io::Result> { + let mut reader = caller.read_vm(self.remote_ptr); + let mut pos = 0; + while let Some((_, unfilled)) = buf.split_at_mut_checked(pos) { + if unfilled.is_empty() { + break; } - - let local_iov = - libc::iovec { iov_base: chunk.as_mut_ptr().cast(), iov_len: chunk.len() }; - - let remote_iov = libc::iovec { iov_base: self.remote_ptr, iov_len: chunk.len() }; - - let read_size = - unsafe { libc::process_vm_readv(self.pid, &local_iov, 1, &remote_iov, 1, 0) }; - - let Ok(read_size) = usize::try_from(read_size) else { - return Err(io::Error::last_os_error()); - }; - - // chunk[..read_size] are all initialized, but we are only going to advance until '\0' - let chunk = unsafe { - transmute::<&[MaybeUninit], &[u8]>(&chunk.as_uninit_slice_mut()[..read_size]) - }; - let Some(nul_index) = chunk.iter().position(|byte| *byte == b'\0') else { - // No '\0' found, could be a partial read, advance all of `read_size` and continue reading. - unsafe { buf.advance_mut(read_size) }; - continue; - }; - unsafe { buf.advance_mut(nul_index) }; - return Ok(true); + let read_bytes = reader.read(unfilled)?; + if read_bytes == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "reached EOF while reading C string from remote process", + )); + } + if let Some(null_pos) = unfilled[..read_bytes].iter().position(|&b| b == 0) { + return Ok(Some(pos + null_pos)); + } + pos += read_bytes; } + Ok(None) } +} - // Reads the C string from the remote process into a fixed-size buffer. - // The closure is called with `Some(&[u8])` if a null-terminator was found within the buffer size, - // or `None` if the buffer was filled without encountering a null-terminator. - pub fn read_with_buf) -> io::Result>( - &self, - f: F, - ) -> io::Result { - let mut read_buf: [MaybeUninit; BUF_SIZE] = [const { MaybeUninit::uninit() }; BUF_SIZE]; - let mut read_buf = ReadBuf::uninit(read_buf.as_mut_slice()); - let success = self.read(&mut read_buf)?; - f(if success { Some(read_buf.filled()) } else { None }) +impl FromSyscallArg for CStrPtr { + fn from_syscall_arg(arg: u64) -> io::Result { + Ok(Self { remote_ptr: arg as _ }) } } -impl FromSyscallArg for CStrPtr { - fn from_syscall_arg(pid: u32, arg: u64) -> io::Result { - Ok(Self { pid: pid as _, remote_ptr: arg as _ }) +pub struct Ptr { + remote_ptr: *mut c_void, + _marker: PhantomData, +} +impl FromSyscallArg for Ptr { + fn from_syscall_arg(arg: u64) -> io::Result { + Ok(Self { remote_ptr: arg as _, _marker: PhantomData }) + } +} +impl Ptr { + /// Reads the value of type T from the remote process memory. + /// # Safety: + /// The remote pointer must be valid and point to a value of type T in the remote process memory. + pub unsafe fn read(&self, caller: Caller<'_>) -> io::Result { + let mut reader = caller.read_vm(self.remote_ptr as usize); + let mut buf = MaybeUninit::::zeroed(); + let buf_slice = unsafe { + std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, std::mem::size_of::()) + }; + reader.read_exact(buf_slice)?; + Ok(unsafe { buf.assume_init() }) } } #[derive(Debug)] pub struct Ignored(()); impl FromSyscallArg for Ignored { - fn from_syscall_arg(_pid: u32, _arg: u64) -> io::Result { + fn from_syscall_arg(_arg: u64) -> io::Result { Ok(Ignored(())) } } #[derive(Debug)] pub struct Fd { - pid: u32, fd: RawFd, } + +impl Fd { + pub fn cwd() -> Self { + Self { fd: libc::AT_FDCWD } + } +} + impl FromSyscallArg for Fd { - fn from_syscall_arg(pid: u32, arg: u64) -> io::Result { - Ok(Self { pid, fd: arg as _ }) + fn from_syscall_arg(arg: u64) -> io::Result { + Ok(Self { fd: arg as _ }) } } impl Fd { // TODO: allocate in arena - pub fn get_path(&self) -> nix::Result { + pub fn get_path(&self, caller: Caller<'_>) -> nix::Result { nix::fcntl::readlink( if self.fd == libc::AT_FDCWD { - format!("/proc/{}/cwd", self.pid) + format!("/proc/{}/cwd", caller.pid) } else { - format!("/proc/{}/fd/{}", self.pid, self.fd) + format!("/proc/{}/fd/{}", caller.pid, self.fd) } .as_str(), ) } } +impl FromSyscallArg for c_int { + fn from_syscall_arg(arg: u64) -> io::Result { + Ok(arg as _) + } +} + pub trait FromNotify: Sized { fn from_notify(notif: &seccomp_notif) -> io::Result; } impl FromNotify for (T,) { fn from_notify(notif: &seccomp_notif) -> io::Result { - Ok((T::from_syscall_arg(notif.pid, notif.data.args[0])?,)) + Ok((T::from_syscall_arg(notif.data.args[0])?,)) } } impl FromNotify for (T1, T2) { fn from_notify(notif: &seccomp_notif) -> io::Result { - Ok(( - T1::from_syscall_arg(notif.pid, notif.data.args[0])?, - T2::from_syscall_arg(notif.pid, notif.data.args[1])?, - )) + Ok((T1::from_syscall_arg(notif.data.args[0])?, T2::from_syscall_arg(notif.data.args[1])?)) } } impl FromNotify for (T1, T2, T3) { fn from_notify(notif: &seccomp_notif) -> io::Result { Ok(( - T1::from_syscall_arg(notif.pid, notif.data.args[0])?, - T2::from_syscall_arg(notif.pid, notif.data.args[1])?, - T3::from_syscall_arg(notif.pid, notif.data.args[2])?, + T1::from_syscall_arg(notif.data.args[0])?, + T2::from_syscall_arg(notif.data.args[1])?, + T3::from_syscall_arg(notif.data.args[2])?, )) } } @@ -142,10 +192,10 @@ impl io::Result { Ok(( - T1::from_syscall_arg(notif.pid, notif.data.args[0])?, - T2::from_syscall_arg(notif.pid, notif.data.args[1])?, - T3::from_syscall_arg(notif.pid, notif.data.args[2])?, - T4::from_syscall_arg(notif.pid, notif.data.args[3])?, + T1::from_syscall_arg(notif.data.args[0])?, + T2::from_syscall_arg(notif.data.args[1])?, + T3::from_syscall_arg(notif.data.args[2])?, + T4::from_syscall_arg(notif.data.args[3])?, )) } } diff --git a/crates/fspy_seccomp_unotify/src/supervisor/handler/mod.rs b/crates/fspy_seccomp_unotify/src/supervisor/handler/mod.rs index 737fecd4e4..d741a70c6d 100644 --- a/crates/fspy_seccomp_unotify/src/supervisor/handler/mod.rs +++ b/crates/fspy_seccomp_unotify/src/supervisor/handler/mod.rs @@ -24,13 +24,15 @@ macro_rules! impl_handler { ),* ] } fn handle_notify(&mut self, notify: &::libc::seccomp_notif) -> ::std::io::Result<()> { - $( - $(#[$attr])? - if notify.data.nr == ::syscalls::Sysno::$syscall as _ { - return self.$syscall($crate::supervisor::handler::arg::FromNotify::from_notify(notify)?) - } - )* - Ok(()) + $crate::supervisor::handler::arg::Caller::with_pid(notify.pid as _, |caller| { + $( + $(#[$attr])? + if notify.data.nr == ::syscalls::Sysno::$syscall as _ { + return self.$syscall(caller, $crate::supervisor::handler::arg::FromNotify::from_notify(notify)?) + } + )* + Ok(()) + }) } } }; diff --git a/crates/fspy_seccomp_unotify/src/supervisor/mod.rs b/crates/fspy_seccomp_unotify/src/supervisor/mod.rs index dff4319780..846f22211e 100644 --- a/crates/fspy_seccomp_unotify/src/supervisor/mod.rs +++ b/crates/fspy_seccomp_unotify/src/supervisor/mod.rs @@ -96,11 +96,11 @@ pub fn supervise() -> io::Re join_set.spawn(async move { while let Some(notify) = listener.next().await? { let _span = span!(Level::TRACE, "notify loop tick"); - // Errors on the supervisor side shouldn't block the syscall. - let handle_result = handler.handle_notify(notify); + // Errors on the supervisor side could be caused by a target process aborting. + // It shouldn't break the syscall handling loop as there might be target processes. + let _handle_result = handler.handle_notify(notify); let notify_id = notify.id; listener.send_continue(notify_id, &mut resp_buf)?; - handle_result?; } io::Result::Ok(handler) }); diff --git a/crates/fspy_seccomp_unotify/tests/arg_types.rs b/crates/fspy_seccomp_unotify/tests/arg_types.rs index 74f3ffd369..8d75f2baae 100644 --- a/crates/fspy_seccomp_unotify/tests/arg_types.rs +++ b/crates/fspy_seccomp_unotify/tests/arg_types.rs @@ -13,7 +13,7 @@ use assertables::assert_contains; use fspy_seccomp_unotify::{ impl_handler, supervisor::{ - handler::arg::{CStrPtr, Fd}, + handler::arg::{CStrPtr, Caller, Fd}, supervise, }, target::install_target, @@ -34,11 +34,14 @@ enum Syscall { #[derive(Default, Clone, Debug)] struct SyscallRecorder(Vec); impl SyscallRecorder { - fn openat(&mut self, (fd, path): (Fd, CStrPtr)) -> io::Result<()> { - let at_dir = fd.get_path()?; - let path = path.read_with_buf::<32768, _, _>(|path| { - Ok(path.map(|path| OsString::from_vec(path.to_vec()))) - })?; + fn openat(&mut self, caller: Caller<'_>, (fd, path): (Fd, CStrPtr)) -> io::Result<()> { + let at_dir = fd.get_path(caller)?; + let mut buf = [0u8; 40000]; + let path = if let Some(null_pos) = path.read(caller, &mut buf)? { + Some(OsString::from_vec(buf[..null_pos].to_vec())) + } else { + None + }; self.0.push(Syscall::Openat { at_dir, path }); Ok(()) } diff --git a/crates/fspy_test_bin/Cargo.toml b/crates/fspy_test_bin/Cargo.toml index 79e04a688f..bdde144007 100644 --- a/crates/fspy_test_bin/Cargo.toml +++ b/crates/fspy_test_bin/Cargo.toml @@ -7,7 +7,8 @@ license.workspace = true publish = false rust-version.workspace = true -[dependencies] +[target.'cfg(target_os = "linux")'.dependencies] +nix = { workspace = true } [lints] workspace = true diff --git a/crates/fspy_test_bin/src/main.rs b/crates/fspy_test_bin/src/main.rs index 51a834adca..81ffa7e1a8 100644 --- a/crates/fspy_test_bin/src/main.rs +++ b/crates/fspy_test_bin/src/main.rs @@ -1,6 +1,13 @@ -use std::fs::File; +#[cfg(not(target_os = "linux"))] +fn main() { + unimplemented!("fspy_test_bin is only for Linux"); +} +#[cfg(target_os = "linux")] fn main() { + use std::fs::File; + + use nix::fcntl::{AT_FDCWD, OFlag, OpenHow, openat2}; let args = std::env::args().collect::>(); assert!(args.len() == 3, "expected 2 arguments: "); let action = args[1].as_str(); @@ -13,8 +20,27 @@ fn main() { "open_write" => { let _ = File::options().write(true).open(path); } + "open_readwrite" => { + let _ = File::options().read(true).write(true).open(path); + } + "openat2_read" => { + let _ = openat2(AT_FDCWD, path, OpenHow::new().flags(OFlag::O_RDONLY)); + } + "openat2_write" => { + let _ = openat2(AT_FDCWD, path, OpenHow::new().flags(OFlag::O_WRONLY)); + } + "openat2_readwrite" => { + let _ = openat2(AT_FDCWD, path, OpenHow::new().flags(OFlag::O_RDWR)); + } "readdir" => { - let _ = std::fs::read_dir(path); + let mut entries = std::fs::read_dir(path).unwrap(); + let _ = entries.next(); + } + "stat" => { + let _ = std::fs::metadata(path); + } + "execve" => { + let _ = std::process::Command::new(path).spawn(); } _ => panic!("unknown action: {}", action), }