aligned_vmem/api/recommit.rs
1use crate::error::VmemError;
2#[cfg(aligned_vmem_mock)]
3use crate::mock;
4#[cfg(not(aligned_vmem_mock))]
5use crate::os::recommit_pages_impl;
6use crate::page_size::{page_size_or_poison, PAGE_SIZE_QUERY_FAILED};
7
8/// Recommit pages `[base + start, base + end)` previously passed to
9/// [`decommit`](crate::api::decommit). On Windows this re-commits physical pages
10/// (`VirtualAlloc(MEM_COMMIT)`); on Unix re-access is implicit so this is a
11/// no-op. On the Darwin family (macOS/iOS/tvOS/watchOS) specifically, whether
12/// re-access reads back zeroed pages or the pre-decommit contents is not
13/// guaranteed either way — see [`decommit`](crate::api::decommit)'s Darwin caveat for why.
14///
15/// Returns `true` if the range is now committed (or the call was a
16/// well-formed no-op — an empty PAGE-ALIGNED range, `start == end`), and
17/// `false` if the OS refused to
18/// commit the pages (commit-charge exhaustion / true OOM) OR the offsets
19/// violated the contract below. On `false` the caller MUST NOT write into
20/// `[base+start, base+end)`. Never panics. For the cause use [`try_recommit`].
21///
22/// # Safety
23///
24/// - `base` must be the [`as_ptr`](crate::Reservation::as_ptr) of a live
25/// reservation whose `[base+start, base+end)` range was previously
26/// decommitted.
27/// - **`end <= reservation.len()`** (the reservation's usable span, in
28/// bytes) — this is a MANDATORY precondition of the pointer arithmetic
29/// this function performs internally (`base.add(start)` in the Windows
30/// backend's `recommit_pages_impl`; the Unix and miri backends are no-ops
31/// but the contract is stated platform-independently), not merely a
32/// functional/behavioral preference. Before task #1229/F6 this function
33/// was the only range-taking free function whose `# Safety` lacked the
34/// bound: [`decommit`](crate::api::decommit)'s states it in full (task
35/// #1213/L2, whose wording this matches), `try_decommit` and
36/// `decommit_lazy` carry it (restated in prose / in full — the latter
37/// since task #1235, which replaced `decommit_lazy`'s earlier bare
38/// same-contract reference), and the
39/// [`commit_range`](crate::api::commit_range) pair spells it out as
40/// `end <= len`. For an `unsafe fn`, a
41/// bounds requirement that determines whether pointer arithmetic is even
42/// defined belongs inside `# Safety` itself, restated in full. Passing
43/// `end > reservation.len()` is undefined behavior (with `start <= end`
44/// the bound is what keeps the backend's `base.add(start)` offset
45/// in-bounds and the OS call's span `[base+start, base+end)` inside the
46/// reservation), distinct from — and a strictly worse violation than —
47/// the `page_size()`-multiple contract below, which merely returns
48/// `false` on violation, never UB. Callers through the safe
49/// [`Reservation::recommit`](crate::Reservation::recommit) /
50/// [`Reservation::try_recommit`](crate::Reservation::try_recommit)
51/// methods are not exposed: both bounds-check `end <= self.len()` before
52/// delegating here, so the gap reaches only callers of this free
53/// function directly.
54/// - `start`/`end` must be multiples of the runtime page size
55/// ([`page_size()`](crate::page_size)) with `start <= end` — a violation
56/// returns `false` (task #712: an earlier version of this function
57/// clamped a contract violation to the WRITE-PERMITTING `true` sentinel,
58/// which already caused a real crash — see
59/// <https://github.com/PHPCraftdream/sefer-alloc/blob/main/docs/CORRECTNESS_OPEN_ITEMS.md>
60/// item 6 for the incident this class of bug produces on Windows).
61#[must_use]
62pub unsafe fn recommit(base: *mut u8, start: usize, end: usize) -> bool {
63 // SAFETY: forwarded from the caller's contract.
64 unsafe { try_recommit(base, start, end).is_ok() }
65}
66
67/// Fallible [`recommit`]: `Ok(())` if the range is now committed (or was a
68/// well-formed no-op), `Err(VmemError::invalid_argument())` if the offsets
69/// violated the contract (misaligned, or `start > end`), `Err(VmemError)`
70/// carrying the OS cause on genuine commit failure.
71///
72/// # Safety
73///
74/// Same contract as [`recommit`], with the bound restated here rather than
75/// only referenced (task #1229/F6): this function is the one that actually
76/// reaches the backend — its non-mock arm calls `recommit_pages_impl`
77/// directly, and [`recommit`] forwards through here — so a caller auditing
78/// only this section must see it. `base` must be the
79/// [`as_ptr`](crate::Reservation::as_ptr) of a live reservation whose
80/// `[base+start, base+end)` range was previously decommitted, and
81/// **`end <= reservation.len()`** — passing a larger `end` is undefined
82/// behavior (the backend computes `base.add(start)` and nothing from
83/// `end`; with `start <= end` the bound is what keeps that offset
84/// in-bounds and the OS call's span `[base+start, base+end)` inside the
85/// reservation), a strictly worse violation than the
86/// `page_size()`-multiple / `start <= end` contract, which merely returns
87/// `Err(VmemError::invalid_argument())`, never UB.
88pub unsafe fn try_recommit(base: *mut u8, start: usize, end: usize) -> Result<(), VmemError> {
89 let ps = page_size_or_poison();
90 // Failed OS page-size query: fail closed with the OS-side no-code error
91 // (NOT `invalid_argument` — the caller's arguments are not at fault).
92 // See `page_size`'s "If the one-time OS query fails" paragraph.
93 if ps == PAGE_SIZE_QUERY_FAILED {
94 return Err(VmemError::os_refusal_unknown_code());
95 }
96 if start > end || !start.is_multiple_of(ps) || !end.is_multiple_of(ps) {
97 return Err(VmemError::invalid_argument());
98 }
99 if start == end {
100 return Ok(());
101 }
102 #[cfg(aligned_vmem_mock)]
103 {
104 mock::record(mock::Call::Recommit {
105 base: base.addr(),
106 start,
107 end,
108 });
109 mock::take_commit_fault().map_or(Ok(()), Err)
110 }
111 #[cfg(not(aligned_vmem_mock))]
112 // SAFETY: forwarded from the caller's contract.
113 unsafe {
114 recommit_pages_impl(base, start, end)
115 }
116}