Skip to main content

aligned_vmem/api/
decommit.rs

1use crate::decommit_outcome::DecommitOutcome;
2use crate::error::VmemError;
3#[cfg(all(feature = "fault-injection", not(aligned_vmem_mock)))]
4use crate::fault_injection;
5#[cfg(aligned_vmem_mock)]
6use crate::mock;
7#[cfg(not(aligned_vmem_mock))]
8use crate::os::{decommit_pages_impl, DecommitKind};
9use crate::page_size::{page_size_or_poison, PAGE_SIZE_QUERY_FAILED};
10
11/// Decommit pages `[base + start, base + end)`: hint the OS to return
12/// their physical backing while keeping the address-space reservation alive.
13///
14/// **Programmatically check platform guarantees:** use
15/// [`Reservation::decommit_reclaims_and_zeroes`](crate::Reservation::decommit_reclaims_and_zeroes) to query whether the current
16/// platform guarantees reclaim+zero-fill semantics. Returns `true` on Linux/Windows,
17/// `false` on Darwin/BSD where decommit is advisory-only.
18///
19/// **Platform behavior:**
20/// - On Linux and Windows this is guaranteed to return physical backing and
21///   zero-fill on next access (Linux `MADV_DONTNEED`, Windows `MEM_DECOMMIT`).
22/// - On the Darwin family (macOS/iOS/tvOS/watchOS) and the four BSDs
23///   (FreeBSD/DragonFly/NetBSD/OpenBSD), this is a best-effort hint with no
24///   zero-fill or reclaim guarantee — the physical pages may remain resident and
25///   old data may be observed after a decommit+recommit roundtrip.
26///   See [`Reservation::decommit_reclaims_and_zeroes`](crate::Reservation::decommit_reclaims_and_zeroes).
27///
28/// `start` and `end` must be multiples of [`page_size()`](crate::page_size::page_size) and within the span.
29/// A no-op if the range is empty AND page-aligned — and a VIOLATED range
30/// (`start > end`, or an endpoint not a multiple of [`page_size()`](crate::page_size::page_size) — which
31/// includes an empty MISALIGNED range such as `decommit(ptr, 1, 1)`) is a
32/// silent no-op in a release build; see "Contract violations, by build
33/// profile" below for the debug-build tripwire and the fallible
34/// [`try_decommit`] form.
35///
36/// # Safety
37///
38/// - `base` must be the [`as_ptr`](crate::Reservation::as_ptr) of a live
39///   reservation the caller owns.
40/// - **`end <= reservation.len()`** (the reservation's usable span, in
41///   bytes) — this is a MANDATORY precondition of the pointer arithmetic
42///   the backends perform (`base.add(start)` in BOTH real backends'
43///   `decommit_pages_impl` — Windows (`src/os/windows.rs`) before its
44///   `VirtualFree(MEM_DECOMMIT)` call, Unix (`src/os/unix.rs`) before its
45///   `madvise` call; the miri backend is a no-op that ignores `base`, and
46///   under `aligned_vmem_mock` no backend call happens at all, but the
47///   contract is stated platform-independently), not merely a
48///   functional/behavioral preference. Task #1235 correction: since task
49///   #1213/L2 (`1522d25`) this bullet enumerated the arithmetic as
50///   "`base.add(start)` / `base.add(end)`" — the second half never
51///   existed. No backend or FFI wrapper forms a pointer from `end` (both
52///   `decommit_pages_impl` bodies and the `winapi_virtual_decommit` /
53///   `libc_madvise` wrappers they call were read in full, task #1235):
54///   `end`'s only arithmetic role is the subtraction `end - start` —
55///   which cannot wrap on this function's paths, since this function
56///   returns on `start >= end` before the backend is reached — whose
57///   result is handed to the OS as a byte LENGTH. With `start <= end`,
58///   this single bound is what keeps the one offset that IS computed,
59///   `base.add(start)`, inside the allocation, and what keeps the OS
60///   call's span `[base+start, base+end)` inside the reservation. This
61///   requirement is stated here explicitly (task #1213/L2) rather than
62///   left to the summary line above ("within the span") — for an `unsafe
63///   fn`, a bounds requirement that determines whether pointer arithmetic
64///   is even defined belongs inside `# Safety` itself, restated in full,
65///   not referenced from an adjacent paragraph a caller auditing only
66///   this section could miss. Passing `end > reservation.len()` is
67///   undefined behavior, distinct from — and a strictly worse violation
68///   than — the `page_size()`-multiple contract below, which is merely a
69///   silent no-op on violation, never UB.
70/// - `[base+start, base+end)` must contain no data the caller still needs —
71///   its contents are discarded.
72///
73/// **Contract violations, by build profile (task #1051):** this entry point
74/// is intentionally infallible — the `()` return carries no write-permitting
75/// sentinel to misuse — so a violated range (`start > end`, or an endpoint
76/// not a multiple of [`page_size()`](crate::page_size::page_size)) is a silent no-op in a RELEASE build:
77/// no OS call is made and nothing is recorded. In a DEBUG build the same
78/// violation trips the `debug_assert!` below before anything happens, so a
79/// consumer's own test fails at the mistake instead of quietly decommitting
80/// nothing and leaving the memory resident; zero cost in release.
81/// [`try_decommit`] is the fallible form for callers who need the violation
82/// reported: it returns `Err` on every profile and never trips the tripwire.
83///
84/// **A poisoned page-size query is a DIFFERENT case and never panics, on
85/// any profile (task #1145/#1139, sharpened task #1173/L1):** if the
86/// one-time OS page-size query itself failed (see
87/// [`page_size()`](crate::page_size::page_size)'s "If the one-time OS query
88/// fails"), this function fails closed silently — no `debug_assert!`, no
89/// tripwire — because the caller's arguments are not at fault and the
90/// crate-wide poison contract promises an unconditional no-op here, matching
91/// [`decommit_lazy`](crate::api::decommit_lazy)'s no-tripwire design and the
92/// README's "never panics" list. This is distinct from the range-contract
93/// tripwire immediately above, which fires only in debug builds and only for
94/// a violated range under a HEALTHY page-size query.
95///
96/// **Platform divergence, not just a data-loss concern:** on Windows,
97/// `MEM_DECOMMIT` genuinely unmaps the pages, so a **write to `[base+start,
98/// base+end)` before [`recommit`](crate::api::recommit) is a hard `STATUS_ACCESS_VIOLATION`
99/// crash**, not a soft re-fault. On Linux, `MADV_DONTNEED` keeps the mapping
100/// resident and transparently re-faults a fresh zero page on next write, so
101/// the same code that is safe on Linux can crash on Windows. This exact
102/// divergence already crashed an in-repo consumer that assumed the Linux
103/// semantics — see
104/// <https://github.com/PHPCraftdream/sefer-alloc/blob/main/docs/CORRECTNESS_OPEN_ITEMS.md>
105/// item 6 (filed 2026-07-30) for the incident record and status.
106///
107/// **Huge-page granularity (task #843 V4/finding R4-4, corrected task #1140):**
108/// on huge-page reservations (those returned by
109/// [`reserve_aligned_huge`](crate::api::reserve_aligned_huge) with [`Reservation::is_huge`](crate::Reservation::is_huge) == `true`),
110/// **on Windows, decommit does not work at all**: `VirtualFree` with
111/// `MEM_DECOMMIT` unconditionally fails on large-page regions.
112///
113/// **On Linux/Android, whether decommit works depends on the requested range and
114/// the running kernel**, not on whether the mapping is huge — `madvise(2)`
115/// documents that `MADV_DONTNEED` gained HugeTLB support in Linux 5.18, with
116/// the same requirement it already has for ordinary mappings: `[base+start,
117/// base+end)` must be aligned to the mapping's huge page size (2 MiB on this
118/// crate's supported targets) at BOTH endpoints. This crate's own Linux/Android
119/// `huge-pages` contract already requires `reserve_aligned_huge`'s `size`/`align`
120/// to be multiples of that same 2 MiB, so a huge-aligned `[start, end)` is not a
121/// hypothetical — decommitting an entire huge reservation, or any 2-MiB-granular
122/// sub-range of it, is exactly such a range. A `page_size()`-granular (e.g. 4
123/// KiB) but NOT 2-MiB-granular offset still gets `EINVAL` from the kernel and
124/// does nothing — **this free function issues the syscall regardless of
125/// eligibility** (unlike [`Reservation::decommit`](crate::Reservation::decommit), which can consult
126/// [`Reservation::is_huge`](crate::Reservation::is_huge) and the requested range to skip the
127/// ineligible case before the syscall — see that method's doc for the exact
128/// split), so an ineligible range here is a wasted syscall that the kernel
129/// itself turns into a no-op, not a Rust-level skip. On a pre-5.18 kernel,
130/// EVERY range is ineligible regardless of alignment (the capability did not
131/// exist yet), so decommit is unconditionally a no-op there, matching the
132/// prior (task #843) documented behavior exactly. Either way — ineligible
133/// range, or eligible range on a pre-5.18 kernel — the effect is
134/// indistinguishable from a silent no-op: the caller's RSS does not decrease,
135/// and subsequent reads return the old (stale) data rather than zeroed pages.
136///
137/// Documented per the `madvise(2)` man page cited above, and — since task
138/// #1152 (F1) — empirically exercised by this crate's own CI: the
139/// `aligned-vmem-hugetlb-real` job (`.github/workflows/ci.yml`) configures a
140/// real `nr_hugepages` pool and hard-asserts (via a dedicated
141/// path-activation oracle) that `reserve_aligned_huge` actually received a
142/// `MAP_HUGETLB` grant rather than silently falling back to ordinary pages.
143/// Under that real grant, the job runs
144/// `huge_aligned_range_takes_the_real_backend_path_not_the_skip_path` and
145/// `huge_decommit_attempts_increments_on_huge_reservation`
146/// (`tests/decommit_capability.rs`), which drive a huge-page-eligible
147/// `[start, end)` through [`Reservation::decommit`](crate::Reservation::decommit)'s eligible-huge
148/// branch — the same `decommit_pages_impl`/`MADV_DONTNEED` backend call this
149/// free function itself makes. **What that job proves, stated precisely
150/// (task #1160/F1 correction of an earlier overclaim; strengthened tasks
151/// #1164 and #1174):** the eligible-range case genuinely REACHES the real
152/// `madvise(2)`/`MADV_DONTNEED` backend call under a real `MAP_HUGETLB`
153/// grant, rather than silently taking the Rust-level skip path — AND, since
154/// task #1164's `ci_hugetlb_real_pool_kernel_actually_accepts_eligible_madvise`
155/// (`tests/decommit_capability.rs`), the kernel's own syscall-level response
156/// is also asserted: under `bench-internals`, `libc_madvise`
157/// (`src/os/unix.rs`) records whether the syscall returned `0` or `-1`, and
158/// that job hard-asserts it returned `0` for this eligible-range call — AND,
159/// since task #1174's
160/// `ci_hugetlb_real_pool_decommit_actually_zeroes_memory_on_reaccess`
161/// (`tests/decommit_capability.rs`), the zero-fill half of the *effect* (as
162/// opposed to the *return code*) is no longer reasoned from the man page
163/// either: that test writes a non-zero byte pattern across the whole
164/// eligible range, calls [`Reservation::decommit`](crate::Reservation::decommit),
165/// then reads every byte back and hard-asserts each one is zero —
166/// zero-fill-on-readback is proven for this eligible-range/post-5.18 case
167/// on a Linux runner (the code path is gated Linux **and Android** as a
168/// pair; the Android half is inherited from that shared cfg, not separately
169/// executed by any CI job). What still remains NOT proven, deliberately
170/// kept separate from that zero-fill result: that the kernel actually
171/// returned the physical backing to the OS/hugetlb pool — the job logs
172/// `HugePages_Free` around that test as an observation only, never a
173/// pass/fail gate, because it is a kernel-global counter shared with the
174/// job's other huge-page reservations. On builds WITHOUT `bench-internals`,
175/// `libc_madvise` still
176/// discards the return value entirely (task #719) — the kernel-response
177/// proof above is scoped to the one CI job that enables the counters. It
178/// also does not call this free function's own entry point directly (no
179/// test invokes `decommit` outside a `Reservation` method), so this
180/// function's own unconditional-syscall behavior on an INELIGIBLE range
181/// (still a no-op by kernel contract, not by Rust-level skip) remains
182/// reasoned-from-spec, not independently exercised under a real pool.
183///
184/// **Diagnostic visibility:** under the `bench-internals` feature, the
185/// `huge_decommit_attempts` counter (not an intra-doc link: `bench-internals` is excluded from the published docs.rs feature set) is incremented each time
186/// [`Reservation::decommit`](crate::Reservation::decommit)/[`Reservation::try_decommit`](crate::Reservation::try_decommit) skip the
187/// backend call on a huge-page reservation — it is NOT incremented by calls
188/// through this free function (which has no `is_huge()` to consult and always
189/// issues the syscall) or by an eligible Linux/Android >= 5.18 huge-aligned
190/// call through the safe methods (those forward to the real backend instead
191/// of skipping). Use [`reserve_aligned`](crate::api::reserve_aligned) instead of
192/// [`reserve_aligned_huge`](crate::api::reserve_aligned_huge) if you need decommit to work
193/// unconditionally, regardless of range shape or kernel version.
194///
195/// **Darwin zero-fill gap (confirmed as a real, failing-test-level gap by
196/// this crate's first real-macOS CI run, 2026-08-13 — the underlying hazard
197/// was already known repo-wide since Round 9, see
198/// <https://github.com/PHPCraftdream/sefer-alloc/blob/main/docs/CORRECTNESS_OPEN_ITEMS.md>
199/// item 48):** `MADV_DONTNEED` on Darwin and the four BSDs (FreeBSD/DragonFly/
200/// NetBSD/OpenBSD) is advisory-only for anonymous memory — unlike Linux, it does
201/// not reliably unmap the physical pages, so a decommit + [`recommit`](crate::api::recommit) roundtrip
202/// on these OS families (macOS/iOS/tvOS/watchOS — all share XNU and the same
203/// `MADV_DONTNEED` semantics, not just macOS — plus the four BSDs which use
204/// identical `MADV_DONTNEED` semantics) can observe the OLD data still resident
205/// instead of a fresh zero page. This is the same "indistinguishable
206/// from a silent no-op" shape as the huge-page case above, but for ORDINARY
207/// (non-huge) reservations on Darwin and the BSDs specifically. See
208/// <https://github.com/PHPCraftdream/sefer-alloc/blob/main/docs/CORRECTNESS_OPEN_ITEMS.md>
209/// item 48 for the open item; no fix is implemented
210/// yet (the real fix needs re-`mmap`(`MAP_FIXED`) over the range, a larger
211/// change deserving its own review round). Note: this caveat applies only to
212/// the EAGER `decommit` path (which uses `MADV_DONTNEED` on all Unix); the
213/// lazy `decommit_lazy` path uses `MADV_FREE`-family advice on Darwin/BSDs and
214/// DOES free pages on those platforms.
215pub unsafe fn decommit(base: *mut u8, start: usize, end: usize) {
216    let ps = page_size_or_poison();
217    // Failed OS page-size query (never observed on a supported platform):
218    // with the real page unknown, ANY granularity guess could make the OS
219    // round the length up across live data — fail closed instead. Silent on
220    // EVERY profile, deliberately, by design (task #1145/#1139, `4cba9c1`):
221    // that commit's own message records "Rejected: panicking (the README's
222    // 'never panics' list stays at three)", and the crate-wide poison
223    // contract documented in `page_size()`'s rustdoc and the README's
224    // "If the one-time OS query fails" section states unconditionally that
225    // `decommit`/`decommit_lazy` become no-ops, with no build-profile
226    // qualifier — unlike the range-contract tripwire below, which IS
227    // profile-qualified and documented as such. A `debug_assert!(false, ..)`
228    // here used to contradict that design decision (task #1173/L1): it made
229    // every debug-build caller of `decommit`/`Reservation::decommit` panic
230    // under a poisoned page size regardless of how well-formed the caller's
231    // OWN range was, silently promoted from a documented no-op into a crash
232    // no `# Panics` section on this function (there isn't one) ever
233    // disclosed. Use `try_decommit`/`try_page_size` to observe this state
234    // instead — see `page_size()`'s "If the one-time OS query fails".
235    if ps == PAGE_SIZE_QUERY_FAILED {
236        return;
237    }
238    // A contract violation here is silent BY SIGNATURE — this function returns
239    // `()` and has nowhere to report one. In a debug build say so loudly, so a
240    // consumer's own test fails at the mistake rather than quietly decommitting
241    // nothing and leaving the memory resident. Zero cost in release.
242    debug_assert!(
243        decommit_range_is_well_formed(start, end, ps),
244        "aligned-vmem: decommit({start}, {end}) violates the range contract \
245         (start > end, or an endpoint is not a multiple of page_size()); the \
246         call does nothing. Use try_decommit for the fallible form."
247    );
248    if start >= end || !start.is_multiple_of(ps) || !end.is_multiple_of(ps) {
249        return;
250    }
251    #[cfg(aligned_vmem_mock)]
252    mock::record(mock::Call::Decommit {
253        base: base.addr(),
254        start,
255        end,
256    });
257    #[cfg(not(aligned_vmem_mock))]
258    // SAFETY: forwarded from the caller's contract; the per-OS routine touches
259    // only kernel page-state, never the bytes.
260    //
261    // task #1180 (PUB-R2 phase 2): `decommit_pages_impl` now reports the
262    // backend's own accept/refuse outcome. This function stays infallible BY
263    // SIGNATURE (see its own doc and `# Safety`'s "Contract violations"
264    // section) — the outcome is deliberately discarded here, exactly as it
265    // always was before this task (which only changed WHERE the discard
266    // happens, from inside `libc_madvise`/`winapi_virtual_decommit`
267    // unconditionally, to here). Use [`try_decommit`] to observe it.
268    let _ = unsafe { decommit_pages_impl(base, start, end, DecommitKind::Eager) };
269}
270
271/// Whether `[start, end)` is a well-formed decommit range: `start <= end` and
272/// both endpoints are multiples of `ps`.
273///
274/// An EMPTY range (`start == end`, page-aligned) is well-formed — it is a
275/// deliberate no-op, not a mistake. That distinction is why this predicate
276/// exists separately from the `start >= end` early-return in [`decommit`]:
277/// the early return conflates "nothing to do" with "you got the arguments
278/// wrong", and only the second deserves a diagnostic.
279///
280/// **Takes `ps` as a parameter instead of reading [`page_size_or_poison`]
281/// itself (task #1213/L1 — corrected doc-vs-code drift: this comment
282/// previously claimed both callers already read `page_size_or_poison()`
283/// once and the predicate reading it again "makes no observable
284/// difference," which was true only of the debug-only `decommit`/
285/// `debug_assert!` call site, never of `try_decommit`, which read
286/// `page_size_or_poison()` once at its own top, then AGAIN inside this
287/// predicate, in every build profile including release — two atomic loads
288/// per call on the exact population `dispatch_try_decommit`'s own doc
289/// above already optimized down to one, for the opposite reason).** Every
290/// caller now takes its own `page_size_or_poison()` snapshot ONCE and
291/// passes it in here — this predicate performs no atomic load of its own.
292/// The fail-closed property (task #1156, finding F16) is unchanged: a
293/// caller MUST pass [`page_size_or_poison`]'s raw value, never the masked
294/// public [`page_size()`](crate::page_size::page_size) (which silently
295/// substitutes [`PAGE`](crate::PAGE), 4 KiB, for "unknown" in the degraded
296/// state) — every current caller pre-checks
297/// `ps == PAGE_SIZE_QUERY_FAILED` and returns before reaching this
298/// predicate, but if a future caller ever reached it without that
299/// pre-check, passing the unmasked value still fails closed by
300/// construction: `is_multiple_of(usize::MAX)` is true only for `0` or
301/// `usize::MAX`, so any ordinary non-empty range is rejected here too, not
302/// just by the callers' own pre-checks — the trap task #1139's design note
303/// ("the arithmetic is suspenders, so forgetting a check cannot reopen the
304/// hole") means to rule out.
305#[must_use]
306fn decommit_range_is_well_formed(start: usize, end: usize, ps: usize) -> bool {
307    start <= end && start.is_multiple_of(ps) && end.is_multiple_of(ps)
308}
309
310/// Task #1180 (PUB-R2 phase 2), poached finding P2: the single private
311/// dispatch point for every `try_decommit`-shaped caller — the free
312/// [`try_decommit`] AND [`Reservation::try_decommit`](crate::Reservation::try_decommit) — issuing exactly
313/// ONE `page_size_or_poison()` snapshot (`ps`, taken here and nowhere else in
314/// either caller) and calling the real backend exactly once when a call is
315/// warranted.
316///
317/// Before this task, `Reservation::try_decommit` re-validated the range
318/// itself (its own `page_size_or_poison()` load) and then, on the non-huge/
319/// eligible-huge path, forwarded to the free `try_decommit`, which validated
320/// AGAIN (a second `page_size_or_poison()` load) before finally calling
321/// [`decommit`] a third time removed from the original caller. Three relaxed
322/// atomic loads and two redundant validations for one logical operation —
323/// cheap next to the syscall when one is actually issued, but wasted work on
324/// every EMPTY/INVALID/SKIPPED call, which is exactly the population that
325/// never reaches a syscall to amortize it against. This function is now the
326/// only place that reads `page_size_or_poison()` on the `try_decommit`
327/// dispatch path and the only place that calls the backend, called by BOTH
328/// public entry points after each does its OWN validation (the free function
329/// has no bounds/huge concept to check first; the method's bounds check and
330/// huge-skip decision must run before this is even reached, since a skip
331/// must never touch the backend at all) — so the total atomic-load count for
332/// ANY call through either `try_decommit`-shaped entry point is now exactly
333/// one, not two or three. **Scope (task #1258/OH13-F5): this property
334/// describes the `try_decommit` dispatch path only, not this crate's other
335/// `page_size()`-reading call chains** — `LazyReservation::ensure_committed`/
336/// `shrink_committed` (`src/lazy_reservation.rs`) each still take their own
337/// `page_size()` snapshot for rounding and then call the free
338/// `try_commit_range`/`decommit`, which take a second, independent
339/// `page_size_or_poison()` snapshot internally; that pair was never touched
340/// by this task and is a separate, still-open residual, not a regression of
341/// the guarantee stated here.
342///
343/// Returns `DecommitOutcome::Advised` / `DecommitOutcome::Refused(_)` for a
344/// call to the SELECTED backend — on the native backend (no
345/// `aligned_vmem_mock` cfg) that is a genuinely-issued syscall, mapped
346/// straight from `decommit_pages_impl`'s own `Result`, EXCEPT when the
347/// `fault-injection` feature's decommit hook (task #1219,
348/// [`crate::fault_injection::arm_fail_next_decommit`]) is armed: then the
349/// syscall is replaced by a simulated no-code `Err` that flows through the
350/// SAME mapping arm below, so what an armed-hook test observes is the
351/// mapping itself, not a parallel construction site. No real OS refusal is
352/// involved on that injected path — no syscall ran — which is exactly why
353/// the injected error is the no-code sentinel rather than
354/// `VmemError::last_os_error()` (the commit-side seam's task #713 rule).
355/// Under the
356/// `aligned_vmem_mock` cfg no syscall runs at all — the mock backend records
357/// the call into its call log and this function unconditionally returns
358/// `Advised` without ever calling `decommit_pages_impl` (see
359/// [`DecommitOutcome::Advised`]'s own doc for why that simulated-vs-real
360/// distinction does not need a separate `Skipped`/third variant here: the
361/// call itself DID happen, from this crate's point of view — only the
362/// backend it reached differs). Never returns `Skipped` — that variant is
363/// produced by the CALLERS (the free function's own empty-range
364/// short-circuit, and `Reservation::try_decommit`'s huge-skip branch), never
365/// by this function, which is reached only when a call has already been
366/// decided.
367///
368/// # Safety
369///
370/// Same contract as [`decommit`]: `base` must be the usable base of a live
371/// reservation owned by the caller, and `[base+start, base+end)` — already
372/// validated well-formed and NON-EMPTY by the caller — must lie within its
373/// usable span.
374pub(crate) unsafe fn dispatch_try_decommit(
375    base: *mut u8,
376    start: usize,
377    end: usize,
378) -> DecommitOutcome {
379    #[cfg(aligned_vmem_mock)]
380    {
381        mock::record(mock::Call::Decommit {
382            base: base.addr(),
383            start,
384            end,
385        });
386        // The mock backend never touches the OS (see the module-level doc in
387        // `mock.rs`), so there is no real syscall outcome to report — treat a
388        // recorded mock call as accepted, matching the pre-#1180 `Ok(())`
389        // this function's callers gave under `mock`.
390        DecommitOutcome::Advised
391    }
392    #[cfg(not(aligned_vmem_mock))]
393    {
394        // Real-path decommit fault injection (feature `fault-injection`, task
395        // #1219 — the decommit-side sibling of `try_commit_range`'s commit-side
396        // seam in `api/commit_range.rs`). The hook is consulted INSTEAD of
397        // issuing the syscall, and the injected `Err` is deliberately routed
398        // through the same `Err(e) => DecommitOutcome::Refused(e)` mapping a
399        // real backend refusal takes (not an early `return` constructing
400        // `Refused` directly), so the fault-injection test exercises the
401        // mapping arm itself — the arm `docs/correctness-open-items/`
402        // `TRACKED_ci_gate_coverage.md` item 92 records as previously
403        // contradictable by NO test on ANY platform. This is the hook's only
404        // call site; the two infallible entry points (`decommit`,
405        // `decommit_lazy`) do NOT consult it — both discard the backend
406        // outcome by signature, so a fault there would have nothing
407        // observable to affect.
408        #[cfg(feature = "fault-injection")]
409        let backend_result: Result<(), VmemError> = if fault_injection::should_fail_decommit() {
410            // task #713 (same rule as the commit-side seam): this is a
411            // SIMULATED failure — no syscall ran, so `VmemError::last_os_error()`
412            // would report whatever stale `errno`/`GetLastError` a prior
413            // unrelated call left behind. The no-code sentinel reports the
414            // state without manufacturing a misleading cause. Note this also
415            // means the `Refused` payload on this path is the sentinel, NOT
416            // the `last_os_error()`-captured value `DecommitOutcome::Refused`'s
417            // own variant doc describes for the real backend path.
418            Err(VmemError::os_refusal_unknown_code())
419        } else {
420            // SAFETY: forwarded from this function's own `# Safety` contract.
421            unsafe { decommit_pages_impl(base, start, end, DecommitKind::Eager) }
422        };
423        #[cfg(not(feature = "fault-injection"))]
424        // SAFETY: forwarded from this function's own `# Safety` contract.
425        let backend_result = unsafe { decommit_pages_impl(base, start, end, DecommitKind::Eager) };
426        match backend_result {
427            Ok(()) => DecommitOutcome::Advised,
428            Err(e) => DecommitOutcome::Refused(e),
429        }
430    }
431}
432
433/// Fallible [`decommit`]: the same operation, with a channel for the one thing
434/// `decommit` cannot report — **and, since task #1180 (PUB-R2 phase 2), a
435/// channel for the OS's own accept/refuse answer too**, not just argument
436/// validity.
437///
438/// Of this crate's state-changing primitives, `decommit`/[`decommit_lazy`](crate::api::decommit_lazy) were
439/// the only pair with no fallible twin — and also the only ones that do nothing
440/// at all on a contract violation. The worst two properties met in one place:
441/// silent AND unreportable. This closes the first half.
442///
443/// # Errors
444///
445/// [`VmemError::invalid_argument`] if `start > end`, or either endpoint is not
446/// a multiple of the runtime [`page_size()`](crate::page_size::page_size). An empty page-aligned range
447/// (`start == end`) is a well-formed no-op and returns `Ok(DecommitOutcome::Skipped)`.
448///
449/// [`VmemError::os_refusal_unknown_code`] if the one-time OS page-size query
450/// itself failed — the caller's arguments are not at fault; see
451/// [`page_size()`](crate::page_size::page_size)'s "If the one-time OS query fails" paragraph.
452///
453/// Note what is deliberately NOT reported as an `Err` (the outer `Result`
454/// keeps reporting only caller-contract validity, exactly as before this
455/// task): the OS refusing or ignoring the request is `Ok(DecommitOutcome::Refused(_))`,
456/// not `Err`. `decommit` is best-effort by nature — on Darwin and the BSDs
457/// `MADV_DONTNEED` is advisory, and on a huge-page reservation eligibility
458/// depends on the platform, the requested range, and (on Linux/Android) the
459/// running kernel — see [`decommit`]'s "Huge-page granularity" section above
460/// for the exact split. Promoting an OS refusal to `Err` would conflate "your
461/// arguments were rejected" with "the platform declined to honor a
462/// well-formed request", which is exactly the ambiguity
463/// [`DecommitOutcome`] exists to separate. Use
464/// [`Reservation::decommit_reclaims_and_zeroes`](crate::Reservation::decommit_reclaims_and_zeroes) to learn what the platform
465/// actually does; [`DecommitOutcome::Advised`]/[`DecommitOutcome::Refused`] tell
466/// you what THIS call did, not what it accomplished (see that type's own doc).
467///
468/// This free function always either short-circuits on an empty range
469/// (`Ok(DecommitOutcome::Skipped)`) or forwards to the real backend — it has
470/// no [`Reservation::is_huge`](crate::Reservation::is_huge) to consult, so unlike
471/// [`Reservation::try_decommit`](crate::Reservation::try_decommit) it can never produce `Skipped` for a
472/// non-empty range; every non-empty well-formed range here becomes either
473/// `Advised` or `Refused`.
474///
475/// # Safety
476///
477/// Identical to [`decommit`]: `base` must be the usable base of a live
478/// reservation owned by the caller, and `[base+start, base+end)` must lie
479/// within its usable span.
480pub unsafe fn try_decommit(
481    base: *mut u8,
482    start: usize,
483    end: usize,
484) -> Result<DecommitOutcome, VmemError> {
485    // Failed OS page-size query: fail closed, reported as an OS-side no-code
486    // failure — the caller's arguments are NOT at fault, so this must not
487    // read as `invalid_argument` (see `page_size`'s "If the one-time OS
488    // query fails" paragraph).
489    //
490    // Single snapshot (task #1213/L1): taken once here and passed into
491    // `decommit_range_is_well_formed` below, instead of that predicate
492    // re-reading `page_size_or_poison()` a second time — this used to be
493    // two atomic loads per call, in every build profile, for a value that
494    // cannot have changed between them (the query result is fixed for the
495    // process lifetime).
496    let ps = page_size_or_poison();
497    if ps == PAGE_SIZE_QUERY_FAILED {
498        return Err(VmemError::os_refusal_unknown_code());
499    }
500    if !decommit_range_is_well_formed(start, end, ps) {
501        return Err(VmemError::invalid_argument());
502    }
503    if start == end {
504        return Ok(DecommitOutcome::Skipped);
505    }
506    // SAFETY: forwarded from this function's own `# Safety` contract, which is
507    // identical to `decommit`'s; the range was just validated well-formed and
508    // non-empty above.
509    Ok(unsafe { dispatch_try_decommit(base, start, end) })
510}