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
3 changes: 3 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions crates/fspy_ipc_str/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ publish = false
rust-version.workspace = true

[dependencies]
allocator-api2 = { workspace = true, features = ["alloc"] }
bumpalo = { workspace = true }
bytemuck = { workspace = true, features = ["must_cast", "derive"] }
fspy_nostd = { workspace = true }
fspy_nostd_alloc = { workspace = true }
wincode = { workspace = true }

[lints]
Expand Down
54 changes: 54 additions & 0 deletions crates/fspy_ipc_str/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ use std::os::windows::ffi::OsStrExt as _;
use std::os::windows::ffi::OsStringExt as _;
use std::{borrow::Cow, ffi::OsStr, fmt::Debug, mem::MaybeUninit};

use allocator_api2::alloc::Allocator;
use bumpalo::Bump;
#[cfg(windows)]
use bytemuck::must_cast_slice;
use bytemuck::{TransparentWrapper, TransparentWrapperAlloc};
use fspy_nostd::{Fat, OsCStr};
use fspy_nostd_alloc::OsCString;
use wincode::{
SchemaRead, SchemaWrite,
config::Config,
Expand Down Expand Up @@ -90,6 +93,57 @@ impl IpcStr {
pub fn clone_in<'bump>(&self, bump: &'bump Bump) -> &'bump Self {
Self::wrap_ref(bump.alloc_slice_copy(&self.data))
}

/// Copies this IPC string into a box.
#[must_use]
pub fn to_boxed(&self) -> Box<Self> {
Self::wrap_box(self.data.into())
}

/// Creates an IPC string that borrows the code units of `path`, without
/// its NUL terminator.
///
/// This is the inverse of [`to_os_c_string_in`](Self::to_os_c_string_in);
/// neither direction goes through [`OsStr`], so both work without std.
#[must_use]
pub fn from_os_c_str(path: OsCStr<'_, Fat>) -> &Self {
#[cfg(unix)]
return Self::wrap_ref(path.as_units());
#[cfg(windows)]
return Self::wrap_ref(must_cast_slice(path.as_units()));
}

/// Decodes this IPC string into an owned NUL-terminated platform C
/// string allocated in `allocator`.
///
/// Returns [`None`] when the contents cannot name a path: an odd byte
/// length on Windows, or an interior NUL code unit.
#[must_use]
pub fn to_os_c_string_in<A: Allocator>(&self, allocator: A) -> Option<OsCString<Fat, A>> {
#[cfg(unix)]
{
let mut units =
allocator_api2::vec::Vec::with_capacity_in(self.data.len() + 1, allocator);
units.extend_from_slice(&self.data);
units.push(0);
OsCString::from_vec_with_nul(units)
}
#[cfg(windows)]
{
if !self.data.len().is_multiple_of(2) {
return None;
}
let len = self.data.len() / 2;
let mut units = allocator_api2::vec::Vec::with_capacity_in(len + 1, allocator);
units.resize(len, 0);
// The destination is aligned `u16` storage; viewing it as bytes
// sidesteps the source's unspecified alignment (see the field
// docs).
bytemuck::must_cast_slice_mut::<u16, u8>(&mut units).copy_from_slice(&self.data);
units.push(0);
OsCString::from_vec_with_nul(units)
}
}
}

impl Debug for IpcStr {
Expand Down
55 changes: 52 additions & 3 deletions crates/fspy_nostd/src/mm/windows.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use core::{ffi::c_void, ptr};
use core::{
ffi::c_void,
ptr::{self, NonNull},
};

use bitflags::bitflags;
use windows_sys::Win32::System::Memory::{
CreateFileMappingW, FILE_MAP_READ, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile,
PAGE_READWRITE, UnmapViewOfFile,
CreateFileMappingW, FILE_MAP_READ, FILE_MAP_WRITE, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE,
MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, PAGE_READWRITE, UnmapViewOfFile, VirtualAlloc,
VirtualFree,
};

use crate::{BorrowedHandle, OwnedHandle, Result, SecurityAttributes};
Expand All @@ -28,6 +32,51 @@ bitflags! {
/// Permit writes to the view.
const WRITE = FILE_MAP_WRITE;
}

/// How `VirtualAlloc` claims address space.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AllocationType: u32 {
/// Reserve address space.
const RESERVE = MEM_RESERVE;
/// Commit pages within reserved address space.
const COMMIT = MEM_COMMIT;
}
}

/// Calls `VirtualAlloc` at a system-chosen address and returns the new
/// region's base.
///
/// Committed pages read as zero until written.
///
/// # Errors
///
/// Returns the error reported by `VirtualAlloc`.
pub fn virtual_alloc(
size: usize,
allocation_type: AllocationType,
protection: PageProtection,
) -> Result<NonNull<c_void>> {
// SAFETY: a system-chosen address cannot alias an existing allocation,
// and Windows validates the size, allocation type, and protection.
let address =
unsafe { VirtualAlloc(ptr::null(), size, allocation_type.bits(), protection as u32) };
NonNull::new(address).ok_or_else(crate::windows::last_error)
}

/// Calls `VirtualFree` with `MEM_RELEASE`, releasing the whole region.
///
/// # Errors
///
/// Returns the error reported by `VirtualFree`.
///
/// # Safety
///
/// `address` must be the base of a region returned by [`virtual_alloc`] that
/// has not been released, with no live references into it.
pub unsafe fn virtual_free(address: NonNull<c_void>) -> Result<()> {
// SAFETY: the caller guarantees that `address` is an unreleased region
// base; releasing passes zero as the size.
crate::windows::bool_result(unsafe { VirtualFree(address.as_ptr(), 0, MEM_RELEASE) })
}

/// An owned view of a file-mapping object.
Expand Down
4 changes: 1 addition & 3 deletions crates/fspy_nostd_alloc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ doctest = false

[dependencies]
allocator-api2 = { workspace = true, features = ["alloc"] }
fspy_nostd = { workspace = true }

[target.'cfg(unix)'.dependencies]
bump-scope = { workspace = true }
fspy_nostd = { workspace = true }

[lints]
workspace = true
37 changes: 16 additions & 21 deletions crates/fspy_nostd_alloc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
//! 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`]. `MmapAllocator` is the bottom: a stateless allocator
//! where every allocation is a fresh anonymous mapping from [`fspy_nostd::mm`].
//! 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
Expand All @@ -18,46 +19,42 @@ mod c_string;
pub mod fs;
#[cfg(unix)]
mod mmap;
#[cfg(unix)]
mod pool;
#[cfg(windows)]
mod virtual_alloc;

#[cfg(unix)]
use allocator_api2::alloc::Allocator;
#[cfg(unix)]
use bump_scope::{
Bump,
alloc::compat::AllocatorApi2V02Compat,
settings::{BumpAllocatorSettings, BumpSettings},
};
pub use c_string::{CString, OsCString};
#[cfg(unix)]
use mmap::MmapAllocator;
#[cfg(unix)]
pub(crate) use mmap::MmapAllocator as PageAllocator;
use pool::ChunkPool;
#[cfg(windows)]
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.
#[cfg(unix)]
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
/// [`MmapAllocator`]'s docs for the links). bump-scope does not export that
/// `MmapAllocator`'s docs for the links). bump-scope does not export that
/// constant, so the `bump_chunk_requests_fit_the_pool_gates` test pins the
/// fit instead: it fails if a bump-scope upgrade ever requests chunks the
/// pool would refuse.
#[cfg(unix)]
const CHUNK_ALIGN: usize = 16;
/// At most this many chunks stay cached, capping retained memory at
/// `SLOTS * CHUNK_SIZE` = 4 MiB.
#[cfg(unix)]
const SLOTS: usize = 64;

/// The process-wide chunk pool. `const`-initialized, so it works from the
/// first allocation on — even before any constructor has run.
#[cfg(unix)]
static CHUNK_POOL: ChunkPool<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS> = ChunkPool::new();
static CHUNK_POOL: ChunkPool<PageAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS> = ChunkPool::new();

/// The `Bump` settings the arenas use — the defaults, with two changes:
///
Expand All @@ -73,16 +70,14 @@ static CHUNK_POOL: ChunkPool<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS> = 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.
#[cfg(unix)]
type ArenaSettings = <<BumpSettings as BumpAllocatorSettings>::WithGuaranteedAllocated<false> as BumpAllocatorSettings>::WithMinimumChunkSize<CHUNK_SIZE>;

/// `Bump::unallocated` requires its base allocator to implement `Default`
/// (an arena 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`.
#[cfg(unix)]
impl Default for &'static ChunkPool<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS> {
impl Default for &'static ChunkPool<PageAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS> {
fn default() -> Self {
&CHUNK_POOL
}
Expand All @@ -99,18 +94,18 @@ impl Default for &'static ChunkPool<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOT
///
/// The arena itself is single-owner — use one per call, do not share it
/// across threads. Creating one is safe anywhere, any time: the pool
/// underneath works in signal handlers and in the child of `fork()` (see
/// `ChunkPool` and `MmapAllocator` in this crate's source for why).
#[cfg(unix)]
/// underneath works in signal handlers, in the child of `fork()`, and under
/// the Windows loader lock (see `ChunkPool` and the platform page allocators
/// in this crate's source for why).
#[must_use]
pub fn arena() -> impl Allocator {
Bump::<
AllocatorApi2V02Compat<&'static ChunkPool<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS>>,
AllocatorApi2V02Compat<&'static ChunkPool<PageAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS>>,
ArenaSettings,
>::unallocated()
}

#[cfg(all(test, unix))]
#[cfg(test)]
mod tests {
use core::alloc::Layout;

Expand Down
16 changes: 8 additions & 8 deletions crates/fspy_nostd_alloc/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ use core::{

use allocator_api2::alloc::{AllocError, Allocator};

use super::mmap::MmapAllocator;
use super::PageAllocator;

/// A fixed-size cache of memory chunks.
///
/// Sits between the bump arenas and the underlying allocator `A` —
/// [`MmapAllocator`] in real use, any allocator in tests — so the chunks
/// the platform `PageAllocator` in real use, any allocator in tests — so the chunks
/// that a finished arena gives back reach the next arena without going back
/// to the kernel.
///
/// The parameters are chosen by the layer above (the arena layer): every
/// cached chunk is exactly `CHUNK_SIZE` bytes and allocated with
/// `CHUNK_ALIGN`, which is also the strictest alignment the pool accepts
/// (`bump_scope::Bump` requests 16 — see [`MmapAllocator`]'s docs for the
/// (`bump_scope::Bump` requests 16 — see `MmapAllocator`'s docs for the
/// links). The cache holds at most `SLOTS` chunks, so at most
/// `SLOTS * CHUNK_SIZE` bytes stay retained.
///
Expand All @@ -45,7 +45,7 @@ use super::mmap::MmapAllocator;
/// lists to leave half-linked, so a thread that disappears mid-operation
/// (`fork`, a signal) can strand at most the one chunk it was holding,
/// never the pool. (The underlying allocator must give the same guarantee;
/// [`MmapAllocator`] does.)
/// both platform page allocators do.)
///
/// The pool is `const`-constructible, so it can live in a `static` and
/// work before anything else has run.
Expand All @@ -60,11 +60,11 @@ pub struct ChunkPool<
}

impl<const CHUNK_SIZE: usize, const CHUNK_ALIGN: usize, const SLOTS: usize>
ChunkPool<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS>
ChunkPool<PageAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS>
{
#[must_use]
pub const fn new() -> Self {
Self::new_in(MmapAllocator)
Self::new_in(PageAllocator)
}
}

Expand Down Expand Up @@ -346,8 +346,8 @@ mod tests {
/// runs against `Global` so Miri can check it.
#[test]
#[cfg(not(miri))]
fn mmap_backed_pool_smoke() {
let pool = ChunkPool::<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, 64>::new();
fn kernel_backed_pool_smoke() {
let pool = ChunkPool::<PageAllocator, CHUNK_SIZE, CHUNK_ALIGN, 64>::new();
let block = pool.allocate(layout(100)).unwrap();
assert_eq!(block.len(), CHUNK_SIZE);
let addr = block.cast::<u8>().as_ptr().addr();
Expand Down
Loading
Loading