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/.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 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/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..81622eb322 100644 --- a/crates/fspy/src/unix/syscall_handler/mod.rs +++ b/crates/fspy/src/unix/syscall_handler/mod.rs @@ -16,8 +16,12 @@ 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 + return Ok(()); + }; self.arena .add(PathAccess { mode: AccessMode::Read, path: NativeStr::from_bytes(path) }); Ok(()) @@ -25,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 { @@ -36,7 +49,8 @@ impl SyscallHandler { } impl_handler!( - SyscallHandler, - openat - getdents64 + SyscallHandler: + #[cfg(target_arch = "x86_64")] open, + openat, + getdents64, ); diff --git a/crates/fspy/tests/static_executable.rs b/crates/fspy/tests/static_executable.rs new file mode 100644 index 0000000000..24ebb54fc9 --- /dev/null +++ b/crates/fspy/tests/static_executable.rs @@ -0,0 +1,53 @@ +#![cfg(target_os = "linux")] + +use std::{ + fs::{self, Permissions}, + os::unix::fs::PermissionsExt as _, + path::{Path, PathBuf}, + sync::LazyLock, +}; + +use fspy::PathAccessIterable; +use fspy_shared_unix::is_dynamically_linked_to_libc; + +use crate::test_utils::assert_contains; + +mod test_utils; + +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, 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"); + + test_bin_path + }); + TEST_BIN_PATH.as_path() +} + +async fn track_test_bin(args: &[&str]) -> PathAccessIterable { + let mut cmd = fspy::Spy::global().unwrap().new_command(test_bin_path()); + cmd.args(args); + let mut tracked_child = cmd.spawn().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/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] 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..57bda12ca6 100644 --- a/crates/fspy_seccomp_unotify/src/payload/mod.rs +++ b/crates/fspy_seccomp_unotify/src/payload/mod.rs @@ -1,10 +1,9 @@ -use std::os::fd::RawFd; mod filter; use bincode::{Decode, Encode}; 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/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/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/src/supervisor/mod.rs b/crates/fspy_seccomp_unotify/src/supervisor/mod.rs index 16c15836de..dff4319780 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, + 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..74f3ffd369 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, }; @@ -13,7 +13,6 @@ use assertables::assert_contains; use fspy_seccomp_unotify::{ impl_handler, supervisor::{ - Supervisor, handler::arg::{CStrPtr, Fd}, supervise, }, @@ -29,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)] @@ -37,27 +36,28 @@ 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(()) } } -impl_handler!(SyscallRecorder, openat); +impl_handler!(SyscallRecorder: openat,); async fn run_in_pre_exec( mut f: impl FnMut() -> io::Result<()> + Send + Sync + 'static, ) -> 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()) }) @@ -99,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: Some("open_at_home".into()) } + ); assert_contains!( syscalls, - &Syscall::Openat { at_dir: "/home".into(), path: "open_at_home".into() } + &Syscall::Openat { at_dir: "/".into(), path: Some("openat_cwd".into()) } ); - assert_contains!(syscalls, &Syscall::Openat { at_dir: "/".into(), path: "openat_cwd".into() }); Ok(()) } @@ -121,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(()) @@ -131,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(()) } 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}, 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..51a834adca --- /dev/null +++ b/crates/fspy_test_bin/src/main.rs @@ -0,0 +1,21 @@ +use std::fs::File; + +fn main() { + 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(); + + 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), + } +} 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" },