From 6144412ff4561e0d8b73105681f163d1fee6f803 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Fri, 14 Aug 2026 15:42:59 +0800 Subject: [PATCH] refactor(fspy-nostd-alloc): rename arena to pooled_bump The crate is infrastructure and should name its exports after their mechanism, not after how its consumers happen to use them: arena() is a bump allocator backed by the process-wide chunk pool, so call it pooled_bump() and its settings PooledBumpSettings. The crate docs now describe the three layers in the same mechanism-first terms. pooled_bump() also returns impl BumpAllocator + Allocator instead of impl Allocator, with the bump-scope trait re-exported: callers get the bump interface (scopes, resets) without a direct bump-scope dependency, not just an anonymous allocator. Co-Authored-By: Claude Fable 5 --- crates/fspy_client_unix/src/lib.rs | 2 +- crates/fspy_client_unix/src/raw_exec.rs | 2 +- crates/fspy_nostd_alloc/src/lib.rs | 92 ++++++++++--------- .../src/interceptions/spawn/exec/mod.rs | 2 +- crates/fspy_shared/src/ipc/channel/mod.rs | 2 +- 5 files changed, 52 insertions(+), 48 deletions(-) diff --git a/crates/fspy_client_unix/src/lib.rs b/crates/fspy_client_unix/src/lib.rs index 3ba3877a1..066e6e07c 100644 --- a/crates/fspy_client_unix/src/lib.rs +++ b/crates/fspy_client_unix/src/lib.rs @@ -120,7 +120,7 @@ impl Client { // SAFETY: mode contains a valid pointer (if ModeStr) or a plain value, // as provided by the caller. let mode = unsafe { mode.to_access_mode() }; - let arena = fspy_nostd_alloc::arena(); + let arena = fspy_nostd_alloc::pooled_bump(); let Some(abs_path) = path.to_absolute_path(&arena)? else { return Ok(()); }; diff --git a/crates/fspy_client_unix/src/raw_exec.rs b/crates/fspy_client_unix/src/raw_exec.rs index c3a287b2f..e7e6c4cf8 100644 --- a/crates/fspy_client_unix/src/raw_exec.rs +++ b/crates/fspy_client_unix/src/raw_exec.rs @@ -53,7 +53,7 @@ impl RawExec { // execs), where malloc's lock may be held by a thread that no longer // exists. A per-call arena has exactly this lifetime, and hands back // the memory when the call ends. - let arena = fspy_nostd_alloc::arena(); + let arena = fspy_nostd_alloc::pooled_bump(); let mut ptr_vec = allocator_api2::vec::Vec::with_capacity_in(strs.len() + 1, &arena); for s in &mut strs { s.push(0); diff --git a/crates/fspy_nostd_alloc/src/lib.rs b/crates/fspy_nostd_alloc/src/lib.rs index 98fabbe80..868de9818 100644 --- a/crates/fspy_nostd_alloc/src/lib.rs +++ b/crates/fspy_nostd_alloc/src/lib.rs @@ -2,15 +2,14 @@ //! //! Taking malloc's lock is the classic way for interposed code to deadlock a //! traced program (see the crate docs), so the preload library allocates -//! through this module instead. It stacks three layers and exposes only the -//! top one, [`arena`]. A stateless page allocator is the bottom: every -//! allocation is fresh pages from [`fspy_nostd::mm`] — an anonymous mapping -//! on Unix, a `VirtualAlloc` region on Windows. -//! `ChunkPool` sits on top of it and caches fixed-size chunks, so that -//! frequent short tracing calls can reuse memory instead of paying two -//! syscalls per call. [`arena`] creates one `bump_scope::Bump` per -//! intercepted call, drawing its chunks from the process-wide pool and -//! returning them on drop. +//! through this crate instead. It stacks three layers: a stateless page +//! allocator at the bottom, where every allocation is fresh pages from +//! [`fspy_nostd::mm`] — an anonymous mapping on Unix, a `VirtualAlloc` +//! region on Windows; a process-wide `ChunkPool` above it that caches +//! fixed-size chunks; and `bump_scope::Bump`s on top. [`pooled_bump`] +//! creates a bump that draws its chunks from the pool and returns them on +//! drop, so frequent short-lived bumps reuse memory instead of paying two +//! syscalls each. #![cfg_attr(not(test), no_std)] @@ -24,6 +23,10 @@ mod pool; mod virtual_alloc; use allocator_api2::alloc::Allocator; +/// The bump interface [`pooled_bump`] returns, re-exported so callers can +/// name the bound and call its methods without a direct bump-scope +/// dependency. +pub use bump_scope::traits::BumpAllocator; use bump_scope::{ Bump, alloc::compat::AllocatorApi2V02Compat, @@ -38,8 +41,8 @@ pub(crate) use virtual_alloc::VirtualAllocator as PageAllocator; /// Every cached chunk is 64 KiB: a whole multiple of the page size on all /// supported targets, and big enough that most intercepted calls fit their -/// allocations into a single chunk. [`ArenaSettings`] pins the arenas' own -/// chunk sizing to this same value. +/// allocations into a single chunk. [`PooledBumpSettings`] pins the bumps' +/// own chunk sizing to this same value. const CHUNK_SIZE: usize = 64 * 1024; /// The alignment chunks are allocated with. Must be at least the alignment /// `bump_scope::Bump` uses for its chunk requests — 16 (see @@ -56,11 +59,12 @@ const SLOTS: usize = 64; /// first allocation on — even before any constructor has run. static CHUNK_POOL: ChunkPool = ChunkPool::new(); -/// The `Bump` settings the arenas use — the defaults, with two changes: +/// The `Bump` settings [`pooled_bump`] uses — the defaults, with two +/// changes: /// -/// - `WithGuaranteedAllocated`: an arena starts life without a chunk, +/// - `WithGuaranteedAllocated`: a bump starts life without a chunk, /// so creating one allocates nothing. -/// - `WithMinimumChunkSize`: the arena's first chunk request is +/// - `WithMinimumChunkSize`: the bump's first chunk request is /// sized to the pool's chunks, making the coupling explicit — rather than /// relying on the pool rounding the default 512-byte first request up to a /// whole chunk. (Both end up serving the same memory: the pool answers any @@ -70,10 +74,10 @@ static CHUNK_POOL: ChunkPool = Ch /// minimum — which is exactly what keeps a minimum-sized request within /// the pool's `size <= CHUNK_SIZE` gate; the /// `bump_chunk_requests_fit_the_pool_gates` test pins that fit. -type ArenaSettings = <::WithGuaranteedAllocated as BumpAllocatorSettings>::WithMinimumChunkSize; +type PooledBumpSettings = <::WithGuaranteedAllocated as BumpAllocatorSettings>::WithMinimumChunkSize; /// `Bump::unallocated` requires its base allocator to implement `Default` -/// (an arena without chunks has nowhere to store an allocator value, so it +/// (a bump without chunks has nowhere to store an allocator value, so it /// conjures one on first use). Point defaulted references at the /// process-wide pool. As an allocator, `&ChunkPool` already works through /// allocator-api2's blanket `impl Allocator for &A`. @@ -83,25 +87,24 @@ impl Default for &'static ChunkPool impl Allocator { +pub fn pooled_bump() -> impl BumpAllocator + Allocator { Bump::< AllocatorApi2V02Compat<&'static ChunkPool>, - ArenaSettings, + PooledBumpSettings, >::unallocated() } @@ -115,16 +118,17 @@ mod tests { /// Pins the fit between `Bump`'s chunk requests and the pool's gates /// (alignment at most [`CHUNK_ALIGN`], size at most [`CHUNK_SIZE`]), - /// under the same [`ArenaSettings`] the arenas use — including that a - /// request under `WithMinimumChunkSize` still fits the + /// under the same [`PooledBumpSettings`] the bumps use — including that + /// a request under `WithMinimumChunkSize` still fits the /// `size <= CHUNK_SIZE` gate. bump-scope keeps its request parameters /// private, so this test is the enforcement: it fails if an upgrade /// ever changes them. - /// [`ArenaSettings`] with `GuaranteedAllocated` flipped back on: without - /// it, `Bump` demands a `Default` base allocator, and a reference to the - /// test's stack-local pool cannot provide one. Chunk request sizing — - /// what the test pins — is unaffected by that flag. - type TestSettings = ::WithGuaranteedAllocated; + /// [`PooledBumpSettings`] with `GuaranteedAllocated` flipped back on: + /// without it, `Bump` demands a `Default` base allocator, and a + /// reference to the test's stack-local pool cannot provide one. Chunk + /// request sizing — what the test pins — is unaffected by that flag. + type TestSettings = + ::WithGuaranteedAllocated; #[test] fn bump_chunk_requests_fit_the_pool_gates() { @@ -153,25 +157,25 @@ mod tests { #[test] #[cfg(not(miri))] - fn arena_allocates_and_returns_chunks_to_the_pool() { + fn pooled_bump_allocates_and_returns_chunks_to_the_pool() { let layout = Layout::from_size_align(100, 8).unwrap(); - let first_arena = arena(); - let first = first_arena.allocate(layout).unwrap(); + let first_bump = pooled_bump(); + let first = first_bump.allocate(layout).unwrap(); assert!(first.len() >= 100); // SAFETY: fresh exclusive block of at least 100 bytes. unsafe { first.cast::().as_ptr().write_bytes(0x5A, 100) }; - let second = first_arena.allocate(layout).unwrap(); + let second = first_bump.allocate(layout).unwrap(); assert_ne!(first.cast::().as_ptr().addr(), second.cast::().as_ptr().addr()); let first_addr = first.cast::().as_ptr().addr(); // Everything dies at once; the chunk goes back to the pool. - drop(first_arena); + drop(first_bump); - // No other test touches the process-wide pool, so a new arena draws + // No other test touches the process-wide pool, so a new bump draws // the same chunk back and its first allocation lands at the same // address. - let second_arena = arena(); - let again = second_arena.allocate(layout).unwrap(); + let second_bump = pooled_bump(); + let again = second_bump.allocate(layout).unwrap(); assert_eq!(again.cast::().as_ptr().addr(), first_addr); } } 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 aac3faf0c..bd1506a37 100644 --- a/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs +++ b/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs @@ -192,7 +192,7 @@ mod linux_only { reason = "suppresses unused warning on *::original" )] let _unused = execveat::original; - let arena = fspy_nostd_alloc::arena(); + let arena = fspy_nostd_alloc::pooled_bump(); // SAFETY: dirfd and pathname are valid arguments from the interposed execveat call. let path = unsafe { PathAt::borrow_raw(dirfd, pathname) }; diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index 6a0da0fde..d04c7f8ad 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -178,7 +178,7 @@ impl ChannelConf { // The arena never touches the process heap, so this stays safe in // the preload contexts that create senders (pre-`main` constructors, // the Windows loader lock). - let arena = fspy_nostd_alloc::arena(); + let arena = fspy_nostd_alloc::pooled_bump(); let shm_path = self .shm_id .to_os_c_string_in(&arena)