Skip to main content

mnemosyne_arena/segment/alloc/
allocate.rs

1//! Handing a segment out: from a pool if one is retained, from the OS
2//! otherwise, then initialized and guarded.
3//!
4//! Returning one is the other direction and lives in [`super::release`],
5//! which reaches `try_return_to_pool` here through the parent.
6
7use super::super::alignment::checked_align_up;
8use super::super::pool::HasSegmentPool;
9#[cfg(feature = "segment-header-guards")]
10use super::SEGMENT_HEADER_GUARD_SIZE;
11use super::SEGMENT_MAPPING_SIZE;
12#[cfg(feature = "segment-tail-guards")]
13use super::SEGMENT_TAIL_GUARD_SIZE;
14use crate::numa::current_numa_node;
15use mnemosyne_core::constants::{SEGMENT_ALIGN, SEGMENT_SIZE};
16use mnemosyne_core::types::Segment;
17
18/// Pops a retained free segment from the global segment pool and
19/// re-initializes it, monomorphized per backend `B` (`#[inline(never)]` keeps
20/// this cold pool path out of the hot caller).
21///
22/// Only the free pool is consulted: a retained segment holds no live
23/// allocation, so re-initializing it erases nothing a caller still reads.
24///
25/// # Safety
26///
27/// The caller must ensure that the global segment pool contains valid,
28/// initialized `Segment` structures. The returned segment (if any) is owned by
29/// the caller.
30#[inline(never)]
31unsafe fn pop_free_segment<B: HasSegmentPool>() -> Option<*mut Segment> {
32    let segment = B::global_segment_pool().pop()?;
33    // SAFETY: segment points to a valid allocated Segment. We re-initialize
34    // the segment to erase stale epoch metadata and reset it for new allocations.
35    unsafe {
36        let raw_ptr = (*segment).raw_alloc_ptr;
37        let node = (*segment).numa_node;
38        Segment::initialize(segment, raw_ptr, node);
39    }
40    Some(segment)
41}
42
43/// Helper to return a segment to the global segment pool, monomorphized per
44/// backend `B`.
45///
46/// # Safety
47///
48/// The `segment` pointer must point to a valid, initialized `Segment` exclusively owned
49/// by the caller.
50#[inline(always)]
51pub(super) unsafe fn try_return_to_pool<B: HasSegmentPool>(segment: *mut Segment) -> bool {
52    debug_assert!(
53        !segment.is_null(),
54        "try_return_to_pool received null segment"
55    );
56    // SAFETY: by this function's contract `segment` is a valid, initialized,
57    // exclusively-owned `Segment`, satisfying `try_push_retained`'s own contract.
58    unsafe { B::global_segment_pool().try_push_retained(segment) }
59}
60
61/// Non-generic helper to initialize an allocated segment header and establish alignment bounds.
62///
63/// # Safety
64///
65/// The caller must guarantee:
66/// - `raw_ptr` must point to a valid, exclusive, and page-aligned allocation of size `SEGMENT_MAPPING_SIZE`.
67/// - The memory range must be writable to initialize the `Segment` structure.
68#[inline(never)]
69unsafe fn initialize_allocated_segment(
70    raw_ptr: *mut u8,
71    numa_node: u32,
72) -> Option<(*mut Segment, usize, usize, usize)> {
73    let aligned_addr = checked_align_up(raw_ptr as usize, SEGMENT_ALIGN)?;
74    let aligned_ptr = raw_ptr.map_addr(|_| aligned_addr).cast::<Segment>();
75
76    // SAFETY: aligned_ptr is within the allocated region.
77    unsafe {
78        Segment::initialize(aligned_ptr, raw_ptr, numa_node);
79    }
80
81    let mapping_end = raw_ptr.addr().checked_add(SEGMENT_MAPPING_SIZE)?;
82    let tail_slack_start = aligned_addr.checked_add(SEGMENT_SIZE)?;
83    if tail_slack_start > mapping_end {
84        return None;
85    }
86    #[cfg(feature = "segment-tail-guards")]
87    let tail_slack_start = super::guard::native_guard_size(SEGMENT_TAIL_GUARD_SIZE)
88        .and_then(|size| tail_slack_start.checked_add(size))
89        .filter(|end| *end <= mapping_end)
90        .unwrap_or(tail_slack_start);
91
92    Some((aligned_ptr, aligned_addr, tail_slack_start, mapping_end))
93}
94
95/// Returns the head (`[raw_ptr, aligned_addr)`) and tail
96/// (`[tail_slack_start, mapping_end)`) alignment-slack subranges of a segment
97/// mapping to the OS via `B::decommit`.
98///
99/// Both slack regions exist only to satisfy `SEGMENT_ALIGN` rounding and never
100/// hold allocator or user data. On Windows `VirtualAlloc` eagerly commits the
101/// whole mapping, so decommitting drops the slack's commit charge (up to
102/// ~`SEGMENT_ALIGN` ≈ 2 MiB of head slack per segment) for the mapping's
103/// lifetime; on Unix the slack is lazily backed, so this is typically a no-op.
104/// Best-effort: a backend without decommit support (`SUPPORTS_DECOMMIT ==
105/// false`) skips entirely, and both subranges stay inside the reservation,
106/// which the base `B::deallocate(raw_ptr, ..)` releases in full.
107///
108/// # Safety
109///
110/// `raw_ptr` must name the base of the live backend mapping that contains both
111/// subranges, with `raw_ptr as usize <= aligned_addr` and
112/// `tail_slack_start <= mapping_end`. Neither subrange may hold allocator or
113/// user data, and the head bounds are page-aligned (`raw_ptr` comes from the
114/// backend allocator and `aligned_addr` is a `SEGMENT_ALIGN` multiple), as
115/// `decommit` requires.
116#[inline]
117pub(crate) unsafe fn decommit_mapping_slack<B: mnemosyne_core::MemoryBackend>(
118    raw_ptr: *mut u8,
119    aligned_addr: usize,
120    tail_slack_start: usize,
121    mapping_end: usize,
122) {
123    if !B::SUPPORTS_DECOMMIT {
124        return;
125    }
126    let head_slack = aligned_addr - raw_ptr as usize;
127    if head_slack > 0 {
128        // SAFETY: per this function's contract, `[raw_ptr, aligned_addr)` is a
129        // page-aligned, data-free subrange of the live mapping.
130        let _ = unsafe { B::decommit(raw_ptr, head_slack) };
131    }
132    if tail_slack_start < mapping_end {
133        // SAFETY: per this function's contract, `[tail_slack_start,
134        // mapping_end)` is a data-free subrange of the live mapping. Deriving
135        // the subrange pointer from `raw_ptr` retains the mapping provenance.
136        let tail_slack = raw_ptr.map_addr(|_| tail_slack_start);
137        let _ = unsafe { B::decommit(tail_slack, mapping_end - tail_slack_start) };
138    }
139}
140
141/// A segment handed out by [`acquire_segment`], tagged with whether it may
142/// still hold live allocations.
143///
144/// The distinction decides what the new owner may do with the segment: a
145/// [`Free`](Self::Free) segment holds nothing, so it may be carved up or handed
146/// back through [`deallocate_segment`](super::deallocate_segment); an
147/// [`Orphan`](Self::Orphan) holds blocks a dead thread cache allocated and
148/// other threads may still read, write, or free, so it may be adopted but
149/// never returned to the free pool or the OS until every one of its pages is
150/// empty.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum AcquiredSegment {
153    /// A freshly mapped or pool-reinitialized segment with no live allocation.
154    Free(*mut Segment),
155    /// A segment from the orphan pool, handed out as is with its live
156    /// allocations, per-page free lists, and pending cross-thread frees.
157    Orphan(*mut Segment),
158}
159
160/// Allocates an empty, aligned segment, either from the free pool or from the
161/// OS.
162///
163/// Never returns an orphan: an orphaned segment still holds live allocations,
164/// and a caller of this function may hand the segment straight back through
165/// [`deallocate_segment`](super::deallocate_segment), which would put those
166/// allocations in the free pool for reuse or for a purge to unmap. Thread
167/// caches that can adopt an orphan use [`acquire_segment`].
168///
169/// # Monomorphization and ZST Static Routing
170///
171/// The backend parameter `B` acts as a Zero-Sized Type (ZST) policy marker. Calls
172/// to this function are fully monomorphized by the compiler into direct machine-code
173/// calls for the target backend, preserving the zero-cost abstraction invariant.
174///
175/// # Safety
176///
177/// This function is unsafe because it allocates virtual memory from the OS/backend,
178/// aligns and initializes a raw `Segment` pointer. The caller must guarantee:
179/// - The backend `B` must be a valid implementor of `HasSegmentPool`.
180/// - The returned pointer must eventually be returned to the pool via
181///   `deallocate_segment` or released to the OS via `release_segment_mapping`.
182#[inline]
183pub unsafe fn allocate_segment<B: HasSegmentPool>() -> Option<*mut Segment> {
184    // SAFETY: forwarded to the caller — the free pool holds valid segments.
185    if let Some(segment) = unsafe { pop_free_segment::<B>() } {
186        return Some(segment);
187    }
188    // SAFETY: forwarded to the caller.
189    unsafe { map_fresh_segment::<B>() }
190}
191
192/// Acquires a segment for a thread cache: a retained free segment first, then
193/// an orphan to adopt, then a fresh OS mapping.
194///
195/// # Safety
196///
197/// As [`allocate_segment`]. Additionally, an [`AcquiredSegment::Orphan`] must
198/// be adopted — its live pages kept intact — or pushed back to the orphan
199/// pool; it must not reach [`deallocate_segment`](super::deallocate_segment)
200/// or [`release_segment_mapping`](super::release_segment_mapping) while any of
201/// its pages holds a live allocation.
202#[inline]
203pub unsafe fn acquire_segment<B: HasSegmentPool>() -> Option<AcquiredSegment> {
204    // SAFETY: forwarded to the caller — the free pool holds valid segments.
205    if let Some(segment) = unsafe { pop_free_segment::<B>() } {
206        return Some(AcquiredSegment::Free(segment));
207    }
208    if let Some(segment) = B::global_orphan_pool().pop() {
209        return Some(AcquiredSegment::Orphan(segment));
210    }
211    // SAFETY: forwarded to the caller.
212    unsafe { map_fresh_segment::<B>() }.map(AcquiredSegment::Free)
213}
214
215/// Maps, aligns, and initializes a fresh segment from the OS, purging the free
216/// pool and retrying once when the first mapping request fails.
217///
218/// # Safety
219///
220/// As [`allocate_segment`].
221#[inline(never)]
222unsafe fn map_fresh_segment<B: HasSegmentPool>() -> Option<*mut Segment> {
223    // We allocate twice the segment size to ensure we can find an aligned boundary.
224    // SAFETY: SEGMENT_MAPPING_SIZE is non-zero and aligned. We call B::allocate.
225    let mut raw_ptr = unsafe { B::allocate(SEGMENT_MAPPING_SIZE) };
226    if raw_ptr.is_null() {
227        // First OS allocation failed: release every retained free segment
228        // back to the OS to reclaim the address space / commit charge a
229        // transient working-set spike may be holding, then retry exactly
230        // once. Bounded to one purge and one retry: a still-exhausted OS
231        // fails fast on the second call rather than looping.
232        //
233        // SAFETY: segments retained in the pool at this point are
234        // exclusively pool-owned — a segment a thread-local allocator still
235        // references is "active", not "retained" — so releasing them
236        // invalidates no live pointer, and the retried `B::allocate` call
237        // follows the same contract as the first.
238        unsafe { super::release::purge_segment_pool::<B>() };
239        raw_ptr = unsafe { B::allocate(SEGMENT_MAPPING_SIZE) };
240        // Surfaces the recovery attempt and its outcome as a stats counter
241        // (`ArenaMemoryStats::oom_retries` / `oom_retry_successes`) — the
242        // crate has no tracing dependency, so telemetry follows the
243        // existing `purge_calls`/`reset_calls` counter convention.
244        B::global_segment_pool().record_oom_retry(!raw_ptr.is_null());
245        if raw_ptr.is_null() {
246            return None;
247        }
248    }
249
250    let numa_node = current_numa_node();
251    // SAFETY: `raw_ptr` is the non-null `SEGMENT_MAPPING_SIZE` mapping just
252    // returned by `B::allocate`, which is exclusively owned and writable —
253    // exactly `initialize_allocated_segment`'s contract.
254    let (aligned_ptr, aligned_addr, tail_slack_start, mapping_end) =
255        match unsafe { initialize_allocated_segment(raw_ptr, numa_node) } {
256            Some(val) => val,
257            None => {
258                // SAFETY: Releasing raw memory back to the backend because alignment check overflowed.
259                let _released = unsafe { B::deallocate(raw_ptr, SEGMENT_MAPPING_SIZE) };
260                return None;
261            }
262        };
263
264    // Bind the aligned segment mapping to the allocating thread's NUMA node.
265    // `mbind(MPOL_BIND)` enforces first-touch locality so pages are allocated
266    // from the correct socket rather than relying on the OS default policy.
267    // This call goes to the kernel only once per fresh segment (cold path);
268    // segments recycled from the pool retain their prior NUMA binding and never
269    // reach this function (they return early via `pop_free_segment`).
270    //
271    // SAFETY: `aligned_ptr` points to the segment-aligned base of the
272    // `SEGMENT_MAPPING_SIZE` mapping that is exclusively owned at this point
273    // (not yet published to any pool or thread), `SEGMENT_SIZE` is the
274    // usable extent of the aligned portion, and `numa_node` was read from
275    // Themis's thread-local cache immediately before this call.
276    unsafe {
277        crate::numa::bind_segment_to_numa_node(
278            raw_ptr.map_addr(|_| aligned_ptr as usize),
279            SEGMENT_SIZE,
280            numa_node,
281        )
282    };
283
284    #[cfg(feature = "segment-header-guards")]
285    {
286        if B::SUPPORTS_MAKE_GUARD
287            && let Some(size) = super::guard::native_guard_size(SEGMENT_HEADER_GUARD_SIZE)
288            && let Some(offset) = super::guard::header_guard_offset(size)
289        {
290            // Install a header guard at the end of Page 0.
291            // Underflows (backward OOB writes) from Page 1 land in this guard region
292            // instead of overwriting the segment metadata at the start of Page 0.
293            //
294            // SAFETY: size is a whole system page and offset is aligned to it.
295            // The range ends at Page 0's boundary and follows all live metadata.
296            let header_guard_addr = aligned_addr + offset;
297            let header_guard = raw_ptr.map_addr(|_| header_guard_addr);
298            let _guarded = unsafe { B::make_guard(header_guard, size) };
299        }
300    }
301
302    #[cfg(feature = "segment-tail-guards")]
303    {
304        let tail_guard_size = tail_slack_start - (aligned_addr + SEGMENT_SIZE);
305        if B::SUPPORTS_MAKE_GUARD && tail_guard_size != 0 {
306            // Install a tail guard immediately after the segment's user-page
307            // region. Forward OOB writes that walk past Page 31 land in this
308            // guard region instead of an unrelated mapping. The address lives
309            // inside the `SEGMENT_MAPPING_SIZE - SEGMENT_SIZE` slack the arena
310            // reserves to satisfy `SEGMENT_ALIGN` rounding, so it is always
311            // part of the same backend allocation and is released together
312            // with the segment by `B::deallocate(raw_ptr, SEGMENT_MAPPING_SIZE)`.
313            // The install is best-effort: a backend without a `make_guard`
314            // implementation (default `false`) or a kernel that declines the
315            // request
316            // silently skips, leaving the slack accessible. Backend telemetry
317            // (`guard_install_calls`) surfaces the actual install count.
318            //
319            // SAFETY: the start and size are system-page aligned; initialization
320            // checked the reserved guard end against the complete raw mapping.
321            // `make_guard` never invalidates the reservation.
322            let tail_guard_addr = aligned_addr + SEGMENT_SIZE;
323            let tail_guard = raw_ptr.map_addr(|_| tail_guard_addr);
324            let _guarded = unsafe { B::make_guard(tail_guard, tail_guard_size) };
325        }
326    }
327
328    // The mapping over-reserves `SEGMENT_MAPPING_SIZE = 2 * SEGMENT_SIZE` so a
329    // `SEGMENT_ALIGN`-aligned base can always be found; return the resulting
330    // head and tail slack to the OS (guard pages installed above lie outside
331    // both slack subranges, so ordering relative to the guards is immaterial).
332    //
333    // SAFETY: `[raw_ptr, aligned_addr)` precedes the header and
334    // `[tail_slack_start, mapping_end)` succeeds the segment pages (and tail
335    // guard, when enabled), so neither holds allocator data; both stay inside
336    // the live reservation covered by the base release, and the head bounds
337    // are page-aligned (`raw_ptr` from `allocate`, `aligned_addr` a
338    // `SEGMENT_ALIGN` multiple).
339    unsafe { decommit_mapping_slack::<B>(raw_ptr, aligned_addr, tail_slack_start, mapping_end) };
340
341    Some(aligned_ptr)
342}