Skip to content

Commit a2c9208

Browse files
wan9chiclaude
andauthored
refactor(fspy-nostd-alloc): rename arena to pooled_bump (#672)
## Motivation `fspy_nostd_alloc` is infrastructure: it 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 this renames it to `pooled_bump()` (and `ArenaSettings` to `PooledBumpSettings`), and rewrites the crate docs in the same mechanism-first terms. `pooled_bump()` now also returns `impl BumpAllocator + Allocator` instead of `impl Allocator`, with the bump-scope trait re-exported: callers get the bump interface (scopes, resets) rather than an anonymous allocator, without taking a direct bump-scope dependency. Groundwork for the payload-view PR on top of this stack, which adds a page-backed sibling with the same signature. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c83bad8 commit a2c9208

5 files changed

Lines changed: 52 additions & 48 deletions

File tree

crates/fspy_client_unix/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ impl Client {
120120
// SAFETY: mode contains a valid pointer (if ModeStr) or a plain value,
121121
// as provided by the caller.
122122
let mode = unsafe { mode.to_access_mode() };
123-
let arena = fspy_nostd_alloc::arena();
123+
let arena = fspy_nostd_alloc::pooled_bump();
124124
let Some(abs_path) = path.to_absolute_path(&arena)? else {
125125
return Ok(());
126126
};

crates/fspy_client_unix/src/raw_exec.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ impl RawExec {
5353
// execs), where malloc's lock may be held by a thread that no longer
5454
// exists. A per-call arena has exactly this lifetime, and hands back
5555
// the memory when the call ends.
56-
let arena = fspy_nostd_alloc::arena();
56+
let arena = fspy_nostd_alloc::pooled_bump();
5757
let mut ptr_vec = allocator_api2::vec::Vec::with_capacity_in(strs.len() + 1, &arena);
5858
for s in &mut strs {
5959
s.push(0);

crates/fspy_nostd_alloc/src/lib.rs

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
//!
33
//! Taking malloc's lock is the classic way for interposed code to deadlock a
44
//! traced program (see the crate docs), so the preload library allocates
5-
//! through this module instead. It stacks three layers and exposes only the
6-
//! top one, [`arena`]. A stateless page allocator is the bottom: every
7-
//! allocation is fresh pages from [`fspy_nostd::mm`] — an anonymous mapping
8-
//! on Unix, a `VirtualAlloc` region on Windows.
9-
//! `ChunkPool` sits on top of it and caches fixed-size chunks, so that
10-
//! frequent short tracing calls can reuse memory instead of paying two
11-
//! syscalls per call. [`arena`] creates one `bump_scope::Bump` per
12-
//! intercepted call, drawing its chunks from the process-wide pool and
13-
//! returning them on drop.
5+
//! through this crate instead. It stacks three layers: a stateless page
6+
//! allocator at the bottom, where every allocation is fresh pages from
7+
//! [`fspy_nostd::mm`] — an anonymous mapping on Unix, a `VirtualAlloc`
8+
//! region on Windows; a process-wide `ChunkPool` above it that caches
9+
//! fixed-size chunks; and `bump_scope::Bump`s on top. [`pooled_bump`]
10+
//! creates a bump that draws its chunks from the pool and returns them on
11+
//! drop, so frequent short-lived bumps reuse memory instead of paying two
12+
//! syscalls each.
1413
1514
#![cfg_attr(not(test), no_std)]
1615

@@ -24,6 +23,10 @@ mod pool;
2423
mod virtual_alloc;
2524

2625
use allocator_api2::alloc::Allocator;
26+
/// The bump interface [`pooled_bump`] returns, re-exported so callers can
27+
/// name the bound and call its methods without a direct bump-scope
28+
/// dependency.
29+
pub use bump_scope::traits::BumpAllocator;
2730
use bump_scope::{
2831
Bump,
2932
alloc::compat::AllocatorApi2V02Compat,
@@ -38,8 +41,8 @@ pub(crate) use virtual_alloc::VirtualAllocator as PageAllocator;
3841

3942
/// Every cached chunk is 64 KiB: a whole multiple of the page size on all
4043
/// supported targets, and big enough that most intercepted calls fit their
41-
/// allocations into a single chunk. [`ArenaSettings`] pins the arenas' own
42-
/// chunk sizing to this same value.
44+
/// allocations into a single chunk. [`PooledBumpSettings`] pins the bumps'
45+
/// own chunk sizing to this same value.
4346
const CHUNK_SIZE: usize = 64 * 1024;
4447
/// The alignment chunks are allocated with. Must be at least the alignment
4548
/// `bump_scope::Bump` uses for its chunk requests — 16 (see
@@ -56,11 +59,12 @@ const SLOTS: usize = 64;
5659
/// first allocation on — even before any constructor has run.
5760
static CHUNK_POOL: ChunkPool<PageAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS> = ChunkPool::new();
5861

59-
/// The `Bump` settings the arenas use — the defaults, with two changes:
62+
/// The `Bump` settings [`pooled_bump`] uses — the defaults, with two
63+
/// changes:
6064
///
61-
/// - `WithGuaranteedAllocated<false>`: an arena starts life without a chunk,
65+
/// - `WithGuaranteedAllocated<false>`: a bump starts life without a chunk,
6266
/// so creating one allocates nothing.
63-
/// - `WithMinimumChunkSize<CHUNK_SIZE>`: the arena's first chunk request is
67+
/// - `WithMinimumChunkSize<CHUNK_SIZE>`: the bump's first chunk request is
6468
/// sized to the pool's chunks, making the coupling explicit — rather than
6569
/// relying on the pool rounding the default 512-byte first request up to a
6670
/// whole chunk. (Both end up serving the same memory: the pool answers any
@@ -70,10 +74,10 @@ static CHUNK_POOL: ChunkPool<PageAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS> = Ch
7074
/// minimum — which is exactly what keeps a minimum-sized request within
7175
/// the pool's `size <= CHUNK_SIZE` gate; the
7276
/// `bump_chunk_requests_fit_the_pool_gates` test pins that fit.
73-
type ArenaSettings = <<BumpSettings as BumpAllocatorSettings>::WithGuaranteedAllocated<false> as BumpAllocatorSettings>::WithMinimumChunkSize<CHUNK_SIZE>;
77+
type PooledBumpSettings = <<BumpSettings as BumpAllocatorSettings>::WithGuaranteedAllocated<false> as BumpAllocatorSettings>::WithMinimumChunkSize<CHUNK_SIZE>;
7478

7579
/// `Bump::unallocated` requires its base allocator to implement `Default`
76-
/// (an arena without chunks has nowhere to store an allocator value, so it
80+
/// (a bump without chunks has nowhere to store an allocator value, so it
7781
/// conjures one on first use). Point defaulted references at the
7882
/// process-wide pool. As an allocator, `&ChunkPool` already works through
7983
/// allocator-api2's blanket `impl Allocator for &A`.
@@ -83,25 +87,24 @@ impl Default for &'static ChunkPool<PageAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOT
8387
}
8488
}
8589

86-
/// Creates a fresh bump arena for one intercepted call, backed by the
87-
/// process-wide chunk pool.
90+
/// Creates a fresh bump backed by the process-wide chunk pool.
8891
///
89-
/// Creating the arena allocates nothing; the first allocation grabs a whole
90-
/// chunk — usually a recycled one, so most calls touch no syscalls at all.
91-
/// Deallocation only takes back the most recent allocation (bump-arena
92-
/// semantics); everything is freed at once when the arena is dropped, and
92+
/// Creating the bump allocates nothing; the first allocation grabs a whole
93+
/// chunk — usually a recycled one, so most uses touch no syscalls at all.
94+
/// Deallocation only takes back the most recent allocation (bump
95+
/// semantics); everything is freed at once when the bump is dropped, and
9396
/// its chunks go back to the pool.
9497
///
95-
/// The arena itself is single-owner — use one per call, do not share it
96-
/// across threads. Creating one is safe anywhere, any time: the pool
97-
/// underneath works in signal handlers, in the child of `fork()`, and under
98-
/// the Windows loader lock (see `ChunkPool` and the platform page allocators
99-
/// in this crate's source for why).
98+
/// The bump is single-owner — do not share it across threads. Creating one
99+
/// is safe anywhere, any time: the pool underneath works in signal
100+
/// handlers, in the child of `fork()`, and under the Windows loader lock
101+
/// (see `ChunkPool` and the platform page allocators in this crate's source
102+
/// for why).
100103
#[must_use]
101-
pub fn arena() -> impl Allocator {
104+
pub fn pooled_bump() -> impl BumpAllocator + Allocator {
102105
Bump::<
103106
AllocatorApi2V02Compat<&'static ChunkPool<PageAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS>>,
104-
ArenaSettings,
107+
PooledBumpSettings,
105108
>::unallocated()
106109
}
107110

@@ -115,16 +118,17 @@ mod tests {
115118

116119
/// Pins the fit between `Bump`'s chunk requests and the pool's gates
117120
/// (alignment at most [`CHUNK_ALIGN`], size at most [`CHUNK_SIZE`]),
118-
/// under the same [`ArenaSettings`] the arenas use — including that a
119-
/// request under `WithMinimumChunkSize<CHUNK_SIZE>` still fits the
121+
/// under the same [`PooledBumpSettings`] the bumps use — including that
122+
/// a request under `WithMinimumChunkSize<CHUNK_SIZE>` still fits the
120123
/// `size <= CHUNK_SIZE` gate. bump-scope keeps its request parameters
121124
/// private, so this test is the enforcement: it fails if an upgrade
122125
/// ever changes them.
123-
/// [`ArenaSettings`] with `GuaranteedAllocated` flipped back on: without
124-
/// it, `Bump` demands a `Default` base allocator, and a reference to the
125-
/// test's stack-local pool cannot provide one. Chunk request sizing —
126-
/// what the test pins — is unaffected by that flag.
127-
type TestSettings = <ArenaSettings as BumpAllocatorSettings>::WithGuaranteedAllocated<true>;
126+
/// [`PooledBumpSettings`] with `GuaranteedAllocated` flipped back on:
127+
/// without it, `Bump` demands a `Default` base allocator, and a
128+
/// reference to the test's stack-local pool cannot provide one. Chunk
129+
/// request sizing — what the test pins — is unaffected by that flag.
130+
type TestSettings =
131+
<PooledBumpSettings as BumpAllocatorSettings>::WithGuaranteedAllocated<true>;
128132

129133
#[test]
130134
fn bump_chunk_requests_fit_the_pool_gates() {
@@ -153,25 +157,25 @@ mod tests {
153157

154158
#[test]
155159
#[cfg(not(miri))]
156-
fn arena_allocates_and_returns_chunks_to_the_pool() {
160+
fn pooled_bump_allocates_and_returns_chunks_to_the_pool() {
157161
let layout = Layout::from_size_align(100, 8).unwrap();
158162

159-
let first_arena = arena();
160-
let first = first_arena.allocate(layout).unwrap();
163+
let first_bump = pooled_bump();
164+
let first = first_bump.allocate(layout).unwrap();
161165
assert!(first.len() >= 100);
162166
// SAFETY: fresh exclusive block of at least 100 bytes.
163167
unsafe { first.cast::<u8>().as_ptr().write_bytes(0x5A, 100) };
164-
let second = first_arena.allocate(layout).unwrap();
168+
let second = first_bump.allocate(layout).unwrap();
165169
assert_ne!(first.cast::<u8>().as_ptr().addr(), second.cast::<u8>().as_ptr().addr());
166170
let first_addr = first.cast::<u8>().as_ptr().addr();
167171
// Everything dies at once; the chunk goes back to the pool.
168-
drop(first_arena);
172+
drop(first_bump);
169173

170-
// No other test touches the process-wide pool, so a new arena draws
174+
// No other test touches the process-wide pool, so a new bump draws
171175
// the same chunk back and its first allocation lands at the same
172176
// address.
173-
let second_arena = arena();
174-
let again = second_arena.allocate(layout).unwrap();
177+
let second_bump = pooled_bump();
178+
let again = second_bump.allocate(layout).unwrap();
175179
assert_eq!(again.cast::<u8>().as_ptr().addr(), first_addr);
176180
}
177181
}

crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ mod linux_only {
192192
reason = "suppresses unused warning on *::original"
193193
)]
194194
let _unused = execveat::original;
195-
let arena = fspy_nostd_alloc::arena();
195+
let arena = fspy_nostd_alloc::pooled_bump();
196196

197197
// SAFETY: dirfd and pathname are valid arguments from the interposed execveat call.
198198
let path = unsafe { PathAt::borrow_raw(dirfd, pathname) };

crates/fspy_shared/src/ipc/channel/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ impl ChannelConf {
178178
// The arena never touches the process heap, so this stays safe in
179179
// the preload contexts that create senders (pre-`main` constructors,
180180
// the Windows loader lock).
181-
let arena = fspy_nostd_alloc::arena();
181+
let arena = fspy_nostd_alloc::pooled_bump();
182182
let shm_path = self
183183
.shm_id
184184
.to_os_c_string_in(&arena)

0 commit comments

Comments
 (0)