Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -124,15 +125,16 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result<TrackedChild>
});
}

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::<Vec<_>>())
};

Expand Down
24 changes: 24 additions & 0 deletions crates/fspy/src/unix/syscall_handler/execve.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
16 changes: 16 additions & 0 deletions crates/fspy/src/unix/syscall_handler/getdents.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
89 changes: 66 additions & 23 deletions crates/fspy/src/unix/syscall_handler/mod.rs
Original file line number Diff line number Diff line change
@@ -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_MAX, _, _>(|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);
}
Comment thread
branchseer marked this conversation as resolved.
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()),
Expand All @@ -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,
);
35 changes: 35 additions & 0 deletions crates/fspy/src/unix/syscall_handler/open.rs
Original file line number Diff line number Diff line change
@@ -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<u64>`
(dir_fd, path, open_how): (Fd, CStrPtr, Ptr<u64>),
) -> 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))
Comment thread
branchseer marked this conversation as resolved.
}
}
35 changes: 35 additions & 0 deletions crates/fspy/src/unix/syscall_handler/stat.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
61 changes: 59 additions & 2 deletions crates/fspy/tests/static_executable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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);
}
Loading
Loading