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
5 changes: 1 addition & 4 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions crates/fspy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
9 changes: 2 additions & 7 deletions crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result<TrackedChild>
#[cfg(target_os = "linux")]
let supervisor = supervise::<SyscallHandler>()?;

#[cfg(target_os = "linux")]
let supervisor_pre_exec = supervisor.pre_exec;

let (ipc_channel_conf, ipc_receiver) = channel(SHM_CAPACITY)?;

let payload = Payload {
Expand All @@ -99,7 +96,7 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result<TrackedChild>
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);
Expand All @@ -120,8 +117,6 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result<TrackedChild>

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()?;
}
Expand All @@ -137,7 +132,7 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result<TrackedChild>
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::<Vec<_>>())
};

Expand Down
22 changes: 18 additions & 4 deletions crates/fspy/src/unix/syscall_handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,28 @@ 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_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(())
}

#[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 {
Expand All @@ -36,7 +49,8 @@ impl SyscallHandler {
}

impl_handler!(
SyscallHandler,
openat
getdents64
SyscallHandler:
#[cfg(target_arch = "x86_64")] open,
openat,
getdents64,
);
53 changes: 53 additions & 0 deletions crates/fspy/tests/static_executable.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf> = 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);
}
33 changes: 19 additions & 14 deletions crates/fspy/tests/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf, StripPrefixError>, _>(
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<PathBuf, StripPrefixError>, _>(
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::<Vec<_>>()
Comment thread
branchseer marked this conversation as resolved.
);
}
}

#[macro_export]
Expand Down
4 changes: 3 additions & 1 deletion crates/fspy_seccomp_unotify/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 1 addition & 2 deletions crates/fspy_seccomp_unotify/src/payload/mod.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
pub(crate) filter: Filter,
}
22 changes: 12 additions & 10 deletions crates/fspy_seccomp_unotify/src/supervisor/handler/arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ pub struct CStrPtr {
}

impl CStrPtr {
pub fn read<B: BufMut>(&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<B: BufMut>(&self, buf: &mut B) -> io::Result<bool> {
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 =
Expand All @@ -52,18 +51,21 @@ impl CStrPtr {
continue;
};
unsafe { buf.advance_mut(nul_index) };
return Ok(());
return Ok(true);
}
}

pub fn read_with_buf<const BUF_SIZE: usize, R, F: FnOnce(&[u8]) -> io::Result<R>>(
// 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<const BUF_SIZE: usize, R, F: FnOnce(Option<&[u8]>) -> io::Result<R>>(
&self,
f: F,
) -> io::Result<R> {
let mut read_buf: [MaybeUninit<u8>; 32768] = [const { MaybeUninit::uninit() }; 32768];
let mut read_buf: [MaybeUninit<u8>; BUF_SIZE] = [const { MaybeUninit::uninit() }; BUF_SIZE];
Comment thread
branchseer marked this conversation as resolved.
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 })
}
}

Expand Down
11 changes: 9 additions & 2 deletions crates/fspy_seccomp_unotify/src/supervisor/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?)
}
Expand Down
Loading
Loading