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
29 changes: 8 additions & 21 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ tar = "0.4.43"
tempfile = "3.14.0"
test-log = { version = "0.2.18", features = ["trace"] }
thiserror = "2"
tokio = "1.46.1"
tokio-util = "0.7.15"
tokio = "1.48.0"
tokio-util = "0.7.17"
toml = "0.9.5"
tracing = "0.1.41"
tracing-error = "0.2.1"
Expand Down
2 changes: 2 additions & 0 deletions crates/fspy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ libc = { workspace = true }
ouroboros = { workspace = true }
rand = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["net", "process", "io-util", "sync"] }
which = { workspace = true }
xxhash-rust = { workspace = true }
Expand All @@ -40,6 +41,7 @@ winsafe = { workspace = true }
tempfile = { workspace = true }

[dev-dependencies]
anyhow = { workspace = true }
csv-async = { workspace = true }
ctor = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "fs", "io-std"] }
Expand Down
4 changes: 2 additions & 2 deletions crates/fspy/examples/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{env::args_os, ffi::OsStr, io, path::PathBuf, pin::Pin};
use std::{env::args_os, ffi::OsStr, path::PathBuf, pin::Pin};

use fspy::{AccessMode, TrackedChild};
use tokio::{
Expand All @@ -7,7 +7,7 @@ use tokio::{
};

#[tokio::main]
async fn main() -> io::Result<()> {
async fn main() -> anyhow::Result<()> {
let mut args = args_os();
let _ = args.next();
assert_eq!(args.next().as_deref(), Some(OsStr::new("-o")));
Expand Down
26 changes: 16 additions & 10 deletions crates/fspy/src/command.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::{
collections::HashMap,
ffi::{OsStr, OsString},
io,
path::{Path, PathBuf},
process::Stdio,
};
Expand All @@ -12,6 +11,7 @@ use tokio::process::Command as TokioCommand;

use crate::{
TrackedChild,
error::SpawnError,
os_impl::{self, spawn_impl},
};

Expand Down Expand Up @@ -145,13 +145,13 @@ impl Command {
self
}

pub async fn spawn(mut self) -> io::Result<TrackedChild> {
pub async fn spawn(mut self) -> Result<TrackedChild, SpawnError> {
self.resolve_program()?;
spawn_impl(self).await
}

/// Resolve program name to full path using `PATH` and cwd.
pub fn resolve_program(&mut self) -> io::Result<()> {
pub fn resolve_program(&mut self) -> Result<(), SpawnError> {
let mut path_env: Option<&OsStr> = None;
for (env_name, env_value) in &self.envs {
let Some(env_name) = env_name.to_str() else {
Expand All @@ -163,13 +163,19 @@ impl Command {
}
}

self.program = which::which_in(
self.program.as_os_str(),
path_env,
if let Some(cwd) = &self.cwd { cwd.clone() } else { std::env::current_dir()? },
)
.map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))?
.into_os_string();
let cwd = if let Some(cwd) = &self.cwd {
cwd.clone()
} else {
std::env::current_dir().expect("failed to get current dir")
};
self.program = which::which_in(self.program.as_os_str(), path_env, &cwd)
.map_err(|err| SpawnError::WhichError {
program: self.program.clone(),
path: path_env.map(OsStr::to_owned),
cwd,
cause: err,
})?
.into_os_string();
Ok(())
}

Expand Down
29 changes: 29 additions & 0 deletions crates/fspy/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use std::{ffi::OsString, path::PathBuf};

#[derive(thiserror::Error, Debug)]
pub enum SpawnError {
#[error(
"could not resolve the full path of program '{program:?}' with PATH={path:?} under cwd({cwd:?})"
)]
WhichError {
program: OsString,
path: Option<OsString>,
cwd: PathBuf,
#[source]
cause: which::Error,
},

#[error("failed to initialize seccomp_unotify supervisor: {0}")]
SupervisorError(std::io::Error),

#[error("failed to create IPC channel: {0}")]
ChannelCreationError(std::io::Error),

/// On unix systems, the injection happens before the spawn actually occurs on.
/// On Windows, the injection happens after the spawn but before resuming the process.
#[error("failed to prepare the command for injection: {0}")]
InjectionError(std::io::Error),

#[error("underlying os error: {0}")]
OsSpawnError(std::io::Error),
}
2 changes: 2 additions & 0 deletions crates/fspy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// Persist the injected DLL/shared library somewhere in the filesystem.
mod fixture;

pub mod error;

mod ipc;

#[cfg(unix)]
Expand Down
16 changes: 11 additions & 5 deletions crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use tokio::task::spawn_blocking;
use crate::{
Command, TrackedChild,
arena::PathAccessArena,
error::SpawnError,
ipc::{OwnedReceiverLockGuard, SHM_CAPACITY},
};

Expand Down Expand Up @@ -82,11 +83,12 @@ impl PathAccessIterable {
}
}

pub(crate) async fn spawn_impl(mut command: Command) -> io::Result<TrackedChild> {
pub(crate) async fn spawn_impl(mut command: Command) -> Result<TrackedChild, SpawnError> {
#[cfg(target_os = "linux")]
let supervisor = supervise::<SyscallHandler>()?;
let supervisor = supervise::<SyscallHandler>().map_err(SpawnError::SupervisorError)?;

let (ipc_channel_conf, ipc_receiver) = channel(SHM_CAPACITY)?;
let (ipc_channel_conf, ipc_receiver) =
channel(SHM_CAPACITY).map_err(SpawnError::ChannelCreationError)?;

let payload = Payload {
ipc_channel_conf,
Expand All @@ -111,7 +113,8 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result<TrackedChild>
|path_access| {
exec_resolve_accesses.add(path_access);
},
)?;
)
.map_err(|err| SpawnError::InjectionError(err.into()))?;
command.set_exec(exec);

let mut tokio_command = command.into_tokio_command();
Expand All @@ -128,7 +131,10 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result<TrackedChild>
// 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 child = spawn_blocking(move || tokio_command.spawn())
.await
.map_err(|err| SpawnError::OsSpawnError(err.into()))?
.map_err(SpawnError::OsSpawnError)?;

let arenas_future = async move {
let arenas = std::iter::once(exec_resolve_accesses);
Expand Down
91 changes: 52 additions & 39 deletions crates/fspy/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use xxhash_rust::const_xxh3::xxh3_128;
use crate::{
TrackedChild,
command::Command,
error::SpawnError,
fixture::Fixture,
ipc::{OwnedReceiverLockGuard, SHM_CAPACITY},
};
Expand Down Expand Up @@ -71,13 +72,14 @@ impl SpyInner {
}
}

pub(crate) async fn spawn_impl(command: Command) -> io::Result<TrackedChild> {
pub(crate) async fn spawn_impl(command: Command) -> Result<TrackedChild, SpawnError> {
let asni_dll_path_with_nul = Arc::clone(&command.spy_inner.asni_dll_path_with_nul);
let mut command = command.into_tokio_command();

command.creation_flags(CREATE_SUSPENDED);

let (channel_conf, receiver) = channel(SHM_CAPACITY)?;
let (channel_conf, receiver) =
channel(SHM_CAPACITY).map_err(SpawnError::ChannelCreationError)?;

let accesses_future = async move {
let ipc_receiver_lock_guard = OwnedReceiverLockGuard::lock_async(receiver).await?;
Expand All @@ -87,43 +89,54 @@ pub(crate) async fn spawn_impl(command: Command) -> io::Result<TrackedChild> {

// let path_access_stream = PathAccessIterable { pipe_receiver };

let child = command.spawn_with(|std_command| {
let std_child = std_command.spawn()?;

let mut dll_paths = asni_dll_path_with_nul.as_ptr().cast::<c_char>();
let process_handle = std_child.as_raw_handle().cast::<winapi::ctypes::c_void>();
let success = unsafe { DetourUpdateProcessWithDll(process_handle, &mut dll_paths, 1) };
if success != TRUE {
return Err(io::Error::last_os_error());
}

let payload = Payload {
channel_conf: channel_conf.clone(),
asni_dll_path_with_nul: asni_dll_path_with_nul.to_bytes(),
};
let payload_bytes = bincode::encode_to_vec(payload, BINCODE_CONFIG).unwrap();
let success = unsafe {
DetourCopyPayloadToProcess(
process_handle,
&PAYLOAD_ID,
payload_bytes.as_ptr().cast(),
payload_bytes.len().try_into().unwrap(),
)
};
if success != TRUE {
return Err(io::Error::last_os_error());
}

let main_thread_handle = std_child.main_thread_handle();
let resume_thread_ret =
unsafe { ResumeThread(main_thread_handle.as_raw_handle().cast()) } as i32;

if resume_thread_ret == -1 {
return Err(io::Error::last_os_error());
}

Ok(std_child)
})?;
let mut spawn_success = false;
let spawn_success = &mut spawn_success;
let child = command
.spawn_with(|std_command| {
let std_child = std_command.spawn()?;
*spawn_success = true;

let mut dll_paths = asni_dll_path_with_nul.as_ptr().cast::<c_char>();
let process_handle = std_child.as_raw_handle().cast::<winapi::ctypes::c_void>();
let success = unsafe { DetourUpdateProcessWithDll(process_handle, &mut dll_paths, 1) };
if success != TRUE {
return Err(io::Error::last_os_error());
}

let payload = Payload {
channel_conf: channel_conf.clone(),
asni_dll_path_with_nul: asni_dll_path_with_nul.to_bytes(),
};
let payload_bytes = bincode::encode_to_vec(payload, BINCODE_CONFIG).unwrap();
let success = unsafe {
DetourCopyPayloadToProcess(
process_handle,
&PAYLOAD_ID,
payload_bytes.as_ptr().cast(),
payload_bytes.len().try_into().unwrap(),
)
};
if success != TRUE {
return Err(io::Error::last_os_error());
}

let main_thread_handle = std_child.main_thread_handle();
let resume_thread_ret =
unsafe { ResumeThread(main_thread_handle.as_raw_handle().cast()) } as i32;

if resume_thread_ret == -1 {
return Err(io::Error::last_os_error());
}

Ok(std_child)
})
.map_err(|err| {
if !*spawn_success {
SpawnError::InjectionError(err.into())
} else {
SpawnError::OsSpawnError(err.into())
}
})?;

Ok(TrackedChild { tokio_child: child, accesses_future })
}
Loading