aligned_vmem/error.rs
1//! [`VmemError`] — the failure cause carried by the `try_*` API.
2//!
3//! Every fallible entry point ([`crate::try_reserve_aligned`],
4//! [`crate::try_recommit`], …) returns `Result<_, VmemError>`. The error either
5//! carries the raw OS error code (`errno` on Unix, `GetLastError` on Windows)
6//! captured at the point of failure, or a sentinel for a caller contract
7//! violation (bad `size`/`align`) that never reached the OS.
8
9use core::fmt;
10
11/// The cause of a virtual-memory operation failure.
12///
13/// - [`os_code`](Self::os_code) is `Some(code)` for a genuine OS refusal with
14/// a known cause, where `code` is `errno` (Unix) or `GetLastError`
15/// (Windows).
16/// - [`os_code`](Self::os_code) is `None` for [`VmemError::invalid_argument`]
17/// — a contract violation (e.g. non-power-of-two `align`, zero `size`)
18/// detected before any syscall — **and also** for a no-code failure on
19/// the OS side (see [`os_refusal_unknown_code`](Self::os_refusal_unknown_code)).
20/// Use [`is_invalid_argument`](Self::is_invalid_argument)
21/// to tell the two `None` cases apart — task #712/#713 (2026-08-09): an
22/// earlier version of this type stored the raw code as a bare `u32`
23/// defaulting to `0` when unavailable, making "no OS code available"
24/// indistinguishable from a genuine `code 0` / `ERROR_SUCCESS` — `os_code()`
25/// reported `Some(0)` for both. Storing `Option<u32>` closes that gap at the
26/// type level.
27#[derive(Clone, Copy, PartialEq, Eq)]
28pub struct VmemError {
29 /// Raw OS error code, or `None` when this is an invalid-argument error OR
30 /// a no-code failure on the OS side.
31 code: Option<u32>,
32 /// `true` when the error is a caller contract violation (no OS involved).
33 invalid_arg: bool,
34}
35
36impl VmemError {
37 /// A caller-contract-violation error: the arguments were rejected before
38 /// any OS call. This covers MORE than the `size`/`align` contract, which is
39 /// why neither this doc nor `Display` names that one contract specifically
40 /// any more (task #1046, finding R7-7 — `Display` used to print
41 /// "size/align contract violation" for every one of these). The rejected
42 /// classes, enumerated from the actual call sites rather than guessed:
43 ///
44 /// - `size`/`align` contract: `align` not a power of two, `size` not a page
45 /// multiple, `size == 0`, or the `size + align` sum overflowing.
46 /// - The `initial_commit` contract on the lazy path.
47 /// - The commit/recommit RANGE contract: `start > end`, either endpoint not
48 /// a multiple of the runtime `page_size()`, or `end` past `len()`.
49 /// - Huge-page alignment on the Linux/Android huge path.
50 /// - An internal fit computation failing — deliberately mapped here rather
51 /// than to a stale OS error code, because no OS call refused anything.
52 ///
53 /// The specific cause is documented on the method that returned it; this
54 /// type carries no payload naming which parameter was at fault.
55 #[must_use]
56 #[inline]
57 pub const fn invalid_argument() -> Self {
58 Self {
59 code: None,
60 invalid_arg: true,
61 }
62 }
63
64 /// Wrap a raw OS error code (`errno` / `GetLastError`).
65 #[must_use]
66 #[inline]
67 pub const fn from_os_code(code: u32) -> Self {
68 Self {
69 code: Some(code),
70 invalid_arg: false,
71 }
72 }
73
74 /// A no-code failure on the OS side — the operation failed without a
75 /// real OS error code to report. FOUR sources — **keep this count in
76 /// sync with the list below when adding one**: task #1139 added the
77 /// fourth and left the count reading "Three", corrected by task #1141
78 /// (task #1106/L2 — an earlier revision of this doc called ALL of them
79 /// a "genuine OS refusal", which is false for the third and fourth):
80 /// - under miri (no real `errno`/`GetLastError` exists to read) — a
81 /// genuine refusal by the miri stand-in;
82 /// - the rare case where the platform's own `raw_os_error()` itself
83 /// returns `None` — a genuine OS refusal with an unavailable cause;
84 /// - (task #1068/F2) the crate's own rejection of the kernel's R7-11
85 /// address-zero `mmap` grant on Unix — `mmap` SUCCEEDED and the crate
86 /// unmapped the grant itself, so no syscall refused anything and there
87 /// is no real code to report. This source is not a refusal by the OS
88 /// at all; it shares this sentinel because it is equally not a caller
89 /// contract violation, and the type carries no further discrimination
90 /// (crate still at 0.2.0, unpublished — a distinct kind was judged not
91 /// worth the public-API surface; see the task #1106/L2 record);
92 /// - a FAILED one-time OS page-size query (never observed on a supported
93 /// platform): [`crate::try_page_size`] and every page-granular `try_*`
94 /// state operation (`try_decommit`, `try_recommit`,
95 /// `try_commit_range`, the lazy reservation constructor) report the
96 /// crate's fail-closed degraded state through this sentinel — the
97 /// caller's arguments are not at fault, and no per-call OS code
98 /// exists (the query failed once, at first use, possibly long before
99 /// the reporting call). Same no-new-kind reasoning as the third
100 /// source above.
101 ///
102 /// Distinct from [`invalid_argument`](Self::invalid_argument):
103 /// `is_invalid_argument()` is `false` here — the failure originated on
104 /// the OS side (or in the crate's response to an unusable OS grant), not
105 /// in the caller's arguments.
106 ///
107 /// **This FOUR-source count is scoped to production causes; it
108 /// deliberately excludes two TEST-ONLY construction SOURCES, spread
109 /// across FOUR TEST-ONLY construction SITES** (task #1173/L2,
110 /// re-measured for this doc's own correction — task #1194 — against the
111 /// actual call sites rather than re-asserted from an earlier audit's
112 /// count; re-measured again task #1249 after task #1219 added the
113 /// decommit-side fault-injection hook, which grew the `fault-injection`
114 /// source from one site to two. Counted with doc mentions EXCLUDED,
115 /// because a raw `grep -rn "VmemError::os_refusal_unknown_code()"
116 /// crates/aligned-vmem/src/` also matches prose like this very
117 /// sentence — its total therefore changes whenever this paragraph is
118 /// edited, which is exactly how task #1194's first attempt recorded a
119 /// figure its own edit falsified one line later. The stable count is
120 /// `grep -rn "VmemError::os_refusal_unknown_code()"
121 /// crates/aligned-vmem/src/ | grep -vE ":\s*(///|//!|//)"` → 11 real
122 /// construction sites; of those 11, 7 are production sites — matching
123 /// the four causes below — and 4 are
124 /// test-only sites): the `aligned_vmem_mock` backend's scripted
125 /// commit/reserve fault injection (`crate::mock`, gated on that cfg —
126 /// TWO sites, `take_reserve_fault`/`take_commit_fault`) and the
127 /// real-path `fault-injection` feature's simulated commit AND decommit
128 /// failures (`crate::fault_injection`, TWO sites — `api/commit_range.rs`
129 /// and, since task #1219, `api/decommit.rs`'s `dispatch_try_decommit`)
130 /// both also construct this sentinel, to simulate a no-code OS failure
131 /// deterministically without touching the OS — see each module's own
132 /// doc for why NEITHER SOURCE is a fifth or sixth PRODUCTION source:
133 /// all four sites exist only under test-only cfgs or an explicitly-armed
134 /// opt-in feature, and none is reachable in an ordinary disarmed build.
135 #[must_use]
136 #[inline]
137 pub const fn os_refusal_unknown_code() -> Self {
138 Self {
139 code: None,
140 invalid_arg: false,
141 }
142 }
143
144 /// The raw OS error code. `None` for
145 /// [`invalid_argument`](Self::invalid_argument) OR for a no-code failure
146 /// on the OS side
147 /// ([`os_refusal_unknown_code`](Self::os_refusal_unknown_code)) — use
148 /// [`is_invalid_argument`](Self::is_invalid_argument) to tell those two
149 /// `None` cases apart.
150 #[must_use]
151 #[inline]
152 pub const fn os_code(&self) -> Option<u32> {
153 self.code
154 }
155
156 /// `true` if this is a caller contract violation rather than an OS refusal.
157 #[must_use]
158 #[inline]
159 pub const fn is_invalid_argument(&self) -> bool {
160 self.invalid_arg
161 }
162
163 /// Capture the current thread's last OS error (`errno` / `GetLastError`).
164 /// Yields [`os_refusal_unknown_code`](Self::os_refusal_unknown_code) under
165 /// miri, or if the platform's own `raw_os_error()` returns `None`.
166 ///
167 /// **Timing contract**: call this IMMEDIATELY after the syscall whose
168 /// failure it is meant to capture, before any other FFI call (including
169 /// cleanup) — any intervening call may overwrite `errno`/`GetLastError`
170 /// (task #713).
171 #[must_use]
172 pub fn last_os_error() -> Self {
173 match last_os_error_code() {
174 Some(code) => Self::from_os_code(code),
175 None => Self::os_refusal_unknown_code(),
176 }
177 }
178}
179
180impl fmt::Debug for VmemError {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 if self.invalid_arg {
183 f.write_str("VmemError::InvalidArgument")
184 } else {
185 f.debug_struct("VmemError")
186 .field("os_code", &self.code)
187 .finish()
188 }
189 }
190}
191
192impl fmt::Display for VmemError {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 if self.invalid_arg {
195 f.write_str("invalid argument (argument contract violation)")
196 } else {
197 match self.code {
198 Some(code) => write!(f, "OS virtual-memory error (code {code})"),
199 None => f.write_str(
200 "OS virtual-memory error (unknown OS error code — either a \
201 genuine OS refusal with an unreadable cause, or the crate \
202 rejected an unusable OS grant, e.g. a granted address-zero \
203 mapping)",
204 ),
205 }
206 }
207 }
208}
209
210impl std::error::Error for VmemError {}
211
212impl From<VmemError> for std::io::Error {
213 fn from(e: VmemError) -> Self {
214 match e.os_code() {
215 Some(code) => {
216 // Win32 GetLastError returns a u32; a code with the high bit set
217 // (e.g. HRESULT 0x8007000E) becomes negative when cast to i32.
218 // Use try_from to detect overflow; fall back to Unknown on
219 // overflow (theoretical—no real VirtualAlloc/VirtualFree
220 // failure produces such a code in practice).
221 match i32::try_from(code) {
222 Ok(signed) => std::io::Error::from_raw_os_error(signed),
223 Err(_) => {
224 // Code doesn't fit in i32 (high bit set). Preserve the
225 // VmemError as io::Error::other to avoid silent
226 // misinterpretation.
227 std::io::Error::other(e)
228 }
229 }
230 }
231 None if e.is_invalid_argument() => {
232 std::io::Error::new(std::io::ErrorKind::InvalidInput, e)
233 }
234 None => std::io::Error::other(e),
235 }
236 }
237}
238
239#[cfg(not(miri))]
240fn last_os_error_code() -> Option<u32> {
241 std::io::Error::last_os_error()
242 .raw_os_error()
243 .map(|c| c as u32)
244}
245
246#[cfg(miri)]
247fn last_os_error_code() -> Option<u32> {
248 None
249}