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}