From 045ce54855703bf3d440254530309f67b1ca3a9c Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 14:59:01 +0800 Subject: [PATCH 01/11] fix(fspy): fix ipc of seccomp filter fd --- .devcontainer/devcontainer.json | 5 +- Cargo.lock | 6 ++ crates/fspy/Cargo.toml | 6 ++ crates/fspy/src/command.rs | 2 +- crates/fspy/tests/static_executable.rs | 45 +++++++++++ crates/fspy_seccomp_unotify/Cargo.toml | 4 +- .../fspy_seccomp_unotify/src/payload/mod.rs | 2 +- .../src/supervisor/mod.rs | 75 +++++++++++-------- crates/fspy_seccomp_unotify/src/target.rs | 14 +++- .../fspy_seccomp_unotify/tests/arg_types.rs | 26 +++---- crates/fspy_test_bin/Cargo.toml | 13 ++++ crates/fspy_test_bin/src/main.rs | 6 ++ 12 files changed, 148 insertions(+), 56 deletions(-) create mode 100644 crates/fspy/tests/static_executable.rs create mode 100644 crates/fspy_test_bin/Cargo.toml create mode 100644 crates/fspy_test_bin/src/main.rs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 262f67e19f..44cf1e9a72 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,16 +5,13 @@ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/vscode/devcontainers/base:ubuntu-22.04", "updateContentCommand": { - "systemPackages": "sudo apt-get update -y && sudo apt-get install -y pkg-config libssl-dev", - "rustToolchain": "rustup show", - "mise": "mise trust && mise install && mkdir -p ~/.config/fish && echo 'mise activate fish --shims | source' >> ~/.config/fish/config.fish" + "rustToolchain": "rustup show" }, "containerEnv": { "CARGO_TARGET_DIR": "/tmp/target" }, "features": { "ghcr.io/devcontainers/features/rust:1": {}, - "ghcr.io/devcontainers-extra/features/mise:1": {}, "ghcr.io/devcontainers-extra/features/fish-apt-get:1": {} }, "customizations": { diff --git a/Cargo.lock b/Cargo.lock index 31281c52a7..8b19700ea0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1530,6 +1530,7 @@ dependencies = [ "fspy_seccomp_unotify", "fspy_shared", "fspy_shared_unix", + "fspy_test_bin", "futures-util", "libc", "memmap2", @@ -1636,6 +1637,7 @@ dependencies = [ "passfd 0.2.0 (git+https://github.com/polachok/passfd?rev=d55881752c16aced1a49a75f9c428d38d3767213)", "seccompiler", "syscalls", + "tempfile", "test-log", "tokio", "tracing", @@ -1684,6 +1686,10 @@ dependencies = [ "stackalloc", ] +[[package]] +name = "fspy_test_bin" +version = "0.0.0" + [[package]] name = "fspy_test_utils" version = "0.0.0" diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index 0f36b8b4c5..66f4b3852c 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -55,6 +55,12 @@ csv-async = { workspace = true } ctor = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "fs", "io-std"] } +[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dev-dependencies] +fspy_test_bin = { path = "../fspy_test_bin", artifact = "bin", target = "aarch64-unknown-linux-musl" } + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dev-dependencies] +fspy_test_bin = { path = "../fspy_test_bin", artifact = "bin", target = "x86_64-unknown-linux-musl" } + [build-dependencies] anyhow = { workspace = true } attohttpc = { workspace = true } diff --git a/crates/fspy/src/command.rs b/crates/fspy/src/command.rs index 18130f8afc..0b594e0529 100644 --- a/crates/fspy/src/command.rs +++ b/crates/fspy/src/command.rs @@ -164,7 +164,7 @@ impl Command { } self.program = which::which_in( - self.program.as_os_str(), + dbg!(self.program.as_os_str()), path_env, if let Some(cwd) = &self.cwd { cwd.clone() } else { std::env::current_dir()? }, ) diff --git a/crates/fspy/tests/static_executable.rs b/crates/fspy/tests/static_executable.rs new file mode 100644 index 0000000000..008eaa8a2b --- /dev/null +++ b/crates/fspy/tests/static_executable.rs @@ -0,0 +1,45 @@ +#![cfg(target_os = "linux")] + +use std::{ + fs::{self, Permissions}, + os::unix::fs::PermissionsExt as _, + path::{Path, PathBuf}, + str::from_utf8, + sync::{LazyLock, OnceLock}, +}; + +use bstr::{B, BStr}; + +mod test_utils; + +const PRELOAD_CDYLIB_BINARY: &[u8] = include_bytes!(env!("CARGO_BIN_FILE_FSPY_TEST_BIN")); + +fn test_bin_path() -> &'static Path { + static TEST_BIN_PATH: LazyLock = LazyLock::new(|| { + let tmp_dir = env!("CARGO_TARGET_TMPDIR"); + let test_bin_path = PathBuf::from(tmp_dir).join("fspy-test-bin"); + fs::write(&test_bin_path, PRELOAD_CDYLIB_BINARY).expect("failed to write test binary"); + fs::set_permissions(&test_bin_path, Permissions::from_mode(0o755)) + .expect("failed to set permissions on test binary"); + + // Verify that the test binary is indeed a static executable + let output = std::process::Command::new("ldd") + .arg(&test_bin_path) + .output() + .expect("failed to run ldd"); + let stderr = from_utf8(&output.stderr).unwrap().trim(); + assert_eq!(stderr, "not a dynamic executable"); + + test_bin_path + }); + TEST_BIN_PATH.as_path() +} + +#[tokio::test] +async fn static_executable() { + let mut cmd = fspy::Spy::global().unwrap().new_command(test_bin_path()); + // cmd.envs(std::env::vars_os()); + let mut tracked_child = cmd.spawn().await.unwrap(); + + tracked_child.tokio_child.wait().await.unwrap(); +} diff --git a/crates/fspy_seccomp_unotify/Cargo.toml b/crates/fspy_seccomp_unotify/Cargo.toml index 6261fc2bb6..c1bb1a8aa6 100644 --- a/crates/fspy_seccomp_unotify/Cargo.toml +++ b/crates/fspy_seccomp_unotify/Cargo.toml @@ -13,8 +13,10 @@ nix = { workspace = true, features = ["process", "fs", "poll", "socket", "uio"] passfd = { workspace = true, default-features = false, optional = true } seccompiler = { workspace = true } syscalls = { workspace = true, features = ["std"] } -tokio = { workspace = true, features = ["net", "process", "io-util", "rt"] } +tokio = { workspace = true, features = ["net", "process", "io-util", "rt", "sync"] } tracing = { workspace = true } +tempfile = { workspace = true } +futures-util = { workspace = true } [target.'cfg(target_os = "linux")'.dev-dependencies] assertables = { workspace = true } diff --git a/crates/fspy_seccomp_unotify/src/payload/mod.rs b/crates/fspy_seccomp_unotify/src/payload/mod.rs index 14dd951695..667afc906d 100644 --- a/crates/fspy_seccomp_unotify/src/payload/mod.rs +++ b/crates/fspy_seccomp_unotify/src/payload/mod.rs @@ -5,6 +5,6 @@ pub use filter::Filter; #[derive(Debug, Encode, Decode, Clone)] pub struct SeccompPayload { - pub(crate) ipc_fd: RawFd, + pub(crate) ipc_path: Vec, pub(crate) filter: Filter, } diff --git a/crates/fspy_seccomp_unotify/src/supervisor/mod.rs b/crates/fspy_seccomp_unotify/src/supervisor/mod.rs index 16c15836de..39ccf820e8 100644 --- a/crates/fspy_seccomp_unotify/src/supervisor/mod.rs +++ b/crates/fspy_seccomp_unotify/src/supervisor/mod.rs @@ -2,16 +2,27 @@ pub mod handler; mod listener; use std::{ + convert::Infallible, io::{self}, - os::fd::{AsRawFd, FromRawFd, OwnedFd}, + os::{ + fd::{FromRawFd, OwnedFd}, + unix::ffi::OsStrExt, + }, }; +use futures_util::{ + future::{Either, select}, + pin_mut, +}; pub use handler::SeccompNotifyHandler; use listener::NotifyListener; -use nix::fcntl::{FcntlArg, FdFlag, fcntl}; use passfd::tokio::FdPassingExt; use seccompiler::{BpfProgram, SeccompAction, SeccompFilter}; -use tokio::{net::UnixStream, task::JoinSet}; +use tokio::{ + net::{UnixListener, UnixStream}, + sync::oneshot, + task::{JoinHandle, JoinSet}, +}; use tracing::{Level, span}; use crate::{ @@ -19,27 +30,28 @@ use crate::{ payload::{Filter, SeccompPayload}, }; -pub struct Supervisor { - pub payload: SeccompPayload, - pub pre_exec: PreExec, - pub handling_loop: F, +pub struct Supervisor { + payload: SeccompPayload, + cancel_tx: oneshot::Sender, + handling_loop_task: JoinHandle>>, } -pub struct PreExec(OwnedFd); -impl PreExec { - pub fn run(&self) -> nix::Result<()> { - let mut fd_flag = FdFlag::from_bits_retain(fcntl(&self.0, FcntlArg::F_GETFD)?); - fd_flag.remove(FdFlag::FD_CLOEXEC); - fcntl(&self.0, FcntlArg::F_SETFD(fd_flag))?; - Ok(()) +impl Supervisor { + pub fn payload(&self) -> &SeccompPayload { + &self.payload + } + + pub async fn stop(self) -> io::Result> { + drop(self.cancel_tx); + self.handling_loop_task.await.expect("handling loop task panicked") } } -pub fn supervise() --> io::Result>> + Send>> { - let (notify_fd_receiver, notify_fd_sender) = UnixStream::pair()?; - let notify_fd_sender = notify_fd_sender.into_std()?; - notify_fd_sender.set_nonblocking(false)?; +pub fn supervise() -> io::Result> +{ + let notify_listener = tempfile::Builder::new() + .prefix("fspy_seccomp_notify") + .make(|path| UnixListener::bind(path))?; let filter = SeccompFilter::new( H::syscalls().iter().map(|sysno| (sysno.id().into(), vec![])).collect(), @@ -57,22 +69,25 @@ pub fn supervise() .collect(), ); - let payload = SeccompPayload { ipc_fd: notify_fd_sender.as_raw_fd(), filter }; + let payload = + SeccompPayload { ipc_path: notify_listener.path().as_os_str().as_bytes().to_vec(), filter }; + + // The oneshot channel is used to cancel the accept loop. + // The sender doesn't need to actually send anything. Drop is enough. + let (cancel_tx, mut cancel_rx) = oneshot::channel::(); let handling_loop = async move { let mut join_set: JoinSet> = JoinSet::new(); loop { - let notify_fd = match notify_fd_receiver.recv_fd().await { - Ok(fd) => unsafe { OwnedFd::from_raw_fd(fd) }, - Err(err) => { - if err.kind() == io::ErrorKind::UnexpectedEof { - break; - } else { - return Err(err); - } - } + let accept_future = notify_listener.as_file().accept(); + pin_mut!(accept_future); + let (incoming_stream, _) = match select(&mut cancel_rx, accept_future).await { + Either::Left((Err(_), _)) => break, + Either::Right((incoming, _)) => incoming?, }; + let notify_fd = incoming_stream.recv_fd().await?; + let notify_fd = unsafe { OwnedFd::from_raw_fd(notify_fd) }; let mut listener = NotifyListener::try_from(notify_fd)?; let mut handler = H::default(); @@ -96,5 +111,5 @@ pub fn supervise() } Ok(handlers) }; - Ok(Supervisor { payload, pre_exec: PreExec(notify_fd_sender.into()), handling_loop }) + Ok(Supervisor { payload, cancel_tx, handling_loop_task: tokio::spawn(handling_loop) }) } diff --git a/crates/fspy_seccomp_unotify/src/target.rs b/crates/fspy_seccomp_unotify/src/target.rs index d527abdd23..e70cfe09a8 100644 --- a/crates/fspy_seccomp_unotify/src/target.rs +++ b/crates/fspy_seccomp_unotify/src/target.rs @@ -1,4 +1,10 @@ -use std::os::fd::AsRawFd; +use std::{ + ffi::OsStr, + os::{ + fd::AsRawFd, + unix::{ffi::OsStrExt, net::UnixStream}, + }, +}; use libc::sock_filter; use nix::sys::prctl::set_no_new_privs; @@ -11,8 +17,10 @@ pub fn install_target(payload: &SeccompPayload) -> nix::Result<()> { let sock_filters = payload.filter.0.iter().copied().map(sock_filter::from).collect::>(); let notify_fd = install_unotify_filter(&sock_filters)?; - payload - .ipc_fd + let ipc_path = OsStr::from_bytes(&payload.ipc_path); + let ipc_unix_stream = UnixStream::connect(ipc_path) + .map_err(|err| nix::Error::try_from(err).unwrap_or(nix::Error::UnknownErrno))?; + ipc_unix_stream .send_fd(notify_fd.as_raw_fd()) .map_err(|err| nix::Error::try_from(err).unwrap_or(nix::Error::UnknownErrno))?; Ok(()) diff --git a/crates/fspy_seccomp_unotify/tests/arg_types.rs b/crates/fspy_seccomp_unotify/tests/arg_types.rs index c8f9f4055b..9014dc20c6 100644 --- a/crates/fspy_seccomp_unotify/tests/arg_types.rs +++ b/crates/fspy_seccomp_unotify/tests/arg_types.rs @@ -13,7 +13,6 @@ use assertables::assert_contains; use fspy_seccomp_unotify::{ impl_handler, supervisor::{ - Supervisor, handler::arg::{CStrPtr, Fd}, supervise, }, @@ -52,12 +51,13 @@ async fn run_in_pre_exec( ) -> Result, Box> { Ok(timeout(Duration::from_secs(5), async move { let mut cmd = Command::new("/bin/echo"); - let Supervisor { payload, handling_loop, pre_exec } = supervise::()?; + let supervisor = supervise::()?; + + let payload = supervisor.payload().clone(); unsafe { cmd.pre_exec(move || { install_target(&payload)?; - pre_exec.run()?; f()?; Ok(()) }); @@ -66,23 +66,17 @@ async fn run_in_pre_exec( let _span = span!(Level::TRACE, "spawn test child process"); cmd.spawn() }); + + let exit_status = child_fut.await.unwrap()?.wait().await?; + trace!("test child process exited with status: {:?}", exit_status); + trace!("waiting for handler to finish and test child process to exit"); - let (recorders, exit_status) = futures_util::future::try_join( - async move { - let recorders = handling_loop.await?; - trace!("{} recorders awaited", recorders.len()); - Ok(recorders) - }, - async move { - let exit_status = child_fut.await.unwrap()?.wait().await?; - trace!("test child process exited with status: {:?}", exit_status); - io::Result::Ok(exit_status) - }, - ) - .await?; assert!(exit_status.success()); + let recorders = supervisor.stop().await?; + trace!("{} recorders awaited", recorders.len()); + let syscalls = recorders.into_iter().map(|recorder| recorder.0.into_iter()).flatten(); io::Result::Ok(syscalls.collect()) }) diff --git a/crates/fspy_test_bin/Cargo.toml b/crates/fspy_test_bin/Cargo.toml new file mode 100644 index 0000000000..79e04a688f --- /dev/null +++ b/crates/fspy_test_bin/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "fspy_test_bin" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[dependencies] + +[lints] +workspace = true diff --git a/crates/fspy_test_bin/src/main.rs b/crates/fspy_test_bin/src/main.rs new file mode 100644 index 0000000000..1546084156 --- /dev/null +++ b/crates/fspy_test_bin/src/main.rs @@ -0,0 +1,6 @@ +fn main() { + eprintln!("zz"); + let _ = std::fs::File::open("hello"); + + eprintln!("bb"); +} From 752b10b50683fc9a9a4b7d46fe3fcf0a0ed7c9eb Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 15:22:05 +0800 Subject: [PATCH 02/11] fix path overflow test --- .../src/supervisor/handler/arg.rs | 22 +++++++------ .../fspy_seccomp_unotify/tests/arg_types.rs | 31 +++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/crates/fspy_seccomp_unotify/src/supervisor/handler/arg.rs b/crates/fspy_seccomp_unotify/src/supervisor/handler/arg.rs index 92b282c192..5703695eec 100644 --- a/crates/fspy_seccomp_unotify/src/supervisor/handler/arg.rs +++ b/crates/fspy_seccomp_unotify/src/supervisor/handler/arg.rs @@ -20,14 +20,13 @@ pub struct CStrPtr { } impl CStrPtr { - pub fn read(&self, buf: &mut B) -> io::Result<()> { + // 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 Err(io::Error::new( - io::ErrorKind::InvalidFilename, - "CStrPtr::read: buf is filled before null-terminator is found", - )); + return Ok(false); } let local_iov = @@ -52,18 +51,21 @@ impl CStrPtr { continue; }; unsafe { buf.advance_mut(nul_index) }; - return Ok(()); + return Ok(true); } } - pub fn read_with_buf io::Result>( + // 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; 32768] = [const { MaybeUninit::uninit() }; 32768]; + let mut read_buf: [MaybeUninit; BUF_SIZE] = [const { MaybeUninit::uninit() }; BUF_SIZE]; let mut read_buf = ReadBuf::uninit(read_buf.as_mut_slice()); - self.read(&mut read_buf)?; - f(read_buf.filled()) + let success = self.read(&mut read_buf)?; + f(if success { Some(read_buf.filled()) } else { None }) } } diff --git a/crates/fspy_seccomp_unotify/tests/arg_types.rs b/crates/fspy_seccomp_unotify/tests/arg_types.rs index 9014dc20c6..a396070ac1 100644 --- a/crates/fspy_seccomp_unotify/tests/arg_types.rs +++ b/crates/fspy_seccomp_unotify/tests/arg_types.rs @@ -3,9 +3,9 @@ use std::{ env::{current_dir, set_current_dir}, error::Error, - ffi::{CString, OsStr, OsString}, + ffi::{CString, OsString}, io, - os::unix::ffi::{OsStrExt, OsStringExt}, + os::unix::ffi::OsStringExt, time::Duration, }; @@ -28,7 +28,7 @@ use tracing::{Level, span, trace}; #[derive(Debug, PartialEq, Eq, Clone)] enum Syscall { - Openat { at_dir: OsString, path: OsString }, + Openat { at_dir: OsString, path: Option }, } #[derive(Default, Clone, Debug)] @@ -36,8 +36,8 @@ 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: &[u8]| { - Ok(OsStr::from_bytes(path).to_os_string()) + let path = path.read_with_buf::<32768, _, _>(|path| { + Ok(path.map(|path| OsString::from_vec(path.to_vec()))) })?; self.0.push(Syscall::Openat { at_dir, path }); Ok(()) @@ -93,12 +93,15 @@ async fn fd_and_path() -> Result<(), Box> { Ok(()) }) .await?; - assert_contains!(syscalls, &Syscall::Openat { at_dir: "/".into(), path: "/home".into() }); + assert_contains!(syscalls, &Syscall::Openat { at_dir: "/".into(), path: Some("/home".into()) }); assert_contains!( syscalls, - &Syscall::Openat { at_dir: "/home".into(), path: "open_at_home".into() } + &Syscall::Openat { at_dir: "/home".into(), path: Some("open_at_home".into()) } + ); + assert_contains!( + syscalls, + &Syscall::Openat { at_dir: "/".into(), path: Some("openat_cwd".into()) } ); - assert_contains!(syscalls, &Syscall::Openat { at_dir: "/".into(), path: "openat_cwd".into() }); Ok(()) } @@ -115,7 +118,7 @@ async fn path_long() -> Result<(), Box> { syscalls, &Syscall::Openat { at_dir: current_dir().unwrap().into(), - path: OsString::from_vec(long_path), + path: Some(OsString::from_vec(long_path)), } ); Ok(()) @@ -125,12 +128,14 @@ async fn path_long() -> Result<(), Box> { async fn path_overflow() -> Result<(), Box> { let long_path = [b'a'].repeat(40000); let long_path_cstr = CString::new(long_path.as_slice()).unwrap(); - let ret = run_in_pre_exec(move || { + let syscalls = run_in_pre_exec(move || { let _ = openat(AT_FDCWD, long_path_cstr.as_c_str(), OFlag::O_RDONLY, Mode::empty()); Ok(()) }) - .await; - let err = ret.unwrap_err(); - assert_eq!(err.downcast::().unwrap().kind(), io::ErrorKind::InvalidFilename); + .await?; + assert_contains!( + syscalls, + &Syscall::Openat { at_dir: current_dir().unwrap().into(), path: None } + ); Ok(()) } From 58cf52dae2ef6fa9f80a362659a71f20ad859fee Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 16:23:20 +0800 Subject: [PATCH 03/11] add open_read test for static_executable --- crates/fspy/tests/static_executable.rs | 25 ++++++++++++++++++------- crates/fspy_test_bin/src/main.rs | 21 ++++++++++++++++++--- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/crates/fspy/tests/static_executable.rs b/crates/fspy/tests/static_executable.rs index 008eaa8a2b..c30019e81b 100644 --- a/crates/fspy/tests/static_executable.rs +++ b/crates/fspy/tests/static_executable.rs @@ -1,14 +1,17 @@ #![cfg(target_os = "linux")] use std::{ + ffi::OsStr, fs::{self, Permissions}, - os::unix::fs::PermissionsExt as _, + os::unix::{ffi::OsStrExt, fs::PermissionsExt as _}, path::{Path, PathBuf}, str::from_utf8, - sync::{LazyLock, OnceLock}, + sync::LazyLock, }; -use bstr::{B, BStr}; +use fspy::PathAccessIterable; + +use crate::test_utils::assert_contains; mod test_utils; @@ -35,11 +38,19 @@ fn test_bin_path() -> &'static Path { TEST_BIN_PATH.as_path() } -#[tokio::test] -async fn static_executable() { +async fn track_test_bin(args: &[&str]) -> PathAccessIterable { let mut cmd = fspy::Spy::global().unwrap().new_command(test_bin_path()); - // cmd.envs(std::env::vars_os()); + cmd.args(args); let mut tracked_child = cmd.spawn().await.unwrap(); - tracked_child.tokio_child.wait().await.unwrap(); + let output = tracked_child.tokio_child.wait().await.unwrap(); + assert!(output.success()); + + tracked_child.accesses_future.await.unwrap() +} + +#[tokio::test] +async fn open_read() { + let accesses = track_test_bin(&["open_read", "/hello"]).await; + assert_contains(&accesses, Path::new("/hello"), fspy::AccessMode::Read); } diff --git a/crates/fspy_test_bin/src/main.rs b/crates/fspy_test_bin/src/main.rs index 1546084156..51a834adca 100644 --- a/crates/fspy_test_bin/src/main.rs +++ b/crates/fspy_test_bin/src/main.rs @@ -1,6 +1,21 @@ +use std::fs::File; + fn main() { - eprintln!("zz"); - let _ = std::fs::File::open("hello"); + let args = std::env::args().collect::>(); + assert!(args.len() == 3, "expected 2 arguments: "); + let action = args[1].as_str(); + let path = args[2].as_str(); - eprintln!("bb"); + match action { + "open_read" => { + let _ = File::open(path); + } + "open_write" => { + let _ = File::options().write(true).open(path); + } + "readdir" => { + let _ = std::fs::read_dir(path); + } + _ => panic!("unknown action: {}", action), + } } From c8f8f4d9d1aadf1266225a35b76c54e7a23b902d Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 16:27:05 +0800 Subject: [PATCH 04/11] remove unused imports --- crates/fspy/src/unix/mod.rs | 9 ++------- crates/fspy/src/unix/syscall_handler/mod.rs | 4 ++++ crates/fspy/tests/static_executable.rs | 3 +-- crates/fspy_seccomp_unotify/src/payload/mod.rs | 1 - crates/fspy_seccomp_unotify/src/supervisor/mod.rs | 2 +- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index be82e7c85e..b78b29fabd 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -85,9 +85,6 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result #[cfg(target_os = "linux")] let supervisor = supervise::()?; - #[cfg(target_os = "linux")] - let supervisor_pre_exec = supervisor.pre_exec; - let (ipc_channel_conf, ipc_receiver) = channel(SHM_CAPACITY)?; let payload = Payload { @@ -99,7 +96,7 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result preload_path: command.spy_inner.preload_path.clone(), #[cfg(target_os = "linux")] - seccomp_payload: supervisor.payload, + seccomp_payload: supervisor.payload().clone(), }; let encoded_payload = encode_payload(payload); @@ -120,8 +117,6 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result unsafe { tokio_command.pre_exec(move || { - #[cfg(target_os = "linux")] - supervisor_pre_exec.run()?; if let Some(pre_exec) = pre_exec.as_ref() { pre_exec.run()?; } @@ -137,7 +132,7 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result let arenas = std::iter::once(exec_resolve_accesses); #[cfg(target_os = "linux")] let arenas = - arenas.chain(supervisor.handling_loop.await?.into_iter().map(|handler| handler.arena)); + arenas.chain(supervisor.stop().await?.into_iter().map(|handler| handler.arena)); io::Result::Ok(arenas.collect::>()) }; diff --git a/crates/fspy/src/unix/syscall_handler/mod.rs b/crates/fspy/src/unix/syscall_handler/mod.rs index 4a3303d7a4..34e42a1c42 100644 --- a/crates/fspy/src/unix/syscall_handler/mod.rs +++ b/crates/fspy/src/unix/syscall_handler/mod.rs @@ -18,6 +18,10 @@ pub struct SyscallHandler { impl SyscallHandler { fn openat(&mut self, (_, path): (Ignored, 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(()) diff --git a/crates/fspy/tests/static_executable.rs b/crates/fspy/tests/static_executable.rs index c30019e81b..e9840ac28b 100644 --- a/crates/fspy/tests/static_executable.rs +++ b/crates/fspy/tests/static_executable.rs @@ -1,9 +1,8 @@ #![cfg(target_os = "linux")] use std::{ - ffi::OsStr, fs::{self, Permissions}, - os::unix::{ffi::OsStrExt, fs::PermissionsExt as _}, + os::unix::fs::PermissionsExt as _, path::{Path, PathBuf}, str::from_utf8, sync::LazyLock, diff --git a/crates/fspy_seccomp_unotify/src/payload/mod.rs b/crates/fspy_seccomp_unotify/src/payload/mod.rs index 667afc906d..57bda12ca6 100644 --- a/crates/fspy_seccomp_unotify/src/payload/mod.rs +++ b/crates/fspy_seccomp_unotify/src/payload/mod.rs @@ -1,4 +1,3 @@ -use std::os::fd::RawFd; mod filter; use bincode::{Decode, Encode}; pub use filter::Filter; diff --git a/crates/fspy_seccomp_unotify/src/supervisor/mod.rs b/crates/fspy_seccomp_unotify/src/supervisor/mod.rs index 39ccf820e8..dff4319780 100644 --- a/crates/fspy_seccomp_unotify/src/supervisor/mod.rs +++ b/crates/fspy_seccomp_unotify/src/supervisor/mod.rs @@ -19,7 +19,7 @@ use listener::NotifyListener; use passfd::tokio::FdPassingExt; use seccompiler::{BpfProgram, SeccompAction, SeccompFilter}; use tokio::{ - net::{UnixListener, UnixStream}, + net::UnixListener, sync::oneshot, task::{JoinHandle, JoinSet}, }; From e64860caf471c58de81e22405f51206938d1a0d5 Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 16:30:09 +0800 Subject: [PATCH 05/11] enable typeaware linting test on Linux --- packages/cli/snap-tests/oxlint-typeaware/steps.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/snap-tests/oxlint-typeaware/steps.json b/packages/cli/snap-tests/oxlint-typeaware/steps.json index 2955df5ff9..916bff384d 100644 --- a/packages/cli/snap-tests/oxlint-typeaware/steps.json +++ b/packages/cli/snap-tests/oxlint-typeaware/steps.json @@ -1,5 +1,4 @@ { - "ignoredPlatforms": ["linux"], "env": { "VITE_DISABLE_AUTO_INSTALL": "1" }, From 19bdd898bf0e73579eee2c8ee18f9916687bef49 Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 16:34:25 +0800 Subject: [PATCH 06/11] remove dbg! --- crates/fspy/src/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fspy/src/command.rs b/crates/fspy/src/command.rs index 0b594e0529..18130f8afc 100644 --- a/crates/fspy/src/command.rs +++ b/crates/fspy/src/command.rs @@ -164,7 +164,7 @@ impl Command { } self.program = which::which_in( - dbg!(self.program.as_os_str()), + self.program.as_os_str(), path_env, if let Some(cwd) = &self.cwd { cwd.clone() } else { std::env::current_dir()? }, ) From a9eb1b67b0e2499d0effda798e785f03fecd89de Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 16:36:33 +0800 Subject: [PATCH 07/11] ci: add x86_64-unknown-linux-musl target --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b013cd58af..7eef658b8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,9 @@ jobs: save-cache: ${{ github.ref_name == 'main' }} cache-key: test + - run: rustup target add x86_64-unknown-linux-musl + if: ${{ matrix.os == 'ubuntu-latest' }} + - run: cargo check --all-targets --all-features env: RUSTFLAGS: '-D warnings --cfg tokio_unstable' # also update .cargo/config.toml From c330310363506362d2613b27a118cdb55a89254c Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 16:54:15 +0800 Subject: [PATCH 08/11] check ldd exit code --- crates/fspy/tests/static_executable.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/fspy/tests/static_executable.rs b/crates/fspy/tests/static_executable.rs index e9840ac28b..5716b53f35 100644 --- a/crates/fspy/tests/static_executable.rs +++ b/crates/fspy/tests/static_executable.rs @@ -25,12 +25,14 @@ fn test_bin_path() -> &'static Path { .expect("failed to set permissions on test binary"); // Verify that the test binary is indeed a static executable - let output = std::process::Command::new("ldd") - .arg(&test_bin_path) - .output() - .expect("failed to run ldd"); - let stderr = from_utf8(&output.stderr).unwrap().trim(); - assert_eq!(stderr, "not a dynamic executable"); + let output = std::process::Command::new("ldd").arg(&test_bin_path).output().unwrap(); + assert_eq!( + output.status.code(), + Some(1), + "ldd should fail on static executables. Stdout: {}. Stderr: {}", + from_utf8(&output.stdout).unwrap(), + from_utf8(&output.stderr).unwrap() + ); test_bin_path }); From e00701c27b13f8509bbce8ae37014b91ed2be7e5 Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 17:36:29 +0800 Subject: [PATCH 09/11] ensure static executable using `fspy_shared_unix::is_dynamically_linked_to_lib` --- crates/fspy/tests/static_executable.rs | 22 ++++++++----------- .../src/{spawn/linux => }/elf.rs | 0 crates/fspy_shared_unix/src/lib.rs | 6 +++++ .../fspy_shared_unix/src/spawn/linux/mod.rs | 3 +-- 4 files changed, 16 insertions(+), 15 deletions(-) rename crates/fspy_shared_unix/src/{spawn/linux => }/elf.rs (100%) diff --git a/crates/fspy/tests/static_executable.rs b/crates/fspy/tests/static_executable.rs index 5716b53f35..24ebb54fc9 100644 --- a/crates/fspy/tests/static_executable.rs +++ b/crates/fspy/tests/static_executable.rs @@ -4,36 +4,32 @@ use std::{ fs::{self, Permissions}, os::unix::fs::PermissionsExt as _, path::{Path, PathBuf}, - str::from_utf8, sync::LazyLock, }; use fspy::PathAccessIterable; +use fspy_shared_unix::is_dynamically_linked_to_libc; use crate::test_utils::assert_contains; mod test_utils; -const PRELOAD_CDYLIB_BINARY: &[u8] = include_bytes!(env!("CARGO_BIN_FILE_FSPY_TEST_BIN")); +const TEST_BIN_CONTENT: &[u8] = include_bytes!(env!("CARGO_BIN_FILE_FSPY_TEST_BIN")); fn test_bin_path() -> &'static Path { static TEST_BIN_PATH: LazyLock = LazyLock::new(|| { + assert_eq!( + is_dynamically_linked_to_libc(&TEST_BIN_CONTENT), + Ok(false), + "Test binary is not a static executable" + ); + let tmp_dir = env!("CARGO_TARGET_TMPDIR"); let test_bin_path = PathBuf::from(tmp_dir).join("fspy-test-bin"); - fs::write(&test_bin_path, PRELOAD_CDYLIB_BINARY).expect("failed to write test binary"); + fs::write(&test_bin_path, TEST_BIN_CONTENT).expect("failed to write test binary"); fs::set_permissions(&test_bin_path, Permissions::from_mode(0o755)) .expect("failed to set permissions on test binary"); - // Verify that the test binary is indeed a static executable - let output = std::process::Command::new("ldd").arg(&test_bin_path).output().unwrap(); - assert_eq!( - output.status.code(), - Some(1), - "ldd should fail on static executables. Stdout: {}. Stderr: {}", - from_utf8(&output.stdout).unwrap(), - from_utf8(&output.stderr).unwrap() - ); - test_bin_path }); TEST_BIN_PATH.as_path() diff --git a/crates/fspy_shared_unix/src/spawn/linux/elf.rs b/crates/fspy_shared_unix/src/elf.rs similarity index 100% rename from crates/fspy_shared_unix/src/spawn/linux/elf.rs rename to crates/fspy_shared_unix/src/elf.rs diff --git a/crates/fspy_shared_unix/src/lib.rs b/crates/fspy_shared_unix/src/lib.rs index 505ef477a4..5437a3efc6 100644 --- a/crates/fspy_shared_unix/src/lib.rs +++ b/crates/fspy_shared_unix/src/lib.rs @@ -4,3 +4,9 @@ pub mod exec; pub(crate) mod open_exec; pub mod payload; pub mod spawn; + +#[cfg(target_os = "linux")] +mod elf; + +#[cfg(target_os = "linux")] // exposed for verifying static executables in fspy tests +pub use elf::is_dynamically_linked_to_libc; diff --git a/crates/fspy_shared_unix/src/spawn/linux/mod.rs b/crates/fspy_shared_unix/src/spawn/linux/mod.rs index f80ffa9b4a..570fdcb430 100644 --- a/crates/fspy_shared_unix/src/spawn/linux/mod.rs +++ b/crates/fspy_shared_unix/src/spawn/linux/mod.rs @@ -1,11 +1,10 @@ -mod elf; - use std::{ffi::OsStr, os::unix::ffi::OsStrExt as _, path::Path}; use fspy_seccomp_unotify::{payload::SeccompPayload, target::install_target}; use memmap2::Mmap; use crate::{ + elf, exec::{Exec, ensure_env}, open_exec::open_executable, payload::{EncodedPayload, PAYLOAD_ENV_NAME}, From a90e2723abc0d4e92036f5ceef05d9e50f46077e Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 17:43:57 +0800 Subject: [PATCH 10/11] panic with detailed message in assert_contains --- crates/fspy/tests/test_utils.rs | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/crates/fspy/tests/test_utils.rs b/crates/fspy/tests/test_utils.rs index 8d83934a40..646bb6e279 100644 --- a/crates/fspy/tests/test_utils.rs +++ b/crates/fspy/tests/test_utils.rs @@ -11,20 +11,25 @@ pub fn assert_contains( expected_path: &Path, expected_mode: AccessMode, ) { - accesses - .iter() - .find(|access| { - let Ok(stripped) = - access.path.strip_path_prefix::<_, Result, _>( - expected_path, - |strip_result| strip_result.map(Path::to_path_buf), - ) - else { - return false; - }; - stripped.as_os_str().is_empty() && access.mode == expected_mode - }) - .unwrap(); + let found = accesses.iter().any(|access| { + let Ok(stripped) = + access.path.strip_path_prefix::<_, Result, _>( + expected_path, + |strip_result| strip_result.map(Path::to_path_buf), + ) + else { + return false; + }; + stripped.as_os_str().is_empty() && access.mode == expected_mode + }); + if !found { + panic!( + "Expected to find access to path {:?} with mode {:?}, but it was not found in: {:?}", + expected_path, + expected_mode, + accesses.iter().collect::>() + ); + } } #[macro_export] From 687857fb6097064e9cd96fab46967a46e8009a38 Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 24 Oct 2025 18:10:09 +0800 Subject: [PATCH 11/11] handle syscall open in x86_64 --- crates/fspy/src/unix/syscall_handler/mod.rs | 18 ++++++++++++++---- .../src/supervisor/handler/mod.rs | 11 +++++++++-- crates/fspy_seccomp_unotify/tests/arg_types.rs | 2 +- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/fspy/src/unix/syscall_handler/mod.rs b/crates/fspy/src/unix/syscall_handler/mod.rs index 34e42a1c42..81622eb322 100644 --- a/crates/fspy/src/unix/syscall_handler/mod.rs +++ b/crates/fspy/src/unix/syscall_handler/mod.rs @@ -16,7 +16,7 @@ pub struct SyscallHandler { } impl SyscallHandler { - fn openat(&mut self, (_, path): (Ignored, CStrPtr)) -> io::Result<()> { + 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 @@ -29,6 +29,15 @@ impl SyscallHandler { Ok(()) } + #[cfg(target_arch = "x86_64")] + fn open(&mut self, (path,): (CStrPtr,)) -> io::Result<()> { + self.handle_open(path) + } + + fn openat(&mut self, (_, path): (Ignored, CStrPtr)) -> io::Result<()> { + self.handle_open(path) + } + fn getdents64(&mut self, (fd,): (Fd,)) -> io::Result<()> { let path = fd.get_path()?; self.arena.add(PathAccess { @@ -40,7 +49,8 @@ impl SyscallHandler { } impl_handler!( - SyscallHandler, - openat - getdents64 + SyscallHandler: + #[cfg(target_arch = "x86_64")] open, + openat, + getdents64, ); diff --git a/crates/fspy_seccomp_unotify/src/supervisor/handler/mod.rs b/crates/fspy_seccomp_unotify/src/supervisor/handler/mod.rs index 39a5ac698c..737fecd4e4 100644 --- a/crates/fspy_seccomp_unotify/src/supervisor/handler/mod.rs +++ b/crates/fspy_seccomp_unotify/src/supervisor/handler/mod.rs @@ -11,14 +11,21 @@ pub trait SeccompNotifyHandler { #[macro_export] macro_rules! impl_handler { - ($type: ty, $($syscall:ident)*) => { + ($type:ty: $( + $(#[$attr:meta])? + $syscall:ident, + )* ) => { impl $crate::supervisor::handler::SeccompNotifyHandler for $type { fn syscalls() -> &'static [::syscalls::Sysno] { - &[ $( ::syscalls::Sysno:: $syscall ),* ] + &[ $( + $(#[$attr])? + ::syscalls::Sysno::$syscall + ),* ] } 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)?) } diff --git a/crates/fspy_seccomp_unotify/tests/arg_types.rs b/crates/fspy_seccomp_unotify/tests/arg_types.rs index a396070ac1..74f3ffd369 100644 --- a/crates/fspy_seccomp_unotify/tests/arg_types.rs +++ b/crates/fspy_seccomp_unotify/tests/arg_types.rs @@ -44,7 +44,7 @@ impl SyscallRecorder { } } -impl_handler!(SyscallRecorder, openat); +impl_handler!(SyscallRecorder: openat,); async fn run_in_pre_exec( mut f: impl FnMut() -> io::Result<()> + Send + Sync + 'static,