aligned_vmem/page_size.rs
1use core::sync::atomic::{AtomicUsize, Ordering};
2
3use super::page::PAGE;
4#[cfg(all(unix, not(miri)))]
5use crate::os::{sysconf, _SC_PAGESIZE};
6#[cfg(all(windows, not(miri)))]
7use crate::os::{GetSystemInfo, SystemInfo, WIN_ALLOCATION_GRANULARITY};
8
9/// Cache for [`page_size`]. Three states:
10///
11/// - `0` — not yet queried: the next call queries the OS and stores one of the
12/// other two states.
13/// - [`PAGE_SIZE_QUERY_FAILED`] (`usize::MAX`) — queried, and the OS answer was
14/// unusable (an error return, zero, not a power of two, or below [`PAGE`]).
15/// Cached like a success so the degraded state is stable for the process
16/// lifetime and the hot path stays one relaxed load.
17/// - any other value — the validated OS page size (a power of two `>= PAGE`).
18///
19/// `pub(crate)` so the `page_size_override` test seam can store into it.
20pub(crate) static PAGE_SIZE_CACHE: AtomicUsize = AtomicUsize::new(0);
21
22/// The "queried and the answer was unusable" cache state (see
23/// [`PAGE_SIZE_CACHE`]). `usize::MAX` is not a power of two, so it can never
24/// collide with a real validated page size — and, deliberately, no real
25/// offset is a multiple of it except `0`, so even a page-multiple validator
26/// that reads it RAW (without an explicit poison check) rejects every
27/// non-empty range: the poison fails closed by arithmetic, not by policing.
28pub(crate) const PAGE_SIZE_QUERY_FAILED: usize = usize::MAX;
29
30/// Internal implementation of page size validation.
31#[inline]
32#[must_use]
33pub(crate) fn validate_page_size_impl(queried: usize) -> usize {
34 if queried >= PAGE && queried.is_power_of_two() {
35 queried
36 } else {
37 PAGE
38 }
39}
40
41/// Raw three-state page-size accessor: the validated OS page size, or
42/// [`PAGE_SIZE_QUERY_FAILED`] when the one-time OS query produced an unusable
43/// answer. Never returns `0` (the cold path resolves the not-yet-queried
44/// state before returning).
45///
46/// This is what every VALIDATOR in this crate consults — the poison must be
47/// visible to validation, which is exactly what the public [`page_size`]
48/// deliberately hides (it maps the poison to the conservative [`PAGE`] floor
49/// so its "power of two" contract holds). Hot path: one relaxed load and a
50/// zero test, identical to the pre-poison `page_size` body.
51#[inline]
52pub(crate) fn page_size_or_poison() -> usize {
53 let cached = PAGE_SIZE_CACHE.load(Ordering::Relaxed);
54 if cached != 0 {
55 return cached;
56 }
57 init_page_size_cache()
58}
59
60/// Cold path of [`page_size_or_poison`]: query the OS once and cache either
61/// the validated answer or the poison. Racing threads may both query; the
62/// query is idempotent and both store the same value.
63#[cold]
64#[inline(never)]
65fn init_page_size_cache() -> usize {
66 let queried = query_os_page_size();
67 // `validate_page_size_impl` returns its input exactly when the input is a
68 // valid page size (every invalid input maps to PAGE; the one overlap —
69 // the valid input PAGE mapping to itself — is benign), so
70 // "output == input" IS the validity test, shared verbatim with the
71 // `page_size_override` seam's acceptance rule.
72 let value = if validate_page_size_impl(queried) == queried {
73 queried
74 } else {
75 // Do NOT fold a failed query to PAGE: on a host whose real page is
76 // larger (16 KiB Apple Silicon, 64 KiB aarch64 Linux), a believed
77 // 4 KiB page would let decommit ranges pass validation that the OS
78 // then rounds UP to the real page — discarding live data outside the
79 // requested range. Poison instead: every page-granular state
80 // operation fails closed until the process exits (`try_*` forms
81 // report it; see `try_page_size`).
82 PAGE_SIZE_QUERY_FAILED
83 };
84 PAGE_SIZE_CACHE.store(value, Ordering::Relaxed);
85 value
86}
87
88/// Return the OS page size in bytes, querying the OS once and caching the
89/// result.
90///
91/// Uses `sysconf(_SC_PAGESIZE)` on Unix and `GetSystemInfo` on Windows; under
92/// miri it returns [`PAGE`] (4 KiB) by design (there is no real OS page). The
93/// value is cached in a process-wide atomic after the first call, so repeated
94/// calls are a single relaxed load. The returned value is always a power of
95/// two and at least [`PAGE`].
96///
97/// **Correctness:** on Apple Silicon macOS the page size is 16 KiB, and on
98/// some Linux configurations 64 KiB. Use this value (not [`PAGE`]) to round
99/// decommit offsets: `decommit`/`decommit_lazy` validate BOTH endpoints
100/// against `page_size()` before reaching the OS, and a misaligned endpoint is
101/// a crate-level fail-closed skip (the call returns without any effect).
102/// That crate-level validation is the load-bearing guard — do not rely on the
103/// OS to reject a misaligned range for you. The kernels' own behavior is
104/// asymmetric and platform-divergent: Linux `madvise(2)` rejects the call
105/// only when the ADDRESS is misaligned, and rounds a misaligned LENGTH **up**
106/// to the real page (touching memory past the requested range); Windows
107/// `VirtualFree(MEM_DECOMMIT)` rejects nothing and widens the range in BOTH
108/// directions (it decommits every page containing any byte of the range).
109///
110/// **If the one-time OS query fails** (`sysconf` returning an error, or a
111/// nonsensical answer — not observed on any supported platform: on
112/// Linux/macOS/Windows the page size comes from process-startup data that
113/// cannot fail to exist), this function still returns [`MIN_PAGE`](crate::MIN_PAGE)
114/// (= [`PAGE`], 4 KiB) so it stays infallible — but the crate records the
115/// failure and every page-granular STATE operation fails closed for the
116/// process lifetime: `decommit`/`decommit_lazy` become no-ops,
117/// `recommit`/`commit_range` return `false`, the `try_*` forms and the lazy
118/// reservation constructor report an OS-side no-code error (see
119/// [`VmemError::os_refusal_unknown_code`](crate::VmemError::os_refusal_unknown_code)),
120/// and [`try_page_size`](crate::try_page_size) returns `Err`. Reserving,
121/// using, and releasing memory are unaffected — they never depend on the
122/// runtime page size. Rationale: with the real page size unknown, a
123/// decommit granularity guess could make the OS round a length up across
124/// live data; refusing to decommit loses nothing but an optimization,
125/// while guessing risks silent data loss.
126///
127/// **One bookkeeping value keeps moving even in the degraded state (task
128/// #1156, finding F15):**
129/// `LazyReservation::shrink_committed` (feature `lazy-commit`; not an
130/// intra-doc link here — `page_size` is compiled unconditionally and
131/// `LazyReservation` is not, so a real link would break whenever
132/// `lazy-commit` is off, e.g. under plain `--no-default-features`) lowers
133/// its own watermark unconditionally, even though the `decommit` call it
134/// issues underneath is the no-op described
135/// above. The watermark is rounded UP using the conservative [`PAGE`] value
136/// this degraded state substitutes for the unknown real page size, so the
137/// dropped range can be smaller than a true page on a host with a larger
138/// real page — but `shrink_committed` documents the watermark as this
139/// crate's own bookkeeping guarantee, "not a claim about residency", so a
140/// lowered watermark under poison is consistent with that contract, not a
141/// violation of it: no committed byte the caller is entitled to is released
142/// (the underlying `decommit` did nothing), only the handle's own record of
143/// what it considers committed moves. No other primitive's return value
144/// (`bool`, `Result`) is affected by this — `shrink_committed` is the one
145/// entry point in this enumeration with no fallible/boolean signature to
146/// report the degraded state through, because it never had one even outside
147/// the degraded state.
148#[must_use]
149#[inline]
150pub fn page_size() -> usize {
151 let v = page_size_or_poison();
152 if v == PAGE_SIZE_QUERY_FAILED {
153 // Conservative display value: the documented "power of two >= PAGE"
154 // property must hold even in the degraded state, and PAGE is the
155 // crate-wide validation floor. The poison itself never escapes the
156 // crate through this function.
157 PAGE
158 } else {
159 v
160 }
161}
162
163/// One-time raw OS page-size query, before validation. Routed through the
164/// `aligned_vmem_page_size_override` test seam when that cfg is on, so a test
165/// can simulate a failed query (or a larger-page host) on any hardware; the
166/// production build compiles the seam out entirely.
167pub(crate) fn query_os_page_size() -> usize {
168 #[cfg(aligned_vmem_page_size_override)]
169 if let Some(simulated) = crate::page_size_query_override::armed_query_result() {
170 return simulated;
171 }
172 query_os_page_size_real()
173}
174
175#[cfg(all(unix, not(miri)))]
176pub(crate) fn query_os_page_size_real() -> usize {
177 // SAFETY: `sysconf(_SC_PAGESIZE)` takes an integer name and returns a
178 // `c_long` (the page size, or -1 on error). No pointers involved.
179 let v = unsafe { sysconf(_SC_PAGESIZE) };
180 // An error (or nonsense) return maps to 0, which `init_page_size_cache`
181 // classifies as a FAILED query (poison), never as a 4 KiB answer.
182 if v <= 0 {
183 0
184 } else {
185 v as usize
186 }
187}
188
189#[cfg(all(windows, not(miri)))]
190pub(crate) fn query_os_page_size_real() -> usize {
191 // SAFETY: `GetSystemInfo` fills the caller-provided `SYSTEM_INFO`; the
192 // struct is stack-allocated and fully written by the call.
193 let mut info = SystemInfo::default();
194 unsafe { GetSystemInfo(&mut info) };
195 debug_assert!(
196 info.dw_allocation_granularity as usize >= WIN_ALLOCATION_GRANULARITY,
197 "OS-reported allocation granularity ({}) is smaller than the hardcoded constant ({}); \
198 this would break the single-call fast path's alignment guarantee",
199 info.dw_allocation_granularity,
200 WIN_ALLOCATION_GRANULARITY
201 );
202 // NOTE: This debug_assert fires only when `query_os_page_size()` is called,
203 // which happens on the cold path (decommit/decommit_lazy) — since task #897
204 // removed the `align > page_size() &&` conjunct, the reserve fast path no
205 // longer consults `page_size()` at all. It does NOT fire on the Windows
206 // single-call reservation fast path, which uses `WIN_ALLOCATION_GRANULARITY`
207 // directly.
208 //
209 // A zero/garbage `dw_page_size` needs no explicit check here:
210 // `init_page_size_cache` validates the raw answer and classifies anything
211 // unusable as a failed query (poison).
212 info.dw_page_size as usize
213}
214
215#[cfg(miri)]
216pub(crate) fn query_os_page_size_real() -> usize {
217 // Miri has no real OS page; use the crate's constant granularity.
218 PAGE
219}