Skip to content
Closed
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
8 changes: 8 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ materialized_artifact = { path = "crates/materialized_artifact" }
materialized_artifact_build = { path = "crates/materialized_artifact_build" }
flate2 = "1.0.35"
fspy = { path = "crates/fspy" }
fspy_alloc = { path = "crates/fspy_alloc" }
fspy_benchmark_launcher = { path = "crates/fspy_benchmark_launcher", artifact = "bin" }
fspy_benchmark_target = { path = "crates/fspy_benchmark_target", artifact = "bin" }
fspy_detours_sys = { path = "crates/fspy_detours_sys" }
Expand Down Expand Up @@ -120,6 +121,7 @@ ref-cast = "1.0.24"
regex = "1.11.3"
rusqlite = "0.39.0"
rustc-hash = "2.1.1"
rustix = { version = "1", default-features = false, features = ["mm", "param", "use-libc-auxv"] }
# 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
14 changes: 14 additions & 0 deletions crates/fspy_alloc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "fspy_alloc"
edition = "2024"
license.workspace = true
publish = false

[lib]
doctest = false

[target.'cfg(unix)'.dependencies]
rustix = { workspace = true }

[lints]
workspace = true
38 changes: 38 additions & 0 deletions crates/fspy_alloc/src/class.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! Size-class policy: which layouts the pool serves, and at what block size.

use core::alloc::Layout;

const MIN_CLASS_SHIFT: u32 = 4;
const MAX_CLASS_SHIFT: u32 = 16;
pub const CLASS_COUNT: usize = (MAX_CLASS_SHIFT - MIN_CLASS_SHIFT) as usize + 1;
const MIN_BLOCK_SIZE: usize = 1 << MIN_CLASS_SHIFT;
pub const MAX_BLOCK_SIZE: usize = 1 << MAX_CLASS_SHIFT;
/// Block areas start at this alignment within a slab, making it the largest
/// alignment the pool can serve; stricter layouts map directly.
pub const MAX_POOL_ALIGN: usize = 4096;

pub const fn block_size(class: usize) -> usize {
1 << (MIN_CLASS_SHIFT as usize + class)
}

/// Returns the size class for `layout`, or `None` if the request must be
/// mapped directly (too large or over-aligned).
pub const fn class_of(layout: Layout) -> Option<usize> {
if layout.align() > MAX_POOL_ALIGN {
return None;
}
let mut size = layout.size();
// A block of `size >= align` at a `min(block size, 4 KiB)` boundary is
// aligned to `align` (both are powers of two and `align <= 4 KiB`).
if size < layout.align() {
size = layout.align();
}
if size < MIN_BLOCK_SIZE {
size = MIN_BLOCK_SIZE;
}
if size > MAX_BLOCK_SIZE {
return None;
}
let shift = size.next_power_of_two().trailing_zeros();
Some((shift - MIN_CLASS_SHIFT) as usize)
}
124 changes: 124 additions & 0 deletions crates/fspy_alloc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! Lock-free, async-signal-safe global allocator for the fspy preload library.
//!
//! The preload library interposes libc functions that POSIX declares
//! async-signal-safe (`open`, `stat`, `execve`, ...). Programs may call these
//! from signal handlers, and — more commonly — from the child of `fork()` in a
//! multithreaded process, where only async-signal-safe calls are permitted:
//! the libc allocator's locks may be held forever by threads that no longer
//! exist after the fork. Routing the preload's Rust allocations through this
//! allocator keeps them safe in both contexts:
//!
//! - **No locks.** Every state transition is a lock-free compare-and-swap
//! loop: an attempt only retries because another running thread completed
//! its operation, so nothing ever waits on state that a thread which
//! vanished at `fork()` — or sits suspended under a signal handler — would
//! have to release. (Lock-free, not wait-free: an individual operation has
//! no fixed retry bound under active contention.)
//! - **No thread-locals.** TLS first-touch allocates through libc malloc on
//! some platforms (macOS thread-local variables), which would reintroduce
//! the hazard this crate exists to remove.
//! - **mmap-backed.** Memory comes straight from the kernel. On Linux the
//! allocator relies on nothing from libc: mapping syscalls are issued
//! directly (rustix's raw backend) and even the page size is discovered by
//! probing with raw syscalls. On macOS, which has no stable raw-syscall
//! ABI, calls go through the thin libSystem stubs. libc malloc is never
//! called anywhere.
//!
//! Design: power-of-two size classes (16 B ..= 64 KiB) carve blocks out of
//! 1 MiB slabs; freed blocks recycle through a per-class Treiber free list
//! made ABA-safe by a generation tag. Requests larger than the biggest class
//! (or over-aligned beyond 4 KiB) map and unmap directly. See the `pool`
//! module for the details.
//!
//! Because the allocator is a `const`-initialized static with no lazy setup,
//! it works from the very first allocation in the process — even before the
//! preload library's constructor runs.

#![cfg_attr(not(test), no_std)]

// Compile as an empty crate on non-unix targets: the allocator backs the unix
// preload library. A Windows backend can be added alongside `sys::Mmap` if
// the Windows preload ever needs one.

#[cfg(unix)]
mod class;
#[cfg(unix)]
mod mapping;
#[cfg(unix)]
mod mmap;
#[cfg(unix)]
mod pool;
#[cfg(unix)]
mod slab;
#[cfg(unix)]
mod sys;

#[cfg(unix)]
use core::{
alloc::{GlobalAlloc, Layout},
ptr::{self, NonNull},
};

#[cfg(unix)]
use crate::{mmap::Mmap, pool::Pool};

/// A lock-free, async-signal-safe, fork-safe [`GlobalAlloc`] implementation.
///
/// Intended to be installed as the `#[global_allocator]` of the fspy preload
/// library. All memory comes from anonymous mappings; libc malloc is never
/// called, no locks are taken, and no thread-local state is used.
///
/// Capacity is bounded by design: each size class can hold at most 256 slabs
/// of 1 MiB (roughly 200 MiB per class). Requests beyond that — far outside
/// anything the preload library does — fail like any other out-of-memory
/// condition (`alloc` returns null).
#[cfg(unix)]
pub struct FspyAlloc {
pool: Pool<Mmap>,
}

#[cfg(unix)]
impl FspyAlloc {
/// Creates the allocator. `const` so it can back a `static` with no
/// runtime initialization.
#[must_use]
pub const fn new() -> Self {
Self { pool: Pool::new() }
}
}

#[cfg(unix)]
impl Default for FspyAlloc {
fn default() -> Self {
Self::new()
}
}

// SAFETY: `Pool` hands out blocks that are non-null, at least `layout.size()`
// bytes large, aligned to at least `layout.align()`, and exclusively owned
// until returned via `dealloc`. Allocation failure is reported as null, and
// none of the methods unwind.
#[cfg(unix)]
unsafe impl GlobalAlloc for FspyAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
self.pool.alloc(layout).map_or(ptr::null_mut(), NonNull::as_ptr)
}

unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
let Some(ptr) = NonNull::new(ptr) else { return };
// SAFETY: per the GlobalAlloc contract, `ptr` was returned by this
// allocator for this `layout`.
unsafe { self.pool.dealloc(ptr, layout) }
}

unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
self.pool.alloc_zeroed(layout).map_or(ptr::null_mut(), NonNull::as_ptr)
}

unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let Some(ptr) = NonNull::new(ptr) else { return ptr::null_mut() };
// SAFETY: per the GlobalAlloc contract, `ptr` was returned by this
// allocator for this `layout`, and `new_size` is non-zero.
unsafe { self.pool.realloc(ptr, layout, new_size) }.map_or(ptr::null_mut(), NonNull::as_ptr)
}
}
62 changes: 62 additions & 0 deletions crates/fspy_alloc/src/mapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Owned memory regions obtained from a [`Sys`] provider.

use core::{marker::PhantomData, mem, ptr::NonNull};

use crate::sys::Sys;

/// An owned region obtained from `S`, released on drop.
///
/// This is the only place that calls [`Sys::unmap`]: pool code either lets a
/// `Mapping` drop (probe scratch, install races, freed large allocations) or
/// deliberately leaks it with [`Mapping::into_raw`] (published slabs, live
/// large allocations). Reconstructing ownership from a raw pointer via
/// [`Mapping::from_raw`] is the single unsafe step.
pub struct Mapping<S: Sys> {
ptr: NonNull<u8>,
size: usize,
align: usize,
sys: PhantomData<fn() -> S>,
}

impl<S: Sys> Mapping<S> {
/// Maps `size` bytes of zero-initialized memory aligned to `align`
/// (a power of two). Returns `None` when memory is exhausted.
pub fn new(size: usize, align: usize) -> Option<Self> {
let ptr = S::map(size, align)?;
Some(Self { ptr, size, align, sys: PhantomData })
}

/// Reclaims ownership of a mapping previously released with
/// [`Mapping::into_raw`].
///
/// # Safety
///
/// `ptr` must have come from `Mapping::<S>::into_raw` (or `Sys::map`)
/// with exactly this `size` and `align`, the region must not be in use,
/// and ownership must not be reclaimed twice.
pub unsafe fn from_raw(ptr: NonNull<u8>, size: usize, align: usize) -> Self {
Self { ptr, size, align, sys: PhantomData }
}

/// The mapped region's base address.
pub const fn ptr(&self) -> NonNull<u8> {
self.ptr
}

/// Releases ownership without unmapping; the region lives until (unless)
/// [`Mapping::from_raw`] reclaims it.
pub const fn into_raw(self) -> NonNull<u8> {
let ptr = self.ptr;
mem::forget(self);
ptr
}
}

impl<S: Sys> Drop for Mapping<S> {
fn drop(&mut self) {
// SAFETY: this type owns the mapping (constructed from `Sys::map`
// directly or via the `from_raw` contract), and after drop nothing
// can use it.
unsafe { S::unmap(self.ptr, self.size, self.align) }
}
}
Loading
Loading