Skip to main content

windows_thread_ambient_sys/
error_mode.rs

1// Copyright (c) Mike Grier.
2
3//! The thread error mode aspect.
4//!
5//! The thread error mode decides whether a hard device error -- the classic
6//! absent-removable-drive case -- raises a modal dialog or fails the call. A
7//! thread-pool worker's mode is `0`, meaning the critical-error handler is
8//! enabled, so a blocking call remoted onto shared infrastructure can put a
9//! dialog on a thread the whole process depends on.
10//!
11//! # This aspect is both capturable and declarable, deliberately
12//!
13//! Unlike the others it appears in both halves of the crate's decomposition. It
14//! is readable, so a caller may capture the submitting thread's value and
15//! transplant it; and it is the aspect consumers most often want to *override*
16//! with a policy of their own. Offering only one of those would bake one
17//! consumer's answer into a platform layer: a consumer running on shared threads
18//! will force the dialog-suppressing bits, while a consumer owning a private
19//! thread, where a modal dialog is its own problem and nobody else's, is
20//! entitled to the opposite choice.
21//!
22//! # Why the alignment bit is not representable
23//!
24//! [`ThreadErrorMode`] can hold only the three bits `SetThreadErrorMode`
25//! accepts. `SEM_NOALIGNMENTFAULTEXCEPT` is excluded because it is *rejected*
26//! per-thread -- and, measured, an invalid bit fails the **whole** call rather
27//! than being dropped from it. A type that could represent it would let a caller
28//! combine it with valid bits and silently lose the entire change, so this is a
29//! case for a type that cannot express the invalid state rather than a runtime
30//! check nobody expected to fail. See
31//! [`windows-platform-probes`](../../windows-platform-probes/DESIGN-NOTES.md),
32//! which pins the measurement as a test.
33//!
34//! # Example
35//!
36//! ```
37//! use windows_thread_ambient_sys::ThreadErrorMode;
38//!
39//! let entry = ThreadErrorMode::capture()?;
40//!
41//! let mode = ThreadErrorMode::FAIL_CRITICAL_ERRORS
42//!     .union(ThreadErrorMode::NO_OPEN_FILE_ERROR_BOX);
43//! let guard = mode.apply()?;
44//! assert!(ThreadErrorMode::capture()?.contains(ThreadErrorMode::FAIL_CRITICAL_ERRORS));
45//!
46//! // Release explicitly. Dropping the guard also restores, but discards any
47//! // failure to do so, because a destructor has no caller to report to.
48//! guard.release()?;
49//! assert_eq!(ThreadErrorMode::capture()?, entry);
50//! # Ok::<(), Box<dyn std::error::Error>>(())
51//! ```
52
53use std::fmt;
54use std::io;
55
56use windows_sys::Win32::System::Diagnostics::Debug::{
57    GetThreadErrorMode, SEM_FAILCRITICALERRORS, SEM_NOGPFAULTERRORBOX, SEM_NOOPENFILEERRORBOX,
58    SetThreadErrorMode, THREAD_ERROR_MODE,
59};
60
61fn set_thread_error_mode(mode: THREAD_ERROR_MODE) -> Result<THREAD_ERROR_MODE, io::Error> {
62    #[cfg(test)]
63    if let Some(error) = crate::test_injection::hit(crate::test_injection::FaultPoint::ErrorModeSet)
64    {
65        return Err(error);
66    }
67
68    let mut previous: THREAD_ERROR_MODE = 0;
69    // SAFETY: `previous` is a valid writable destination. Callers provide only
70    // typed values or values previously returned by Windows.
71    let ok = unsafe { SetThreadErrorMode(mode, &mut previous) };
72    if ok == 0 {
73        Err(io::Error::last_os_error())
74    } else {
75        Ok(previous)
76    }
77}
78
79/// Every bit this crate will place in a [`ThreadErrorMode`].
80const SUPPORTED: THREAD_ERROR_MODE = ThreadErrorMode::FAIL_CRITICAL_ERRORS
81    .union(ThreadErrorMode::NO_GP_FAULT_ERROR_BOX)
82    .union(ThreadErrorMode::NO_OPEN_FILE_ERROR_BOX)
83    .0;
84
85/// A thread error mode, restricted to the bits Windows accepts per thread.
86///
87/// Construct one from the associated constants and [`union`](Self::union), or
88/// from a raw value with [`from_bits`](Self::from_bits).
89#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
90pub struct ThreadErrorMode(THREAD_ERROR_MODE);
91
92impl ThreadErrorMode {
93    /// No bits set: hard errors raise the system's dialogs.
94    pub const NONE: Self = Self(0);
95
96    /// Fail calls that hit a critical error instead of raising a dialog.
97    pub const FAIL_CRITICAL_ERRORS: Self = Self(SEM_FAILCRITICALERRORS);
98
99    /// Suppress the general-protection-fault error box.
100    pub const NO_GP_FAULT_ERROR_BOX: Self = Self(SEM_NOGPFAULTERRORBOX);
101
102    /// Suppress the file-open error box.
103    pub const NO_OPEN_FILE_ERROR_BOX: Self = Self(SEM_NOOPENFILEERRORBOX);
104
105    /// The raw value, as Win32 represents it.
106    #[must_use]
107    pub const fn bits(self) -> THREAD_ERROR_MODE {
108        self.0
109    }
110
111    /// Build a mode from a raw value.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`UnsupportedBits`] if `bits` contains anything
116    /// `SetThreadErrorMode` does not accept. This is a rejection rather than a
117    /// mask: silently dropping a bit would report installing a value that was
118    /// not installed, which is the failure this type exists to prevent.
119    ///
120    /// # Example
121    ///
122    /// ```
123    /// use windows_thread_ambient_sys::ThreadErrorMode;
124    ///
125    /// assert_eq!(
126    ///     ThreadErrorMode::from_bits(0x0001),
127    ///     Ok(ThreadErrorMode::FAIL_CRITICAL_ERRORS)
128    /// );
129    ///
130    /// // 0x0004 is SEM_NOALIGNMENTFAULTEXCEPT, which cannot be set per thread.
131    /// // It is refused even beside a valid bit, because Windows would install
132    /// // neither: an invalid bit fails the whole call.
133    /// let refused = ThreadErrorMode::from_bits(0x0001 | 0x0004)
134    ///     .expect_err("the alignment bit is not settable per thread");
135    /// assert_eq!(refused.bits(), 0x0004);
136    /// ```
137    pub const fn from_bits(bits: THREAD_ERROR_MODE) -> Result<Self, UnsupportedBits> {
138        let unsupported = bits & !SUPPORTED;
139        if unsupported == 0 {
140            Ok(Self(bits))
141        } else {
142            Err(UnsupportedBits { bits: unsupported })
143        }
144    }
145
146    /// Both modes' bits.
147    #[must_use]
148    pub const fn union(self, other: Self) -> Self {
149        Self(self.0 | other.0)
150    }
151
152    /// Whether every bit of `other` is set.
153    #[must_use]
154    pub const fn contains(self, other: Self) -> bool {
155        self.0 & other.0 == other.0
156    }
157
158    /// Whether no bits are set.
159    #[must_use]
160    pub const fn is_empty(self) -> bool {
161        self.0 == 0
162    }
163
164    /// Read the calling thread's current mode.
165    ///
166    /// # Errors
167    ///
168    /// Returns [`UnsupportedBits`] if Windows reports a bit this type cannot
169    /// hold. Measured, that does not happen: the thread error mode is
170    /// independent storage rather than a view of the process error mode, so a
171    /// process-scope bit such as `SEM_NOALIGNMENTFAULTEXCEPT` does not show
172    /// through here. The result is still surfaced rather than assumed away,
173    /// because a type unable to represent a state the platform can produce would
174    /// be a bug, and this is where that would first be observable.
175    pub fn capture() -> Result<Self, UnsupportedBits> {
176        // SAFETY: the call takes no arguments and has no preconditions.
177        Self::from_bits(unsafe { GetThreadErrorMode() })
178    }
179
180    /// Install this mode on the calling thread until the guard is released.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`ApplyError`] if Windows refuses the value, in which case
185    /// nothing was installed and the thread is untouched.
186    pub fn apply(self) -> Result<ErrorModeGuard, ApplyError> {
187        let previous = set_thread_error_mode(self.0).map_err(|source| ApplyError {
188            requested: self,
189            source,
190        })?;
191        Ok(ErrorModeGuard {
192            previous,
193            released: false,
194        })
195    }
196}
197
198impl fmt::Display for ThreadErrorMode {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        write!(f, "0x{:04X}", self.0)
201    }
202}
203
204/// A raw value contained bits the per-thread error mode does not accept.
205#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
206pub struct UnsupportedBits {
207    bits: THREAD_ERROR_MODE,
208}
209
210impl UnsupportedBits {
211    /// Just the offending bits.
212    #[must_use]
213    pub const fn bits(self) -> THREAD_ERROR_MODE {
214        self.bits
215    }
216}
217
218impl fmt::Display for UnsupportedBits {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        write!(
221            f,
222            "0x{:04X} cannot be set on a thread; SetThreadErrorMode rejects it \
223             and would install none of the accompanying bits either",
224            self.bits
225        )
226    }
227}
228
229impl std::error::Error for UnsupportedBits {}
230
231/// Windows refused to install a thread error mode.
232#[derive(Debug)]
233pub struct ApplyError {
234    requested: ThreadErrorMode,
235    source: io::Error,
236}
237
238impl ApplyError {
239    /// The mode that could not be installed.
240    #[must_use]
241    pub const fn requested(&self) -> ThreadErrorMode {
242        self.requested
243    }
244
245    /// The underlying Win32 code, if there was one.
246    #[must_use]
247    pub fn raw_os_error(&self) -> Option<i32> {
248        self.source.raw_os_error()
249    }
250
251    /// Builds an `ApplyError` reporting a specific outcome, for tests that
252    /// need one without a real `SetThreadErrorMode` failure to provoke --
253    /// every bit this crate ever installs is one Windows accepts, so there is
254    /// no reachable failure through the public API at all.
255    #[cfg(test)]
256    pub(crate) fn synthetic(requested: ThreadErrorMode, os_error: i32) -> Self {
257        Self {
258            requested,
259            source: io::Error::from_raw_os_error(os_error),
260        }
261    }
262}
263
264impl fmt::Display for ApplyError {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        write!(
267            f,
268            "could not install thread error mode {}: {}",
269            self.requested, self.source
270        )
271    }
272}
273
274impl std::error::Error for ApplyError {
275    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
276        Some(&self.source)
277    }
278}
279
280/// Windows refused to restore the thread's entry error mode.
281#[derive(Debug)]
282pub struct RestoreError {
283    unrestored: THREAD_ERROR_MODE,
284    source: io::Error,
285}
286
287impl RestoreError {
288    #[cfg(test)]
289    pub(crate) fn for_test(unrestored: THREAD_ERROR_MODE, code: i32) -> Self {
290        Self {
291            unrestored,
292            source: io::Error::from_raw_os_error(code),
293        }
294    }
295
296    /// The value the thread should have been returned to.
297    #[must_use]
298    pub const fn unrestored_bits(&self) -> THREAD_ERROR_MODE {
299        self.unrestored
300    }
301
302    /// The underlying Win32 code, if there was one.
303    #[must_use]
304    pub fn raw_os_error(&self) -> Option<i32> {
305        self.source.raw_os_error()
306    }
307
308    /// Builds a `RestoreError` reporting a specific outcome, for tests that
309    /// need one without a genuine restore failure to provoke.
310    #[cfg(test)]
311    pub(crate) fn synthetic(unrestored: THREAD_ERROR_MODE, os_error: i32) -> Self {
312        Self {
313            unrestored,
314            source: io::Error::from_raw_os_error(os_error),
315        }
316    }
317}
318
319impl fmt::Display for RestoreError {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        write!(
322            f,
323            "could not restore thread error mode 0x{:04X}; the thread is left \
324             contaminated: {}",
325            self.unrestored, self.source
326        )
327    }
328}
329
330impl std::error::Error for RestoreError {
331    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
332        Some(&self.source)
333    }
334}
335
336/// Holds an installed error mode until it is released.
337///
338/// # Release explicitly on the ordinary path
339///
340/// [`release`](Self::release) reports whether the thread was actually restored.
341/// Dropping the guard instead restores on a best-effort basis and **discards**
342/// any failure, because a destructor has no caller to report to. That is the
343/// right behaviour while unwinding, where no report could be delivered anyway,
344/// and the wrong behaviour on the ordinary path -- so the ordinary path calls
345/// `release`.
346///
347/// Restoration failure is not fatal here. Contrast impersonation, whose restore
348/// failure is fail-fast because returning a shared worker under an unknown
349/// identity is a process-wide security failure; leaving a thread with the wrong
350/// error mode is a real contamination but not that, and imposing the strictest
351/// aspect's semantics on every aspect is precisely what this crate's composite
352/// exists to avoid.
353#[must_use = "dropping the guard restores the error mode but discards any failure to do so"]
354#[derive(Debug)]
355pub struct ErrorModeGuard {
356    previous: THREAD_ERROR_MODE,
357    released: bool,
358}
359
360impl ErrorModeGuard {
361    /// The mode this thread had before the guard was installed.
362    ///
363    /// # Errors
364    ///
365    /// Returns [`UnsupportedBits`] in the same unreachable-by-measurement case
366    /// as [`ThreadErrorMode::capture`]. The guard itself keeps the raw value, so
367    /// restoration round-trips exactly whatever Windows reported, whether or not
368    /// this crate's type can name it.
369    pub const fn previous(&self) -> Result<ThreadErrorMode, UnsupportedBits> {
370        ThreadErrorMode::from_bits(self.previous)
371    }
372
373    /// Restore the thread's entry mode, reporting whether it worked.
374    ///
375    /// # Errors
376    ///
377    /// Returns [`RestoreError`] if Windows refused, leaving the thread
378    /// contaminated with whatever was installed.
379    pub fn release(mut self) -> Result<(), RestoreError> {
380        self.released = true;
381        Self::restore(self.previous)
382    }
383
384    fn restore(previous: THREAD_ERROR_MODE) -> Result<(), RestoreError> {
385        set_thread_error_mode(previous)
386            .map(|_| ())
387            .map_err(|source| RestoreError {
388                unrestored: previous,
389                source,
390            })
391    }
392}
393
394impl Drop for ErrorModeGuard {
395    fn drop(&mut self) {
396        if !self.released {
397            // Best effort by design: see the type's documentation.
398            let _ = Self::restore(self.previous);
399        }
400    }
401}
402
403#[cfg(test)]
404mod tests;