Skip to main content

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}