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

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ getrandom = "0.4.2"
itoa = "1.0.17"
jsonc-parser = "0.32.0"
libc = "0.2.185"
linux-raw-sys = { version = "0.12", default-features = false }
libtest-mimic = "0.8.2"
memmap2 = "0.9.11"
monostate = "1.0.2"
Expand Down Expand Up @@ -126,7 +127,6 @@ ref-cast = "1.0.24"
regex = "1.11.3"
rusqlite = "0.39.0"
rustc-hash = "2.1.1"
rustix = { version = "1", default-features = false }
# SeccompAction::UserNotif (SECCOMP_RET_USER_NOTIF) was added after the latest published release (v0.5.0)
seccompiler = { git = "https://github.com/rust-vmm/seccompiler", rev = "08587106340b8e3cb361c7561411510039436857" }
serde = "1.0.219"
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy_client_unix/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::ffi::CStr;

use allocator_api2::{alloc::Allocator, vec::Vec};
use bstr::ByteSlice;
use fspy_nostd::{AsRawFd as _, BorrowedFd, CWD};
use fspy_nostd::{BorrowedFd, CWD};
use fspy_shared::ipc::AccessMode;
use libc::{c_char, c_int};

Expand Down
19 changes: 3 additions & 16 deletions crates/fspy_nostd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,15 @@ doctest = false
# Only the borrowed `BStr` type is used. Keeping every feature disabled makes
# the environment iterator usable without `alloc` or `std`.
bstr = { workspace = true }

[target.'cfg(unix)'.dependencies]
rustix = { workspace = true, features = ["fs", "mm"] }
bitflags = { workspace = true }

[target.'cfg(target_os = "macos")'.dependencies]
libc = { workspace = true }

# On Linux the page size is probed from the kernel directly (see param.rs);
# rustix's `param` is only needed where sysconf is the platform interface.
[target.'cfg(all(unix, not(target_os = "linux")))'.dependencies]
rustix = { workspace = true, features = ["param"] }

# The compile-time backend check in lib.rs needs a `linux_raw`-gated rustix
# item to reference; `runtime` is the module that has one.
[target.'cfg(target_os = "linux")'.dependencies]
[target.'cfg(any(target_os = "linux", target_os = "none"))'.dependencies]
# Parsing remains allocation-free and no-std; `atoi` enables `std` by default.
atoi = { version = "3.1.0", default-features = false }
rustix = { workspace = true, features = ["runtime"] }
linux-raw-sys = { workspace = true, features = ["errno", "general", "no_std"] }
syscalls = { workspace = true }

[target.'cfg(windows)'.dependencies]
Expand All @@ -41,9 +32,5 @@ windows-sys = { workspace = true, features = [
"Win32_System_Memory",
] }

# Cross-validates the page-size probe against rustix's auxv-based answer.
[target.'cfg(target_os = "linux")'.dev-dependencies]
rustix = { workspace = true, features = ["param"] }

[lints]
workspace = true
22 changes: 17 additions & 5 deletions crates/fspy_nostd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Low-level operations for fspy code that runs before a process runtime is ready or in a context where normal runtime code can deadlock.

The current implementation supports Linux, macOS, and Windows.
The current implementation supports Linux, `target_os = "none"` code injected
into Linux, macOS, and Windows.

## Execution contexts

Expand Down Expand Up @@ -34,7 +35,12 @@ Preload code can also run before `main`, while libc and the target runtime are s

The injected runtime cannot assume that the target process has libc. Linux kernel operations in `fspy_nostd` use raw syscalls, so injected code can reuse them without linking or calling libc.

The final injected artifact must also reject libc references that dependencies or compiler-generated memory operations introduce. The raw-syscall check covers the `rustix` backend, not the complete artifact link.
For this crate, `target_os = "none"` means code that runs inside a Linux
process. It uses the same Linux kernel ABI as the normal Linux build.

The final injected artifact must also reject libc references introduced by
other dependencies or compiler-generated memory operations. This crate's
Linux operations alone cannot enforce the complete artifact link.

## API rules

Expand All @@ -46,11 +52,17 @@ Every exported operation follows these rules:

Code that needs allocation uses an explicit allocator. [`fspy_nostd_alloc`](../fspy_nostd_alloc) provides one based on memory mappings.

## Linux raw-syscall enforcement
## Platform boundaries

`rustix` can use libc instead of raw syscalls. A dependency can select that backend through feature unification or `RUSTFLAGS`.
- Linux and `none` issue syscalls directly and obtain ABI constants and
structures from `linux-raw-sys`.
- macOS calls libSystem through `libc`, as required by the platform.
- Windows calls Win32 directly.

[`lib.rs`](src/lib.rs) references `rustix::runtime`, which exists only with the raw Linux backend. Selecting the libc backend makes `fspy_nostd` fail to compile.
`Error` owns the raw platform error code. On macOS and Windows it additionally
provides `Error::last_os_error()` because their APIs report failure through a
sentinel and put the reason in thread-local state. Linux and `none` syscalls
return their error directly, so that method deliberately does not exist there.

## Modules

Expand Down
2 changes: 1 addition & 1 deletion crates/fspy_nostd/src/c_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub type WideCStr<'a, R> = CStr<'a, R, u16>;

/// A borrowed NUL-terminated string of the platform's native path code
/// units: bytes on Unix and wide (`u16`) code units on Windows.
#[cfg(unix)]
#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))]
pub type OsCStr<'a, R> = CStr<'a, R>;
/// A borrowed NUL-terminated string of the platform's native path code
/// units: bytes on Unix and wide (`u16`) code units on Windows.
Expand Down
17 changes: 10 additions & 7 deletions crates/fspy_nostd/src/env/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ use core::{ffi::CStr as CoreCStr, num::NonZeroUsize, ptr, slice};

use atoi::FromRadix10Checked as _;
use bstr::{BStr, ByteSlice as _};
use rustix::fs::{Mode, OFlags};

use super::Entry;
use crate::{CStr, CWD, Error, Fat, Result};
use crate::{
CStr, CWD, Error, Fat, Result, Thin,
fs::{Mode, OFlags},
};

#[derive(Clone, Copy)]
struct Bounds {
Expand Down Expand Up @@ -147,9 +149,8 @@ impl Iterator for FatEnvs {
///
/// This opens and reads `/proc/self/stat` once into fixed stack storage and
/// parses `arg_start`, `arg_end`, `env_start`, and `env_end` together. It does
/// not allocate, and rustix's raw Linux backend makes the file operations
/// direct syscalls. Iterators subsequently created from the snapshot perform
/// no syscalls.
/// not allocate, and the file operations are direct syscalls. Iterators
/// subsequently created from the snapshot perform no syscalls.
///
/// # Errors
///
Expand All @@ -174,7 +175,9 @@ fn read_bounds() -> Result<Bounds> {
const STAT_PATH: &CoreCStr = c"/proc/self/stat";
const STAT_CAPACITY: usize = 4096;

let fd = rustix::fs::openat(CWD, STAT_PATH, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())?;
// SAFETY: `STAT_PATH` is a static NUL-terminated byte string.
let path = unsafe { CStr::<Thin>::from_ptr(STAT_PATH.as_ptr().cast()) };
let fd = crate::fs::openat(CWD, path, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())?;
let mut stat = [0; STAT_CAPACITY];
let mut initialized = 0;

Expand All @@ -186,7 +189,7 @@ fn read_bounds() -> Result<Bounds> {
return Err(Error::OVERFLOW);
}

let Some(read) = NonZeroUsize::new(rustix::io::read(&fd, remaining)?) else {
let Some(read) = NonZeroUsize::new(crate::io::read(fd.as_fd(), remaining)?) else {
break;
};
initialized = initialized.checked_add(read.get()).ok_or(Error::OVERFLOW)?;
Expand Down
4 changes: 2 additions & 2 deletions crates/fspy_nostd/src/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ use bstr::BStr;

use crate::{CStr, Fat};

#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "none"))]
mod linux;
#[cfg(target_os = "macos")]
mod mac;

#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "none"))]
pub use linux::{Current, FatArgs, FatEnvs, current};
#[cfg(target_os = "macos")]
pub use mac::{Current, FatArgs, FatEnvs, ThinArgs, ThinEnvs, args, current, envs};
Expand Down
104 changes: 104 additions & 0 deletions crates/fspy_nostd/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#[cfg(target_os = "macos")]
use libc::__error;
#[cfg(windows)]
use windows_sys::Win32::Foundation::GetLastError;

/// An operating-system error code.
#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(transparent)]
pub struct Error(i32);

/// An operating-system error code.
#[cfg(windows)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(transparent)]
pub struct Error(u32);

#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))]
impl Error {
pub const BADF: Self = Self(errno::BADF);
pub const INVAL: Self = Self(errno::INVAL);
pub const NOENT: Self = Self(errno::NOENT);
pub const NOMEM: Self = Self(errno::NOMEM);
pub const OVERFLOW: Self = Self(errno::OVERFLOW);
pub const RANGE: Self = Self(errno::RANGE);

/// Creates an error from a raw errno value.
#[must_use]
pub const fn from_raw_os_error(code: i32) -> Self {
Self(code)
}

/// Returns the raw errno value.
#[must_use]
pub const fn raw_os_error(self) -> i32 {
self.0
}

/// Returns the calling thread's last operating-system error.
///
/// Call this immediately after the failing libc call: anything in between,
/// including drops, can overwrite the thread-local error code.
#[cfg(target_os = "macos")]
#[must_use]
pub fn last_os_error() -> Self {
// SAFETY: libSystem exposes the calling thread's errno through this
// non-null pointer.
Self::from_raw_os_error(unsafe { __error().read() })
}
}

#[cfg(windows)]
impl Error {
/// Creates an error from a raw Windows error code.
#[must_use]
pub const fn from_raw_os_error(code: u32) -> Self {
Self(code)
}

/// Returns the raw Windows error code.
#[must_use]
pub const fn raw_os_error(self) -> u32 {
self.0
}

/// Returns the calling thread's last operating-system error.
///
/// Call this immediately after the failing Win32 call: anything in
/// between, including drops, can overwrite the thread-local error code.
#[must_use]
pub fn last_os_error() -> Self {
// SAFETY: `GetLastError` reads thread-local error state.
Self::from_raw_os_error(unsafe { GetLastError() })
}
}

#[cfg(any(target_os = "linux", target_os = "none"))]
impl From<syscalls::Errno> for Error {
fn from(error: syscalls::Errno) -> Self {
Self::from_raw_os_error(error.into_raw())
}
}

#[cfg(any(target_os = "linux", target_os = "none"))]
mod errno {
pub const BADF: i32 = linux_raw_sys::errno::EBADF.cast_signed();
pub const INVAL: i32 = linux_raw_sys::errno::EINVAL.cast_signed();
pub const NOMEM: i32 = linux_raw_sys::errno::ENOMEM.cast_signed();
pub const NOENT: i32 = linux_raw_sys::errno::ENOENT.cast_signed();
pub const OVERFLOW: i32 = linux_raw_sys::errno::EOVERFLOW.cast_signed();
pub const RANGE: i32 = linux_raw_sys::errno::ERANGE.cast_signed();
}

#[cfg(target_os = "macos")]
mod errno {
pub const BADF: i32 = libc::EBADF;
pub const INVAL: i32 = libc::EINVAL;
pub const NOMEM: i32 = libc::ENOMEM;
pub const NOENT: i32 = libc::ENOENT;
pub const OVERFLOW: i32 = libc::EOVERFLOW;
pub const RANGE: i32 = libc::ERANGE;
}

pub type Result<T> = core::result::Result<T, Error>;
Loading
Loading