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}