aligned_vmem/api/reserve_aligned_huge.rs
1use crate::error::VmemError;
2#[cfg(aligned_vmem_mock)]
3use crate::mock;
4use crate::os::reserve_aligned_huge_raw;
5use crate::Reservation;
6
7use super::internal::{finish_reservation_huge, validate_size_align};
8
9/// Reserve `size` bytes aligned to `align`, requesting OS **large / huge
10/// pages** (Linux/Android `MAP_HUGETLB`, Windows `MEM_LARGE_PAGES`).
11/// Currently a **no-op on macOS and other Unix that is neither Linux nor
12/// Android** — it falls back to
13/// an ordinary reservation, identical to [`reserve_aligned`](crate::api::reserve_aligned).
14///
15/// **Transparent-huge-page hinting (Linux/Android `MADV_HUGEPAGE`) is not used:** it
16/// cannot affect an already-explicitly-huge `MAP_HUGETLB` mapping (the pages
17/// are already huge), so issuing it would be a wasted syscall. This crate's
18/// strategy is the explicit hugetlbfs path only.
19///
20/// Large pages reduce TLB pressure for big allocator segments. The request is
21/// **best-effort**: if the OS refuses large pages (none configured, no
22/// privilege), the reservation transparently falls back to ordinary pages, so
23/// this never fails purely because huge pages are unavailable — it fails only
24/// on a genuine reservation error (OOM) or a contract violation.
25///
26/// To detect whether huge pages were actually granted (as opposed to having
27/// fallen back to ordinary pages), use the returned [`Reservation::is_huge`](crate::Reservation::is_huge)
28/// method.
29///
30/// Base/align/size contract is otherwise identical to [`reserve_aligned`](crate::api::reserve_aligned),
31/// **except on Linux AND Android with `huge-pages` enabled** (the check's
32/// cfg is `any(target_os = "linux", target_os = "android")` + `feature =
33/// "huge-pages"`): `size` and `align` must BOTH
34/// additionally be multiples of the huge-page size (2 MiB) — a request
35/// that only satisfies `reserve_aligned`'s own weaker `PAGE`-multiple contract
36/// is rejected with `VmemError::invalid_argument()` before any syscall runs,
37/// even though such a request could previously succeed there via the documented
38/// ordinary-page fallback. For the failure cause use
39/// [`try_reserve_aligned_huge`].
40///
41/// **Windows limitation:** on Windows, this function returns a reservation with
42/// [`Reservation::is_huge`](crate::Reservation::is_huge) == `true` only when ALL of the following hold:
43/// 1. The fast-path condition `align <= GetLargePageMinimum()` is satisfied
44/// (typically `align <= 2 MiB` on x86_64)
45/// 2. `size` is a multiple of the system's large-page minimum
46/// 3. The calling process has `SeLockMemoryPrivilege` granted AND has
47/// **enabled** it via `AdjustTokenPrivileges` (the crate does not do
48/// this for you — a process with the privilege granted but not enabled
49/// fails exactly like an unprivileged one and silently falls back to
50/// ordinary pages)
51///
52/// NOTE: The widened fast-path condition (II-3, 2026-08-16 audit finding) expanded
53/// the single-call ATTEMPT window from `align <= 64 KiB` to `align <= GetLargePageMinimum()`,
54/// but on an unprivileged host the actual paths that SUCCEED (pass the post-call alignment
55/// check) are typically still limited. When large pages are NOT granted (unprivileged),
56/// `VirtualAlloc`'s alignment guarantee is only 64 KiB; in practice it typically does NOT
57/// happen to land on the requested alignment, so the post-call check fails and the fast
58/// path falls through to the two-call path. Practically, this means `is_huge() == true` only
59/// for shapes where large pages are actually granted, which requires all three conditions
60/// above to hold.
61///
62/// **Extra-syscall cost on unprivileged hosts:** For the widened align range
63/// (`64 KiB < align <= GetLargePageMinimum()`), when large pages are requested but
64/// not granted (e.g., unprivileged process, or `SeLockMemoryPrivilege` not enabled),
65/// the code attempts `VirtualAlloc` with `MEM_LARGE_PAGES` (fails), retries without
66/// it (succeeds with ordinary pages), and if that retry's base doesn't happen to
67/// satisfy the requested alignment, the whole thing is released and falls through to
68/// the two-call path. This means an unprivileged reservation in this align range
69/// can cost up to 2 extra `VirtualAlloc` calls + 1 `VirtualFree` before reaching
70/// the two-call path, versus before the II-3 change (which would have gone straight
71/// to the two-call path for `align > 64 KiB`). This is a real, measurable behavior
72/// change, not a correctness bug — the widening genuinely expands the single-call
73/// attempt window, and unprivileged processes pay the extra-syscall cost for shapes
74/// that now attempt but fail the fast path.
75///
76/// If any of these conditions fail, the function falls back to ordinary
77/// pages and returns a reservation with [`Reservation::is_huge`](crate::Reservation::is_huge) == `false`.
78/// On Windows, large pages (`MEM_LARGE_PAGES`) are only ever requested and
79/// possibly granted via the single-call fast path; the two-call path never requests
80/// large pages, so the result never has
81/// [`Reservation::is_huge`](crate::Reservation::is_huge) == `true`.
82///
83/// **Decommit incompatibility (corrected task #1140):** on Windows,
84/// [`decommit`](crate::api::decommit)/[`decommit_lazy`](crate::api::decommit_lazy) **never work** on huge-page
85/// reservations — `VirtualFree` with `MEM_DECOMMIT` unconditionally fails on
86/// large-page regions. [`decommit_lazy`](crate::api::decommit_lazy) never works on a huge-page
87/// reservation on ANY platform — `MADV_FREE` (its Linux/Android backend) has
88/// no documented HugeTLB support, unlike `MADV_DONTNEED` below.
89///
90/// [`decommit`](crate::api::decommit) on Linux/Android is more nuanced: it depends on BOTH the
91/// requested range and the running kernel. `madvise(2)` documents that
92/// `MADV_DONTNEED` gained HugeTLB support in Linux 5.18, requiring
93/// `[base+start, base+end)` to be aligned to the mapping's huge page size (2
94/// MiB) at both endpoints — the same alignment this function already requires
95/// of `size`/`align` themselves on Linux/Android, so decommitting an entire
96/// huge reservation, or any 2-MiB-granular sub-range of it, is exactly such an
97/// eligible range on a >= 5.18 kernel. A `page_size()`-granular (e.g. 4 KiB)
98/// but not 2-MiB-granular offset still gets `EINVAL` and does nothing, as does
99/// EVERY range on a pre-5.18 kernel. [`Reservation::decommit`](crate::Reservation::decommit)/
100/// [`Reservation::try_decommit`](crate::Reservation::try_decommit) (the safe methods) consult both
101/// [`Reservation::is_huge`](crate::Reservation::is_huge) and the requested range to skip the ineligible
102/// case before issuing the syscall; the free [`decommit`](crate::api::decommit) function has no
103/// `is_huge()` to consult and issues the syscall unconditionally — see that
104/// function's own doc for the precise split. Either way — an ineligible
105/// range, or any range on a pre-5.18 kernel — the effect is indistinguishable
106/// from a silent no-op: the caller's RSS does not decrease, and subsequent
107/// reads return the old (stale) data rather than zeroed pages.
108///
109/// Documented per the `madvise(2)` man page, and — since task #1152 (F1) —
110/// empirically exercised under a real hugetlb pool by this crate's own CI:
111/// the `aligned-vmem-hugetlb-real` job (`.github/workflows/ci.yml`) hard-
112/// asserts (via a path-activation oracle) that this function actually
113/// received a `MAP_HUGETLB` grant, then drives a huge-page-eligible range
114/// through [`Reservation::decommit`](crate::Reservation::decommit)'s eligible-huge branch. **What that
115/// job proves, stated precisely (task #1160/F1 correction of an earlier
116/// overclaim; strengthened tasks #1164 and #1174):** the eligible-range/post-5.18-kernel
117/// case genuinely REACHES the real `madvise(2)`/`MADV_DONTNEED` backend call
118/// — AND, since task #1164's
119/// `ci_hugetlb_real_pool_kernel_actually_accepts_eligible_madvise`
120/// (`tests/decommit_capability.rs`), that the kernel itself returned `0`
121/// (accepted) for that call, not `-1` (rejected): under `bench-internals`,
122/// `libc_madvise` (`src/os/unix.rs`) records the syscall's own return value
123/// into a counter pair, and that job hard-asserts it increased for this
124/// eligible-range case — AND, since task #1174's
125/// `ci_hugetlb_real_pool_decommit_actually_zeroes_memory_on_reaccess`
126/// (`tests/decommit_capability.rs`), that the decommitted range reads back
127/// zero on re-access: that test writes a non-zero byte pattern across the
128/// whole eligible range, calls [`Reservation::decommit`](crate::Reservation::decommit),
129/// then reads every byte back and hard-asserts each one is zero —
130/// zero-fill-on-readback is proven for this eligible-range case on a Linux
131/// runner (the code path is gated Linux **and Android** as a pair; the
132/// Android half is inherited from that shared cfg, not separately executed
133/// by any CI job). What this still does NOT prove: that the kernel's
134/// acceptance actually corresponds to reclaiming the physical backing — the
135/// job logs `HugePages_Free` around that test as an observation only, never
136/// a pass/fail gate, because it is a kernel-global counter shared with the
137/// job's other huge-page reservations. On
138/// builds WITHOUT `bench-internals`, `libc_madvise` still discards the
139/// return value entirely (task #719) — the kernel-response proof above is
140/// scoped to the one CI job that enables the counters. Three things remain
141/// reasoned-from-spec rather than empirically verified, named explicitly:
142/// (1) the free [`decommit`](crate::api::decommit) entry point, reached only through the safe
143/// methods in CI, never called directly by a test; (2) the ineligible-range
144/// case (still a documented no-op) and every range on a pre-5.18 kernel —
145/// CI's runner image kernel version is not pinned by this crate; and (3)
146/// physical reclaim of the decommitted backing to the OS/hugetlb pool —
147/// the job's `HugePages_Free` log is an observation only, not a gate.
148/// Zero-fill on next access is no longer part of this list: task #1174's
149/// content test above reads every byte back and hard-asserts zero.
150///
151/// Use [`reserve_aligned`](crate::api::reserve_aligned) instead if you need decommit to work
152/// unconditionally, regardless of range shape, kernel version, or platform.
153///
154/// **Linux/Android hugetlb pool over-reserve (`align > 2 MiB`):** when huge pages
155/// are actually granted through the over-reserve path — which is every
156/// granted `align > 2 MiB` request (the exact-size fast path exists only
157/// for `align == LINUX_HUGE_PAGE_SIZE`, 2 MiB), and an `align == 2 MiB`
158/// request only when that fast path misses — the whole `size + align`-byte
159/// `MAP_HUGETLB` mapping is kept for the reservation's lifetime, and the
160/// Linux kernel reserves pool pages for a private hugetlb mapping's entire
161/// length at
162/// `mmap` time (no `MAP_NORESERVE` is passed). The exactly `align` bytes
163/// of never-touched slack are therefore charged against the bounded
164/// `nr_hugepages` pool until the reservation is released: a
165/// `size == align == 4 MiB` workload consumes 4 pool pages per segment
166/// for 2 needed (2×), reaching pool exhaustion — and the silent
167/// ordinary-page fallback — with half the segments an exact charge would
168/// allow. Workloads bounding the hugetlb pool should prefer
169/// `align == 2 MiB` shapes, which the exact-size fast path serves with
170/// zero over-reserve whenever it hits (and whose miss cost is at most
171/// `align == 2 MiB` of slack, not `align > 2 MiB`). This cost is
172/// REASONED-FROM-SPEC (documented kernel
173/// reservation semantics; updated task #1160/F4: a hugetlb-configured host
174/// now exists in this crate's CI (`aligned-vmem-hugetlb-real`,
175/// `.github/workflows/ci.yml`), but that job does not measure pool-page
176/// consumption before/after a reservation, so this specific over-reserve
177/// cost remains unmeasured on any host available to this project) and, when
178/// huge pages are granted via
179/// this over-reserve path, is deliberately not trimmed away: the
180/// over-reserved mapping is then kept whole as one soundness-driven
181/// unit (a single `munmap` at the mapping base), and the pool
182/// trade-off has no measurable host available.
183// Historical notes (task #776, #714, #848, #843):
184//
185// - task #776, F3: Linux huge-page request additionally requires both size
186// and align to be multiples of the huge-page size (2 MiB), rejecting
187// PAGE-multiple requests that `reserve_aligned` accepts. (Android joined
188// the same `any(target_os = "linux", target_os = "android")` cfg arm in
189// task #944/U-2, so this contract has been Linux/Android-common since
190// then.) This was added to
191// close a real `munmap` mapping leak (task #714); the trade-off is a
192// stricter contract in exchange for provable correctness.
193//
194// - task #848: Windows single-call fast path is the only
195// path that can grant large pages on Windows; the two-call path never
196// requests them. (For large-page requests, the fast-path condition is
197// `align <= GetLargePageMinimum()`, typically 2 MiB; for ordinary requests,
198// it is `align <= WIN_ALLOCATION_GRANULARITY`, 64 KiB.)
199//
200// - task #843, V4: decommit does not work on huge-page reservations on either
201// platform (Windows: VirtualFree fails; Linux/Android: MADV_DONTNEED/MADV_FREE
202// requires huge-page granularity).
203// SUPERSEDED by task #1140: this was true only for Windows and for
204// MADV_FREE (decommit_lazy) everywhere. On Linux/Android, MADV_DONTNEED
205// (eager decommit) now works for a 2-MiB-aligned range on kernel >= 5.18 —
206// see this function's own rustdoc above ("Decommit incompatibility
207// (corrected task #1140)") for the current, precise contract; this note
208// is kept only as a historical record of the pre-#1140 belief.
209#[must_use]
210#[cfg(feature = "huge-pages")]
211#[cfg_attr(docsrs, doc(cfg(feature = "huge-pages")))]
212pub fn reserve_aligned_huge(size: usize, align: usize) -> Option<Reservation> {
213 try_reserve_aligned_huge(size, align).ok()
214}
215
216/// Fallible [`reserve_aligned_huge`].
217#[cfg(feature = "huge-pages")]
218#[cfg_attr(docsrs, doc(cfg(feature = "huge-pages")))]
219pub fn try_reserve_aligned_huge(size: usize, align: usize) -> Result<Reservation, VmemError> {
220 validate_size_align(size, align)?;
221 #[cfg(aligned_vmem_mock)]
222 if let Some(e) = mock::take_reserve_fault() {
223 mock::record(mock::Call::ReserveHuge { size, align });
224 return Err(e);
225 }
226 #[cfg(aligned_vmem_mock)]
227 mock::record(mock::Call::ReserveHuge { size, align });
228
229 // task #713: `reserve_aligned_huge_raw` now captures its own `VmemError`
230 // immediately at the point of failure; this just propagates it.
231 finish_reservation_huge(size, align, reserve_aligned_huge_raw(size, align))
232}