Skip to main content

aligned_vmem/api/
decommit_lazy.rs

1#[cfg(aligned_vmem_mock)]
2use crate::mock;
3#[cfg(not(aligned_vmem_mock))]
4use crate::os::{decommit_pages_impl, DecommitKind};
5use crate::page_size::{page_size_or_poison, PAGE_SIZE_QUERY_FAILED};
6
7/// Lazy decommit variant: hint the OS it MAY reclaim `[base+start, base+end)`
8/// under memory pressure, cheaper than [`decommit`](crate::api::decommit) (Linux `MADV_FREE`,
9/// macOS/iOS `MADV_FREE_REUSABLE`, FreeBSD/DragonFly `MADV_FREE`,
10/// NetBSD/OpenBSD `MADV_FREE`, other Unix (including tvOS/watchOS) falls
11/// back to `MADV_DONTNEED`; Windows falls back to the eager [`decommit`](crate::api::decommit)
12/// path, which has no lazy equivalent).
13///
14/// Unlike [`decommit`](crate::api::decommit), on Linux the pages are NOT necessarily zeroed on next
15/// access if the kernel has not yet reclaimed them (a write before reclamation
16/// keeps the old contents and cancels the free) — so this is appropriate only
17/// for memory whose contents the caller no longer needs but has not yet
18/// overwritten. Cheaper reclaim; the kernel takes pages only under pressure.
19/// **This benign-re-fault story is Linux-only: on Windows this call is the
20/// eager [`decommit`](crate::api::decommit) path (see the summary above), where a write into the
21/// range before [`recommit`](crate::api::recommit) is a hard `STATUS_ACCESS_VIOLATION` crash, not a
22/// re-fault** — see [`decommit`](crate::api::decommit)'s platform-divergence paragraph above for the
23/// incident this already caused.
24///
25/// **On macOS/iOS specifically, the cost ordering above is INVERTED, on the
26/// RSS axis only** — see [`decommit`](crate::api::decommit)'s Darwin caveat: eager `decommit`'s
27/// `MADV_DONTNEED` is a no-op there (drops nothing), while this lazy variant's
28/// `MADV_FREE_REUSABLE` DOES drop the physical footprint immediately (not just
29/// "under pressure"). Neither call zero-fills on next access on macOS/iOS —
30/// that half of the non-guarantee is unchanged from the eager path. On
31/// tvOS/watchOS this function falls back to the same `MADV_DONTNEED` as
32/// [`decommit`](crate::api::decommit) (see the "other Unix" case in the summary above — the arm
33/// that excludes macOS/iOS specifically, not "other Unix" in a general
34/// sense), so there it IS a true no-op like the eager path, on both axes.
35/// This tvOS/watchOS fallback is this crate's current `madv_free_advice` cfg
36/// coverage (REASONED-FROM-SPEC, not verified on tvOS/watchOS hardware or a
37/// tvOS/watchOS build target -- neither is available to this crate's CI):
38/// `MADV_FREE_REUSABLE`'s numeric value is defined by XNU, the kernel all
39/// four Darwin targets share, so it MAY work identically there too; but
40/// tvOS/watchOS's userspace sandbox restrictions are unverified for this
41/// specific advice value, so this is a plausible widening candidate, not an
42/// established fact (see `madv_free_advice`'s doc and
43/// <https://github.com/PHPCraftdream/sefer-alloc/blob/main/docs/CORRECTNESS_OPEN_ITEMS.md>
44/// item 48's S9 note, which must agree with this wording -- keep both in
45/// sync if either changes).
46///
47/// **No fallible form:** this entry point is intentionally infallible, for the
48/// same safety rationale as [`decommit`](crate::api::decommit). The `()` return carries no
49/// write-permitting sentinel, so silently skipping on a contract violation is
50/// safe. A `try_decommit_lazy` could be added as a future additive API decision.
51///
52/// `start`/`end` requirements and the `# Safety` contract are the same as
53/// [`decommit`](crate::api::decommit)'s, with ONE deliberate behavioral
54/// difference (settled by task #1072): a VIOLATED range here is a silent
55/// no-op on EVERY build profile. The eager [`decommit`](crate::api::decommit)
56/// trips its `debug_assert!` tripwire in debug builds, and
57/// [`try_decommit`](crate::api::try_decommit) reports the violation as `Err`
58/// on every profile; this lazy variant has neither. See `decommit`'s
59/// "Contract violations, by build profile" paragraph for the full split.
60///
61/// # Safety
62///
63/// Same contract as [`decommit`](crate::api::decommit), with the bound
64/// restated here in full rather than only referenced (task #1235,
65/// applying task #1213/L2's rule — the one task #1229/F6 already applied
66/// to `try_recommit`): this function does not forward through
67/// [`decommit`](crate::api::decommit); its non-mock arm calls the same
68/// backend, `decommit_pages_impl(base, start, end, DecommitKind::Lazy)`,
69/// directly, and on Windows there is no lazy/eager split — the identical
70/// `base.add(start)` arithmetic before `VirtualFree(MEM_DECOMMIT)` runs
71/// from THIS entry point — so a caller auditing only this section must
72/// see the bound, not chase a reference to another function's
73/// `# Safety`:
74///
75/// - `base` must be the [`as_ptr`](crate::Reservation::as_ptr) of a live
76///   reservation the caller owns.
77/// - **`end <= reservation.len()`** (the reservation's usable span, in
78///   bytes) — MANDATORY, for the reasons [`decommit`](crate::api::decommit)'s
79///   own `# Safety` bullet states in full (both real backends compute
80///   `base.add(start)` and nothing from `end`; with `start <= end` the
81///   bound is what keeps that offset in-bounds and the OS call's span
82///   `[base+start, base+end)` inside the reservation). Violating it is
83///   undefined behavior, distinct from — and a strictly worse violation
84///   than — the `page_size()`-multiple / `start <= end` range contract,
85///   which here (the deliberate task #1072 difference above) is a silent
86///   no-op on EVERY build profile, never UB.
87/// - `[base+start, base+end)` must contain no data the caller still
88///   needs — its contents are discarded (on the lazy `MADV_FREE`-family
89///   paths the discard is deferred and a write before reclamation cancels
90///   it; see the summary above for the per-platform split).
91pub unsafe fn decommit_lazy(base: *mut u8, start: usize, end: usize) {
92    let ps = page_size_or_poison();
93    // Failed OS page-size query: fail closed (see `decommit`). Silent on
94    // every profile, consistent with this entry point's deliberate
95    // no-tripwire design (task #1072).
96    if ps == PAGE_SIZE_QUERY_FAILED {
97        return;
98    }
99    if start >= end || !start.is_multiple_of(ps) || !end.is_multiple_of(ps) {
100        return;
101    }
102    #[cfg(aligned_vmem_mock)]
103    mock::record(mock::Call::DecommitLazy {
104        base: base.addr(),
105        start,
106        end,
107    });
108    #[cfg(not(aligned_vmem_mock))]
109    // SAFETY: forwarded from the caller's contract; the per-OS routine touches
110    // only kernel page-state, never the bytes.
111    //
112    // task #1180 (PUB-R2 phase 2): `decommit_pages_impl` now reports the
113    // backend's own accept/refuse outcome (needed by the EAGER `try_decommit`
114    // path). This LAZY entry point stays infallible BY SIGNATURE — out of
115    // this task's scope, see its own "No fallible form" doc paragraph above —
116    // so the outcome is discarded here exactly as it always was.
117    let _ = unsafe { decommit_pages_impl(base, start, end, DecommitKind::Lazy) };
118}