diff --git a/Cargo.lock b/Cargo.lock index 614eedb0a..97b264b91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,7 +157,7 @@ checksum = "347ba98d9bd5467cf7e5b5ea0a90b509d9b589060b302fcc0d9ae6268ff7e02e" dependencies = [ "proc-macro2", "quote", - "rustc_version 0.4.1", + "rustc_version", "syn 2.0.117", ] @@ -1012,7 +1012,7 @@ dependencies = [ "convert_case 0.10.0", "proc-macro2", "quote", - "rustc_version 0.4.1", + "rustc_version", "syn 2.0.117", "unicode-xid", ] @@ -1354,7 +1354,6 @@ version = "0.1.0" dependencies = [ "allocator-api2", "anyhow", - "bstr", "bumpalo", "csv-async", "ctor", @@ -1571,7 +1570,6 @@ dependencies = [ "memmap2", "nix 0.31.2", "phf 0.13.1", - "stackalloc", "wincode", ] @@ -2217,7 +2215,7 @@ checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ "proc-macro2", "quote", - "rustc_version 0.4.1", + "rustc_version", "simd_cesu8", "syn 2.0.117", ] @@ -2608,7 +2606,7 @@ dependencies = [ "convert_case 0.11.0", "proc-macro2", "quote", - "semver 1.0.27", + "semver", "syn 2.0.117", ] @@ -3670,22 +3668,13 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.27", + "semver", ] [[package]] @@ -3841,27 +3830,12 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "serde" version = "1.0.228" @@ -4073,7 +4047,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ - "rustc_version 0.4.1", + "rustc_version", "simdutf8", ] @@ -4150,16 +4124,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "stackalloc" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b5b7088084b7f78305a42468c9a3693918620f0b8ec86260f1132dc0521e78" -dependencies = [ - "cc", - "rustc_version 0.2.3", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -5583,7 +5547,7 @@ dependencies = [ "bitflags 2.13.2", "hashbrown 0.15.5", "indexmap", - "semver 1.0.27", + "semver", ] [[package]] @@ -6083,7 +6047,7 @@ dependencies = [ "id-arena", "indexmap", "log", - "semver 1.0.27", + "semver", "serde", "serde_derive", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 766df61b1..8741ad0ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,6 @@ fspy_nostd_alloc = { path = "crates/fspy_nostd_alloc" } similar = "3.0.0" smallvec = "2.0.0-alpha.12" static_cell = "2" -stackalloc = "1.2.1" supports-color = "3.0.1" syn = "2" syscalls = { version = "0.8.0", default-features = false } diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index 16683d743..049050ac6 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -8,7 +8,6 @@ publish = false [dependencies] allocator-api2 = { workspace = true, features = ["alloc"] } wincode = { workspace = true } -bstr = { workspace = true, features = ["alloc", "std"] } bumpalo = { workspace = true, features = ["allocator-api2", "collections"] } derive_more = { workspace = true, features = ["debug"] } vt_materialized_artifact = { workspace = true } diff --git a/crates/fspy/src/command.rs b/crates/fspy/src/command.rs index 2f7801d2e..295a3c2d2 100644 --- a/crates/fspy/src/command.rs +++ b/crates/fspy/src/command.rs @@ -5,7 +5,9 @@ use std::{ }; #[cfg(unix)] -use fspy_shared_unix::exec::Exec; +use allocator_api2::alloc::Global; +#[cfg(unix)] +use fspy_shared_unix::exec::{Exec, to_vec_in}; use rustc_hash::FxHashMap; use tokio::process::Command as TokioCommand; use tokio_util::sync::CancellationToken; @@ -53,44 +55,44 @@ impl Command { #[cfg(unix)] #[must_use] - pub(crate) fn get_exec(&self) -> Exec { - use std::{ - iter::once, - os::unix::ffi::{OsStrExt, OsStringExt}, - }; - - use bstr::{BString, ByteSlice as _}; - let arg0 = - BString::from(self.arg0.clone().unwrap_or_else(|| self.program.clone()).into_vec()); + pub(crate) fn get_exec(&self) -> Exec { + use std::{iter::once, os::unix::ffi::OsStrExt}; + + let arg0 = self.arg0.as_ref().unwrap_or(&self.program); Exec { - program: self.program.as_bytes().into(), + program: to_vec_in(self.program.as_bytes(), Global), args: once(arg0) - .chain(self.args.iter().map(|arg| arg.as_bytes().as_bstr().to_owned())) + .chain(&self.args) + .map(|arg| to_vec_in(arg.as_bytes(), Global)) .collect(), envs: self .envs .iter() .map(|(name, value)| { - (name.inner().as_bytes().into(), Some(value.as_bytes().into())) + ( + to_vec_in(name.inner().as_bytes(), Global), + Some(to_vec_in(value.as_bytes(), Global)), + ) }) .collect(), } } #[cfg(unix)] - pub(crate) fn set_exec(&mut self, mut exec: Exec) { - use std::os::unix::ffi::OsStringExt; + pub(crate) fn set_exec(&mut self, mut exec: Exec) { + use std::os::unix::ffi::OsStrExt; - self.program = OsString::from_vec(exec.program.into()); - self.arg0 = Some(OsString::from_vec(exec.args.remove(0).into())); - self.args = exec.args.into_iter().map(|arg| OsString::from_vec(arg.into())).collect(); + let to_os_string = |bytes: &[u8]| OsStr::from_bytes(bytes).to_os_string(); + self.program = to_os_string(&exec.program); + self.arg0 = Some(to_os_string(&exec.args.remove(0))); + self.args = exec.args.iter().map(|arg| to_os_string(arg)).collect(); self.envs = exec .envs - .into_iter() + .iter() .map(|(name, value)| { ( - EnvName::new(OsString::from_vec(name.into())), - OsString::from_vec(value.unwrap_or_default().into()), + EnvName::new(to_os_string(name)), + to_os_string(value.as_deref().unwrap_or_default()), ) }) .collect(); diff --git a/crates/fspy_client_unix/src/lib.rs b/crates/fspy_client_unix/src/lib.rs index 0a0869286..9ba769751 100644 --- a/crates/fspy_client_unix/src/lib.rs +++ b/crates/fspy_client_unix/src/lib.rs @@ -87,32 +87,31 @@ impl<'a> Client<'a> { ipc_sender.send(&mode.with_path(path.into())); } - /// Resolves and reports an exec before forwarding its transformed arguments. + /// Resolves and reports an exec, returning the transformed arguments to + /// forward and what to run before forwarding them. /// /// # Safety /// /// `raw_exec` must contain the valid C strings and pointer arrays required - /// by [`RawExec::to_exec`]. The callback must not retain pointers from the - /// transformed `RawExec` after it returns. + /// by [`RawExec::to_exec`]. The returned `RawExec` points into + /// `allocator`, and must not be used after `allocator` is dropped. /// /// # Errors /// - /// Returns errors from exec resolution, platform preparation, or the - /// forwarding callback. - pub unsafe fn handle_exec( + /// Returns errors from exec resolution or platform preparation. + pub unsafe fn handle_exec( &self, config: ExecResolveConfig, raw_exec: RawExec, - allocator: impl Allocator, - f: impl FnOnce(RawExec, Option) -> nix::Result, - ) -> nix::Result { + allocator: &A, + ) -> nix::Result<(RawExec, Option)> { // SAFETY: raw_exec contains valid pointers to C strings and // null-terminated arrays, as provided by the caller. - let mut exec = unsafe { raw_exec.to_exec() }; + let mut exec = unsafe { raw_exec.to_exec(allocator) }; let pre_exec = handle_exec(&mut exec, config, &self.encoded_payload, |path| { self.send(AccessMode::Read, path); })?; - RawExec::from_exec(exec, allocator, |raw_command| f(raw_command, pre_exec)) + Ok((RawExec::from_exec(exec), pre_exec)) } /// Resolves and reports one intercepted file access. diff --git a/crates/fspy_client_unix/src/raw_exec.rs b/crates/fspy_client_unix/src/raw_exec.rs index 9ef4fb3bc..d7397c6f0 100644 --- a/crates/fspy_client_unix/src/raw_exec.rs +++ b/crates/fspy_client_unix/src/raw_exec.rs @@ -1,8 +1,7 @@ use std::{ffi::CStr, ptr::null}; -use allocator_api2::alloc::Allocator; -use bstr::{BStr, BString, ByteSlice}; -use fspy_shared_unix::exec::Exec; +use allocator_api2::{alloc::Allocator, vec::Vec}; +use fspy_shared_unix::exec::{Exec, to_vec_in}; #[derive(Clone, Copy)] pub struct RawExec { @@ -12,10 +11,11 @@ pub struct RawExec { } impl RawExec { - unsafe fn collect_c_str_array( + unsafe fn collect_c_str_array( strs: *const *const libc::c_char, - mut map_fn: impl FnMut(&BStr) -> T, - ) -> Vec { + allocator: A, + mut map_fn: impl FnMut(&[u8]) -> T, + ) -> Vec { let mut count = 0usize; let mut cur_str = strs; // SAFETY: cur_str points into a valid null-terminated array of C @@ -27,65 +27,66 @@ impl RawExec { cur_str = unsafe { cur_str.add(1) }; } - let mut str_vec = Vec::::with_capacity(count); + let mut str_vec = Vec::with_capacity_in(count, allocator); for i in 0..count { // SAFETY: i < count, so strs.add(i) is within the bounds of the // pointer array. let cur_str = unsafe { strs.add(i) }; // SAFETY: *cur_str is a non-null pointer to a valid // null-terminated C string, verified by the counting loop above. - str_vec.push(map_fn(unsafe { CStr::from_ptr(*cur_str) }.to_bytes().as_bstr())); + str_vec.push(map_fn(unsafe { CStr::from_ptr(*cur_str) }.to_bytes())); } str_vec } - pub fn to_c_str(mut s: BString, f: impl FnOnce(*const libc::c_char) -> R) -> R { + /// NUL-terminates `s` and leaks it into its allocator. + fn into_c_str(mut s: Vec) -> *const libc::c_char { s.push(0); - f(s.as_ptr().cast()) + s.leak().as_ptr().cast() } - fn to_c_str_array( - mut strs: Vec, - allocator: impl Allocator, - f: impl FnOnce(*const *const libc::c_char) -> R, - ) -> R { - // The pointer array exists only for the `f` call below, and building - // it must not go through libc malloc: exec runs in the child of - // `fork()` in multithreaded programs (`posix_spawn` forks then - // execs), where malloc's lock may be held by a thread that no longer - // exists. The interception's per-call bump has exactly this - // lifetime, and hands back the memory when the call ends. - let mut ptr_vec = allocator_api2::vec::Vec::with_capacity_in(strs.len() + 1, allocator); - for s in &mut strs { - s.push(0); - ptr_vec.push(s.as_ptr().cast::()); - } - ptr_vec.push(null()); - f(ptr_vec.as_ptr()) + /// Builds a NULL-terminated array of C strings, leaked into the strings' + /// allocator. + fn into_c_str_array( + strs: Vec, A>, + ) -> *const *const libc::c_char { + let mut ptrs = Vec::with_capacity_in(strs.len() + 1, strs.allocator().clone()); + ptrs.extend(strs.into_iter().map(Self::into_c_str)); + ptrs.push(null()); + ptrs.leak().as_ptr() } - /// Copies the raw exec arguments into owned Rust values. + /// Copies the raw exec arguments into `allocator`. /// /// # Safety /// /// `prog` must point to a valid C string. `argv` and `envp` must each /// point to a readable, null-terminated array of valid C string pointers. - pub unsafe fn to_exec(self) -> Exec { + pub unsafe fn to_exec(self, allocator: A) -> Exec { // SAFETY: self.prog is a non-null pointer to a valid null-terminated C // string, as guaranteed by the libc exec calling convention. - let program = unsafe { CStr::from_ptr(self.prog) }.to_bytes().as_bstr().to_owned(); + let program = to_vec_in(unsafe { CStr::from_ptr(self.prog) }.to_bytes(), allocator.clone()); // SAFETY: self.argv is a valid null-terminated array of C string // pointers, as guaranteed by the libc exec calling convention. - let args = unsafe { Self::collect_c_str_array(self.argv, BStr::to_owned) }; + let args = unsafe { + Self::collect_c_str_array(self.argv, allocator.clone(), |arg| { + to_vec_in(arg, allocator.clone()) + }) + }; // SAFETY: self.envp is a valid null-terminated array of C string // pointers, as guaranteed by the libc exec calling convention. let envs = unsafe { - Self::collect_c_str_array(self.envp, |env| { + Self::collect_c_str_array(self.envp, allocator.clone(), |env| { env.iter().position(|b| *b == b'=').map_or_else( - || (env.to_owned(), None), - |eq_pos| (env[..eq_pos].to_owned(), Some(env[(eq_pos + 1)..].to_owned())), + || (to_vec_in(env, allocator.clone()), None), + |eq_pos| { + ( + to_vec_in(&env[..eq_pos], allocator.clone()), + Some(to_vec_in(&env[(eq_pos + 1)..], allocator.clone())), + ) + }, ) }) }; @@ -93,24 +94,29 @@ impl RawExec { Exec { program, args, envs } } - pub fn from_exec(cmd: Exec, allocator: impl Allocator, f: impl FnOnce(Self) -> R) -> R { - let envs: Vec = cmd - .envs - .into_iter() - .map(|(name, value)| { - let mut env = name; - if let Some(value) = value { - env.push(b'='); - env.extend_from_slice(&value); - } - env - }) - .collect(); + /// Builds the C strings and pointer arrays for `cmd` in its allocator. + /// + /// They are leaked there rather than freed, so the pointers stay valid + /// until the allocator itself goes away. In the preload that is the + /// interception's per-call bump: exec runs in the child of `fork()` in + /// multithreaded programs, where libc malloc's lock may be held by a + /// thread that no longer exists, and the bump hands everything back + /// when the call ends. + pub fn from_exec(cmd: Exec) -> Self { + let mut envs = Vec::with_capacity_in(cmd.envs.len(), cmd.allocator().clone()); + for (name, value) in cmd.envs { + let mut env = name; + if let Some(value) = value { + env.push(b'='); + env.extend_from_slice(&value); + } + envs.push(env); + } - Self::to_c_str(cmd.program, |prog| { - Self::to_c_str_array(cmd.args, &allocator, |argv| { - Self::to_c_str_array(envs, &allocator, |envp| f(Self { prog, argv, envp })) - }) - }) + Self { + prog: Self::into_c_str(cmd.program), + argv: Self::into_c_str_array(cmd.args), + envp: Self::into_c_str_array(envs), + } } } diff --git a/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs b/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs index c94f81aa9..49ca99fea 100644 --- a/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs +++ b/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs @@ -60,26 +60,22 @@ fn handle_exec( let client = global_client().expect("exec unexpectedly called before client initialized in ctor"); // SAFETY: prog, argv, and envp are valid pointers to C strings/arrays forwarded from the interposed exec function - let result = unsafe { - client.handle_exec( - config, - RawExec { prog, argv, envp }, - allocator, - |raw_command, pre_exec| { - if let Some(pre_exec) = pre_exec { - pre_exec.run()?; - } - Ok(execve::original()(raw_command.prog, raw_command.argv, raw_command.envp)) - }, - ) - }; - match result { - Ok(ret) => ret, + let handled = unsafe { client.handle_exec(config, RawExec { prog, argv, envp }, &allocator) }; + let (raw_command, pre_exec) = match handled { + Ok(handled) => handled, Err(errno) => { errno.set(); - -1 + return -1; } + }; + if let Some(pre_exec) = pre_exec + && let Err(errno) = pre_exec.run() + { + errno.set(); + return -1; } + // SAFETY: raw_command's strings and arrays live in `allocator`, which outlives the call + unsafe { execve::original()(raw_command.prog, raw_command.argv, raw_command.envp) } } intercept!(execve(64): unsafe extern "C" fn( diff --git a/crates/fspy_preload_unix/src/interceptions/spawn/posix_spawn.rs b/crates/fspy_preload_unix/src/interceptions/spawn/posix_spawn.rs index e76373ff7..9e01cb1f3 100644 --- a/crates/fspy_preload_unix/src/interceptions/spawn/posix_spawn.rs +++ b/crates/fspy_preload_unix/src/interceptions/spawn/posix_spawn.rs @@ -42,45 +42,48 @@ unsafe fn handle_posix_spawn( let client = global_client() .expect("posix_spawn(p) unexpectedly called before client initialized in ctor"); + let arena = fspy_nostd_alloc::pooled_bump(); // SAFETY: file, argv, and envp are valid pointers forwarded from the interposed posix_spawn(p) function - let result = unsafe { - client.handle_exec::( + let handled = unsafe { + client.handle_exec( config, RawExec { prog: file, argv: argv.cast(), envp: envp.cast() }, - fspy_nostd_alloc::pooled_bump(), - |raw_command, pre_exec| { - let call_original = move || { - original( - pid, - raw_command.prog, - file_actions, - attrp, - raw_command.argv.cast(), - raw_command.envp.cast(), - ) - }; - if let Some(pre_exec) = pre_exec { - thread::scope(move |s| { - let call_original = AssertSend(call_original); - s.spawn(move || { - let call_original = call_original; - pre_exec.run()?; - - nix::Result::Ok((call_original.0)()) - }) - .join() - .unwrap() - }) - } else { - Ok(call_original()) - } - }, + &arena, ) }; - match result { - Err(errno) => errno as _, - Ok(ret) => ret, - } + let (raw_command, pre_exec) = match handled { + Ok(handled) => handled, + Err(errno) => return errno as _, + }; + let call_original = move || { + // SAFETY: the caller's pointers are forwarded unchanged, and raw_command's + // strings and arrays live in `arena`, which outlives the call + unsafe { + original( + pid, + raw_command.prog, + file_actions, + attrp, + raw_command.argv.cast(), + raw_command.envp.cast(), + ) + } + }; + let Some(pre_exec) = pre_exec else { + return call_original(); + }; + thread::scope(move |s| { + let call_original = AssertSend(call_original); + s.spawn(move || { + let call_original = call_original; + pre_exec.run()?; + + nix::Result::Ok((call_original.0)()) + }) + .join() + .unwrap() + }) + .unwrap_or_else(|errno| errno as _) } intercept!(posix_spawnp(64): PosixSpawnFn); diff --git a/crates/fspy_shared_unix/Cargo.toml b/crates/fspy_shared_unix/Cargo.toml index bbbbd9b69..eda98aaee 100644 --- a/crates/fspy_shared_unix/Cargo.toml +++ b/crates/fspy_shared_unix/Cargo.toml @@ -14,7 +14,6 @@ bstr = { workspace = true, features = ["alloc", "std"] } fspy_shared = { workspace = true } nix = { workspace = true, features = ["fs"] } fspy_nostd = { workspace = true } -stackalloc = { workspace = true } [dev-dependencies] diff --git a/crates/fspy_shared_unix/src/exec/mod.rs b/crates/fspy_shared_unix/src/exec/mod.rs index c0ca65f67..c931b1cbd 100644 --- a/crates/fspy_shared_unix/src/exec/mod.rs +++ b/crates/fspy_shared_unix/src/exec/mod.rs @@ -9,7 +9,8 @@ use std::{ path::Path, }; -use bstr::{BStr, BString, ByteSlice}; +use allocator_api2::{alloc::Allocator, vec::Vec}; +use bstr::{BStr, ByteSlice}; use nix::unistd::{AccessFlags, access}; use shebang::{ParseShebangOptions, parse_shebang}; @@ -49,12 +50,28 @@ impl<'a> ExecResolveConfig<'a> { } } +/// An environment entry: (name, value). value is None when the entry in +/// environ doesn't contain a `=` character. +pub type Env = (Vec, Option>); + +/// An exec call's program, arguments and environment. +/// +/// The strings live in `A`, so the preload can keep them in the +/// interception's bump rather than libc's heap: exec may run in the child of +/// `fork()`, where malloc's lock may be held by a thread that no longer +/// exists. +/// Copies `bytes` into a `Vec` in `allocator`. +pub fn to_vec_in(bytes: &[u8], allocator: A) -> Vec { + let mut vec = Vec::with_capacity_in(bytes.len(), allocator); + vec.extend_from_slice(bytes); + vec +} + #[derive(Debug)] -pub struct Exec { - pub program: BString, - pub args: Vec, - /// vec of (name, value). value is None when the entry in environ doesn't contain a `=` character. - pub envs: Vec<(BString, Option)>, +pub struct Exec { + pub program: Vec, + pub args: Vec, A>, + pub envs: Vec, A>, } fn getenv(name: &CStr) -> Option<&'static CStr> { @@ -82,7 +99,12 @@ fn peek_executable(path: &Path, buf: &mut [u8]) -> nix::Result { Ok(total_read_size) } -impl Exec { +impl Exec { + /// The allocator the strings live in. + pub fn allocator(&self) -> &A { + self.args.allocator() + } + /// Resolve the program path according to exec family semantics /// /// This method replicates the behavior of execve/execvp/execvP/execvpe for program resolution, @@ -114,16 +136,15 @@ impl Exec { |path| path.to_bytes().as_bstr(), ) }); - let program = which::which( - self.program.as_ref(), + self.program = which::which( + self.program.as_bstr(), path, |path| { on_path_read(Path::new(OsStr::from_bytes(path))); access(OsStr::from_bytes(path), AccessFlags::X_OK) }, - |program| Ok(program.to_owned()), + self.allocator().clone(), )?; - self.program = program; } self.parse_shebang(on_path_read, config.shebang_options)?; @@ -143,8 +164,9 @@ impl Exec { }, Path::new(OsStr::from_bytes(&self.program)), options, + self.allocator().clone(), )? { - self.args[0] = shebang.interpreter.clone(); + self.args[0].clone_from(&shebang.interpreter); let old_program = replace(&mut self.program, shebang.interpreter); self.args.splice(1..1, shebang.arguments.into_iter().chain(once(old_program))); } @@ -160,18 +182,20 @@ impl Exec { /// # Errors /// /// Returns `Err(nix::Error::EINVAL)` if the environment variable already exists with a different value. -pub fn ensure_env( - envs: &mut Vec<(BString, Option)>, +pub fn ensure_env( + envs: &mut Vec, A>, name: impl AsRef, value: impl AsRef, ) -> nix::Result<()> { let name = name.as_ref(); let value = value.as_ref(); - let existing_value = envs.iter().find_map(|(n, v)| if n == name { v.as_ref() } else { None }); + let existing_value = + envs.iter().find_map(|(n, v)| if n.as_bstr() == name { v.as_ref() } else { None }); if let Some(existing_value) = existing_value { - return if existing_value == value { Ok(()) } else { Err(nix::Error::EINVAL) }; + return if existing_value.as_bstr() == value { Ok(()) } else { Err(nix::Error::EINVAL) }; } - envs.push((name.to_owned(), Some(value.to_owned()))); + let allocator = envs.allocator().clone(); + envs.push((to_vec_in(name, allocator.clone()), Some(to_vec_in(value, allocator)))); Ok(()) } @@ -190,15 +214,16 @@ pub fn ensure_env( /// - Present otherwise: rewrites to `{existing}:{value}`. If `existing` is /// empty, sets to `value` alone to avoid a leading `:` (which glibc's /// `ld.so` interprets as the current directory). -pub fn append_path_env( - envs: &mut Vec<(BString, Option)>, +pub fn append_path_env( + envs: &mut Vec, A>, name: impl AsRef, value: impl AsRef, ) { let name = name.as_ref(); let value = value.as_ref(); - if let Some(entry) = envs.iter_mut().find(|(n, _)| n == name) { - let existing: &[u8] = entry.1.as_deref().map_or(&[][..], |v| v.as_ref()); + let allocator = envs.allocator().clone(); + if let Some(entry) = envs.iter_mut().find(|(n, _)| n.as_bstr() == name) { + let existing: &[u8] = entry.1.as_deref().unwrap_or_default(); let value_bytes: &[u8] = value.as_ref(); let already_last = existing == value_bytes || (existing.len() > value_bytes.len() @@ -207,28 +232,29 @@ pub fn append_path_env( if already_last { return; } - let mut new_value = Vec::with_capacity(existing.len() + 1 + value_bytes.len()); + let mut new_value = + Vec::with_capacity_in(existing.len() + 1 + value_bytes.len(), allocator); if !existing.is_empty() { new_value.extend_from_slice(existing); new_value.push(b':'); } new_value.extend_from_slice(value_bytes); - entry.1 = Some(BString::from(new_value)); + entry.1 = Some(new_value); } else { - envs.push((name.to_owned(), Some(value.to_owned()))); + envs.push((to_vec_in(name, allocator.clone()), Some(to_vec_in(value, allocator)))); } } #[cfg(test)] mod tests { - use bstr::BString; + use allocator_api2::{alloc::Global, vec::Vec}; + + use super::{append_path_env, to_vec_in}; - use super::append_path_env; + type Envs = Vec<(Vec, Option>)>; - fn env(envs: &[(BString, Option)], name: &[u8]) -> Option> { - envs.iter() - .find(|(n, _)| AsRef::<[u8]>::as_ref(n) == name) - .and_then(|(_, v)| v.as_ref().map(|v| AsRef::<[u8]>::as_ref(v).to_vec())) + fn env(envs: &Envs, name: &[u8]) -> Option> { + envs.iter().find(|(n, _)| n == name).and_then(|(_, v)| v.as_ref().map(|v| v.to_vec())) } /// (case, envs before the call, appended `LD_PRELOAD` value, expected value) @@ -270,9 +296,14 @@ mod tests { ("inserts when present with none value", &[("LD_PRELOAD", None)], "/a.so", b"/a.so"), ]; for &(case, existing, value, expected) in cases { - let mut envs: Vec<(BString, Option)> = existing + let mut envs: Envs = existing .iter() - .map(|&(name, value)| (BString::from(name), value.map(BString::from))) + .map(|&(name, value)| { + ( + to_vec_in(name.as_bytes(), Global), + value.map(|v| to_vec_in(v.as_bytes(), Global)), + ) + }) .collect(); append_path_env(&mut envs, "LD_PRELOAD", value); assert_eq!(env(&envs, b"LD_PRELOAD"), Some(expected.to_vec()), "{case}"); @@ -281,7 +312,7 @@ mod tests { #[test] fn idempotent_on_repeat() { - let mut envs: Vec<(BString, Option)> = vec![]; + let mut envs = Envs::new(); append_path_env(&mut envs, "LD_PRELOAD", "/a.so"); append_path_env(&mut envs, "LD_PRELOAD", "/a.so"); append_path_env(&mut envs, "LD_PRELOAD", "/a.so"); diff --git a/crates/fspy_shared_unix/src/exec/shebang.rs b/crates/fspy_shared_unix/src/exec/shebang.rs index 5c316a43d..7b65d698d 100644 --- a/crates/fspy_shared_unix/src/exec/shebang.rs +++ b/crates/fspy_shared_unix/src/exec/shebang.rs @@ -1,11 +1,14 @@ use std::path::Path; -use bstr::{BString, ByteSlice}; +use allocator_api2::{alloc::Allocator, vec::Vec}; +use bstr::ByteSlice; + +use super::to_vec_in; #[derive(Debug, Clone)] -pub struct Shebang { - pub interpreter: BString, - pub arguments: Vec, +pub struct Shebang { + pub interpreter: Vec, + pub arguments: Vec, A>, } const fn is_whitespace(c: u8) -> bool { @@ -61,7 +64,8 @@ impl Default for ParseShebangOptions { } } -/// Parses the shebang line of the executable at `path` the way the kernel does. +/// Parses the shebang line of the executable at `path` the way the kernel +/// does, copying the interpreter and arguments into `allocator`. /// /// Returns `ENOEXEC` where the kernel would refuse the script instead of /// running a truncated interpreter path. @@ -69,11 +73,12 @@ impl Default for ParseShebangOptions { /// # Panics /// /// Panics if `options.max_line_len` is larger than 512. -pub fn parse_shebang( +pub fn parse_shebang( mut peek_executable: impl FnMut(&Path, &mut [u8]) -> nix::Result, path: &Path, options: ParseShebangOptions, -) -> Result, nix::Error> { + allocator: A, +) -> Result>, nix::Error> { let mut buf = [0u8; MACOS_MAX_LINE_LEN]; let buf = &mut buf[..options.max_line_len]; @@ -110,21 +115,17 @@ pub fn parse_shebang( } let arguments_buf = line[interpreter.len()..].trim_ascii_start().as_bstr(); - let arguments: Vec = if options.split_arguments { - arguments_buf - .split(|ch| is_whitespace(*ch)) - .filter_map(|arg| { - let arg = arg.trim_ascii(); - if arg.is_empty() { None } else { Some(arg.as_bstr().to_owned()) } - }) - .collect() - } else if arguments_buf.is_empty() { - vec![] - } else { - vec![arguments_buf.to_owned()] - }; + let mut arguments = Vec::new_in(allocator.clone()); + if options.split_arguments { + arguments.extend(arguments_buf.split(|ch| is_whitespace(*ch)).filter_map(|arg| { + let arg = arg.trim_ascii(); + if arg.is_empty() { None } else { Some(to_vec_in(arg, allocator.clone())) } + })); + } else if !arguments_buf.is_empty() { + arguments.push(to_vec_in(arguments_buf, allocator.clone())); + } - Ok(Some(Shebang { interpreter: interpreter.as_bstr().to_owned(), arguments })) + Ok(Some(Shebang { interpreter: to_vec_in(interpreter, allocator), arguments })) } // #[derive(Debug)] @@ -223,7 +224,7 @@ pub fn parse_shebang( // let shebang = parse_shebang(&mut buf, "#! /bin/sh a \n".as_bytes()) // .unwrap() // .unwrap(); -// assert_eq!(shebang.interpreter, "/bin/sh"); +// assert_eq!(shebang.interpreter, b"/bin/sh"); // assert_eq!(shebang.arguments.as_one().as_bytes(), b"a"); // assert_eq!( // shebang @@ -241,7 +242,7 @@ pub fn parse_shebang( // let shebang = parse_shebang(&mut buf, "#! /bin/sh a b\tc \n".as_bytes()) // .unwrap() // .unwrap(); -// assert_eq!(shebang.interpreter, "/bin/sh"); +// assert_eq!(shebang.interpreter, b"/bin/sh"); // assert_eq!( // shebang // .arguments @@ -285,9 +286,11 @@ pub fn parse_shebang( mod tests { use std::path::Path; + use allocator_api2::alloc::Global; + use super::*; - fn parse(content: &[u8], options: ParseShebangOptions) -> nix::Result> { + fn parse(content: &[u8], options: ParseShebangOptions) -> nix::Result>> { parse_shebang( |_, buf| { let len = content.len().min(buf.len()); @@ -296,10 +299,11 @@ mod tests { }, Path::new("/script"), options, + Global, ) } - fn script_with_interpreter_len(len: usize) -> (Vec, Vec) { + fn script_with_interpreter_len(len: usize) -> (std::vec::Vec, std::vec::Vec) { let interpreter = [b"/".as_slice(), &vec![b'x'; len - 1]].concat(); let script = [b"#!".as_slice(), &interpreter, b"\necho hi\n"].concat(); (script, interpreter) @@ -310,7 +314,7 @@ mod tests { for options in [ParseShebangOptions::macos(), ParseShebangOptions::linux()] { let (script, interpreter) = script_with_interpreter_len(200); let shebang = parse(&script, options).unwrap().unwrap(); - assert_eq!(shebang.interpreter, interpreter); + assert_eq!(shebang.interpreter, interpreter.as_slice()); } } @@ -319,7 +323,7 @@ mod tests { // "#!" + 509 bytes + "\n" is exactly 512 bytes. let (script, interpreter) = script_with_interpreter_len(509); let shebang = parse(&script, ParseShebangOptions::macos()).unwrap().unwrap(); - assert_eq!(shebang.interpreter, interpreter); + assert_eq!(shebang.interpreter, interpreter.as_slice()); let (script, _) = script_with_interpreter_len(510); assert_eq!(parse(&script, ParseShebangOptions::macos()).unwrap_err(), nix::Error::ENOEXEC); @@ -337,8 +341,8 @@ mod tests { let script = [b"#!/bin/sh -e ".as_slice(), &[b' '; 200], b"# comment", &[b'x'; 400]].concat(); let shebang = parse(&script, ParseShebangOptions::macos()).unwrap().unwrap(); - assert_eq!(shebang.interpreter, "/bin/sh"); - assert_eq!(shebang.arguments, vec![BString::from("-e")]); + assert_eq!(shebang.interpreter, b"/bin/sh"); + assert_eq!(shebang.arguments, [b"-e"]); } #[test] @@ -346,7 +350,7 @@ mod tests { // "#!" + 253 bytes + "\n" is exactly 256 bytes. let (script, interpreter) = script_with_interpreter_len(253); let shebang = parse(&script, ParseShebangOptions::linux()).unwrap().unwrap(); - assert_eq!(shebang.interpreter, interpreter); + assert_eq!(shebang.interpreter, interpreter.as_slice()); let (script, _) = script_with_interpreter_len(254); assert_eq!(parse(&script, ParseShebangOptions::linux()).unwrap_err(), nix::Error::ENOEXEC); @@ -355,8 +359,8 @@ mod tests { #[test] fn linux_accepts_missing_newline_at_end_of_file() { let shebang = parse(b"#!/bin/sh -e", ParseShebangOptions::linux()).unwrap().unwrap(); - assert_eq!(shebang.interpreter, "/bin/sh"); - assert_eq!(shebang.arguments, vec![BString::from("-e")]); + assert_eq!(shebang.interpreter, b"/bin/sh"); + assert_eq!(shebang.arguments, [b"-e"]); } #[test] @@ -365,7 +369,7 @@ mod tests { let interpreter = [b"/".as_slice(), &[b'x'; 252]].concat(); let script = [b"#!".as_slice(), &interpreter, b" ", &[b'a'; 100]].concat(); let shebang = parse(&script, ParseShebangOptions::linux()).unwrap().unwrap(); - assert_eq!(shebang.interpreter, interpreter); + assert_eq!(shebang.interpreter, interpreter.as_slice()); assert!(shebang.arguments.is_empty()); } @@ -379,9 +383,9 @@ mod tests { fn linux_truncates_long_arguments() { let script = [b"#!/bin/sh ".as_slice(), &[b'a'; 300], b"\n"].concat(); let shebang = parse(&script, ParseShebangOptions::linux()).unwrap().unwrap(); - assert_eq!(shebang.interpreter, "/bin/sh"); + assert_eq!(shebang.interpreter, b"/bin/sh"); // 256-byte buffer, minus "#!/bin/sh " and the reserved last byte. - assert_eq!(shebang.arguments, vec![BString::from(vec![b'a'; 256 - 10 - 1])]); + assert_eq!(shebang.arguments, [[b'a'; 256 - 10 - 1]]); } #[test] diff --git a/crates/fspy_shared_unix/src/exec/which.rs b/crates/fspy_shared_unix/src/exec/which.rs index 2d96212cb..9f15f0725 100644 --- a/crates/fspy_shared_unix/src/exec/which.rs +++ b/crates/fspy_shared_unix/src/exec/which.rs @@ -1,84 +1,58 @@ +use allocator_api2::{alloc::Allocator, vec::Vec}; use bstr::{BStr, ByteSlice}; -use stackalloc::alloca; -fn concat(s: &[&BStr], callback: impl FnOnce(&BStr) -> R) -> R { - let size = s.iter().map(|s| s.len()).sum(); - alloca(size, |buf| { - debug_assert_eq!(buf.len(), size); - let mut pos = 0usize; - for s in s { - let next_pos = pos + s.len(); - let bytes: &[u8] = s.as_ref(); - let src_ptr = bytes.as_ptr(); - let dst_ptr = buf[pos..next_pos].as_mut_ptr().cast::(); - // SAFETY: `src_ptr` and `dst_ptr` are derived from valid slices of known lengths, - // they do not overlap (src is from the input slice, dst is from the stack-allocated buffer), - // and `s.len()` bytes are within bounds for both. - unsafe { - std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, s.len()); - } - pos = next_pos; - } - debug_assert_eq!(pos, buf.len()); - // SAFETY: `buf.as_ptr()` points to a valid allocation of `buf.len()` bytes that was - // fully initialized by the copy loop above (verified by the debug_assert). - let bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr().cast::(), buf.len()) }; - callback(bytes.as_bstr()) - }) -} +use super::to_vec_in; const NAME_MAX: usize = 255; -/// Search the executable in PATH. +/// Search the executable in PATH, returning its path in `allocator`. /// /// Referenced musl Implementation: /// /// Difference from musl: -/// - Instead of actually calling execve, use `access_executable` to check if the file is executable, and call `callback` with the found executable. +/// - Instead of actually calling execve, use `access_executable` to check if the file is executable, and return the found executable. /// - The path limit (`PATH_MAX`) is not checked. /// - PATH is passed as parameter instead of using the real environment variable. -pub fn which( +pub fn which( file: &BStr, path: &BStr, mut access_executable: impl FnMut(&BStr) -> nix::Result<()>, - callback: impl FnOnce(&BStr) -> nix::Result, -) -> nix::Result { + allocator: A, +) -> nix::Result> { use nix::Error; // 1. If file is empty, return ENOENT if file.is_empty() { return Err(Error::ENOENT); } - // 2. If file contains '/', call callback directly + // 2. If file contains '/', return it directly if file.contains(&b'/') { - return callback(file); + return Ok(to_vec_in(file, allocator)); } // 3. If file is too long, return ENAMETOOLONG if file.len() > NAME_MAX { return Err(Error::ENAMETOOLONG); } - // 4. Search PATH + // 4. Search PATH, building each candidate in the same buffer let mut seen_eacces = false; let mut last_err = Error::ENOENT; - let mut callback = Some(callback); + let mut candidate = Vec::new_in(allocator); for p in path.split(|ch| *ch == b':') { - let p = p.as_bstr(); - let result_to_return = concat( - // join with '/' if path is not empty - &[p, (if p.is_empty() { "" } else { "/" }).into(), file], - |path| match access_executable(path) { - Ok(()) => Some((callback.take().unwrap())(path)), - Err(err @ (Error::EACCES | Error::ENOENT | Error::ENOTDIR)) => { - if err == Error::EACCES { - seen_eacces = true; - } - last_err = err; - None + candidate.clear(); + candidate.extend_from_slice(p); + // join with '/' if path is not empty + if !p.is_empty() { + candidate.push(b'/'); + } + candidate.extend_from_slice(file); + match access_executable(candidate.as_bstr()) { + Ok(()) => return Ok(candidate), + Err(err @ (Error::EACCES | Error::ENOENT | Error::ENOTDIR)) => { + if err == Error::EACCES { + seen_eacces = true; } - Err(other_err) => Some(Err(other_err)), - }, - ); - if let Some(result) = result_to_return { - return result; + last_err = err; + } + Err(other_err) => return Err(other_err), } } Err(if seen_eacces { Error::EACCES } else { last_err }) @@ -86,16 +60,11 @@ pub fn which( #[cfg(test)] mod tests { - use bstr::{B, BStr}; + use allocator_api2::alloc::Global; + use bstr::B; use super::*; - #[test] - fn test_concat() { - let s = concat(&["a".into(), "bc".into(), "".into(), "e".into()], BStr::to_owned); - assert_eq!(s, "abce"); - } - fn mock_access<'a>( allowed: &'a [&'a str], fail_eacces: &'a [&'a str], @@ -158,8 +127,8 @@ mod tests { ]; for &(case, file, path, executable, fail_eacces, fail_enotdir, expected) in cases { let access = mock_access(executable, fail_eacces, fail_enotdir); - let res = - which(B(file).as_bstr(), B(path).as_bstr(), access, |found| Ok(found.to_owned())); + let res = which(B(file).as_bstr(), B(path).as_bstr(), access, Global) + .map(|found| found.as_bstr().to_owned()); assert_eq!(res, expected.map(|found| B(found).as_bstr().to_owned()), "{case}"); } } diff --git a/crates/fspy_shared_unix/src/spawn/linux/mod.rs b/crates/fspy_shared_unix/src/spawn/linux/mod.rs index 1bac0529e..bf610151b 100644 --- a/crates/fspy_shared_unix/src/spawn/linux/mod.rs +++ b/crates/fspy_shared_unix/src/spawn/linux/mod.rs @@ -1,6 +1,7 @@ #[cfg(not(target_env = "musl"))] use std::{ffi::OsStr, os::unix::ffi::OsStrExt as _, path::Path}; +use allocator_api2::alloc::Allocator; use fspy_seccomp_unotify::{payload::SeccompPayload, target::install_target}; #[cfg(not(target_env = "musl"))] use memmap2::Mmap; @@ -30,8 +31,8 @@ impl PreExec { } } -pub fn handle_exec( - command: &mut Exec, +pub fn handle_exec( + command: &mut Exec, encoded_payload: &EncodedPayload, ) -> nix::Result> { // On musl targets, LD_PRELOAD is not available (cdylib not supported). @@ -58,6 +59,8 @@ pub fn handle_exec( } } - command.envs.retain(|(name, _)| name != LD_PRELOAD && name != PAYLOAD_ENV_NAME); + command + .envs + .retain(|(name, _)| name != LD_PRELOAD.as_bytes() && name != PAYLOAD_ENV_NAME.as_bytes()); Ok(Some(PreExec(encoded_payload.payload.seccomp_payload.clone()))) } diff --git a/crates/fspy_shared_unix/src/spawn/macos.rs b/crates/fspy_shared_unix/src/spawn/macos.rs index eeab53e41..699622d81 100644 --- a/crates/fspy_shared_unix/src/spawn/macos.rs +++ b/crates/fspy_shared_unix/src/spawn/macos.rs @@ -1,14 +1,15 @@ use std::{ convert::Infallible, ffi::OsStr, - os::unix::ffi::{OsStrExt, OsStringExt}, + os::unix::ffi::OsStrExt, path::{Path, absolute}, }; +use allocator_api2::alloc::Allocator; use phf::{Set, phf_set}; use crate::{ - exec::{Exec, append_path_env, ensure_env}, + exec::{Exec, append_path_env, ensure_env, to_vec_in}, payload::{EncodedPayload, PAYLOAD_ENV_NAME}, }; @@ -24,8 +25,8 @@ impl PreExec { } } -pub fn handle_exec( - command: &mut Exec, +pub fn handle_exec( + command: &mut Exec, encoded_payload: &EncodedPayload, ) -> nix::Result> { const DYLD_INSERT_LIBRARIES: &[u8] = b"DYLD_INSERT_LIBRARIES"; @@ -33,7 +34,7 @@ pub fn handle_exec( if command.program.first() != Some(&b'/') { let program = absolute(OsStr::from_bytes(&command.program)).expect("Failed to get absolute path"); - command.program = program.into_os_string().into_vec().into(); + command.program = to_vec_in(program.as_os_str().as_bytes(), command.allocator().clone()); } let program_path = Path::new(OsStr::from_bytes(&command.program)); @@ -44,10 +45,16 @@ pub fn handle_exec( if matches!(parent.as_os_str().as_bytes(), b"/bin" | b"/usr/bin") { let artifacts = &encoded_payload.payload.artifacts; if matches!(file_name.as_bytes(), b"sh" | b"bash") { - command.program = artifacts.bash_path.as_os_str().as_bytes().into(); + command.program = to_vec_in( + artifacts.bash_path.as_os_str().as_bytes(), + command.allocator().clone(), + ); true } else if COREUTILS_FUNCTIONS.contains(file_name.as_bytes()) { - command.program = artifacts.coreutils_path.as_os_str().as_bytes().into(); + command.program = to_vec_in( + artifacts.coreutils_path.as_os_str().as_bytes(), + command.allocator().clone(), + ); true } else { false diff --git a/crates/fspy_shared_unix/src/spawn/mod.rs b/crates/fspy_shared_unix/src/spawn/mod.rs index 4e957636a..1f3467cd5 100644 --- a/crates/fspy_shared_unix/src/spawn/mod.rs +++ b/crates/fspy_shared_unix/src/spawn/mod.rs @@ -8,6 +8,7 @@ mod os_specific; use std::{ffi::OsStr, os::unix::ffi::OsStrExt, path::Path}; +use allocator_api2::alloc::Allocator; #[doc(hidden)] #[cfg(target_os = "macos")] pub use os_specific::COREUTILS_FUNCTIONS as COREUTILS_FUNCTIONS_FOR_TEST; @@ -33,8 +34,8 @@ use crate::{ /// # Panics /// /// Panics if the current working directory cannot be determined when converting a relative path to absolute. -pub fn handle_exec( - command: &mut Exec, +pub fn handle_exec( + command: &mut Exec, config: ExecResolveConfig, encoded_payload: &EncodedPayload, mut on_path_read: impl FnMut(&Path),