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;