windows_thread_ambient_sys/transaction.rs
1// Copyright (c) Mike Grier.
2
3//! The TxF transaction aspect.
4//!
5//! A thread may carry a *current transaction*, and `CreateFileW` and its
6//! relatives silently join it. Remoting such a call to a worker that carries no
7//! transaction would therefore perform it outside the caller's transaction --
8//! quietly, and with no error to notice.
9//!
10//! # The documented entry points do not exist as exports
11//!
12//! `ktmw32.h` documents `GetCurrentTransaction` and `SetCurrentTransaction`, and
13//! MSDN names `Ktmw32.dll` as their library. **Neither is exported from it** --
14//! verified against the export table of the shipping DLL, which offers
15//! `CreateTransaction`, `CommitTransaction`, `RollbackTransaction` and their
16//! neighbours but nothing named `CurrentTransaction`. The header declares them
17//! as `FORCEINLINE` wrappers, so a C caller links nothing; what actually carries
18//! the operation is `RtlGetCurrentTransaction` / `RtlSetCurrentTransaction` in
19//! **`ntdll.dll`**, and that is what this module binds.
20//!
21//! Two consequences worth stating plainly rather than discovering later. The
22//! aspect depends on an `Rtl`-prefixed `ntdll` export rather than a documented
23//! Win32 one -- unavoidable, since no documented export exists, but it is a
24//! weaker footing than the rest of this crate and the reason binding is lazy and
25//! failure is a typed `Unsupported` rather than a link error. And
26//! `RtlSetCurrentTransaction` returns `BOOLEAN`, a **single byte**, not the
27//! four-byte `BOOL` its documented wrapper returns; reading it as `BOOL` would
28//! test three bytes of whatever happened to be in the register.
29//!
30//! Binding is lazy so that a consumer which never captures a transaction pays
31//! nothing and, on a system where the symbols are absent, gets a typed failure
32//! instead of a process that will not start. The module handle is deliberately
33//! never freed: it is a process-lifetime binding resolved at most once, and
34//! unloading it while another thread is inside a call would be a use-after-free
35//! for no benefit.
36//!
37//! # The hazard this aspect cannot remove
38//!
39//! A transaction handle is a reference to a shared kernel object, so capturing
40//! one does **not** give the worker a private transaction: the caller may commit
41//! or roll it back while the worker is still inside it. Owning a duplicate fixes
42//! only the *lifetime* problem -- the request cannot be left holding a closed
43//! handle -- and not the *state* problem. Sequencing that is the consumer's
44//! responsibility and cannot be enforced here.
45//!
46//! TxF is also deprecated by Microsoft. That is a reason to keep this aspect
47//! optional and out of any minimal capture set, not a reason to omit it: a
48//! caller using transacted NTFS today still needs its work remoted correctly.
49//!
50//! # Example
51//!
52//! ```
53//! use windows_thread_ambient_sys::Captured;
54//! use windows_thread_ambient_sys::transaction;
55//!
56//! // An ordinary thread carries no transaction. That is an *answer*, not a
57//! // failure, so it is `Absent` rather than an error.
58//! let captured = transaction::capture()?;
59//! assert!(matches!(captured, Captured::Absent));
60//!
61//! // Applying `Absent` installs "no transaction" rather than leaving the
62//! // running thread's own alone -- the caller asked, and the answer was none,
63//! // so a worker that happened to carry one must not enlist this work in it.
64//! let value = transaction::with_applied(&captured, || 42)?;
65//! assert_eq!(value, 42);
66//! # Ok::<(), Box<dyn std::error::Error>>(())
67//! ```
68
69use std::ffi::c_void;
70use std::fmt;
71use std::io;
72use std::marker::PhantomData;
73use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
74use std::sync::OnceLock;
75
76use windows_sys::Win32::Foundation::{
77 DUPLICATE_SAME_ACCESS, DuplicateHandle, FALSE, HANDLE, HMODULE, INVALID_HANDLE_VALUE,
78};
79use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
80use windows_sys::Win32::System::Threading::GetCurrentProcess;
81
82use crate::captured::Captured;
83
84type GetCurrentTransactionFn = unsafe extern "system" fn() -> HANDLE;
85
86/// Returns `BOOLEAN`, which is one byte. See the module documentation.
87type SetCurrentTransactionFn = unsafe extern "system" fn(HANDLE) -> u8;
88
89/// The lazily resolved thread-transaction entry points.
90struct Ktm {
91 get_current: GetCurrentTransactionFn,
92 set_current: SetCurrentTransactionFn,
93}
94
95static KTM: OnceLock<Option<Ktm>> = OnceLock::new();
96
97/// Resolve one symbol from a system DLL, loading it on first use.
98///
99/// Returns `None` if the library or the symbol is absent, which the aspect
100/// reports as [`TransactionFailure::Unsupported`] rather than silently treating
101/// as "no transaction" -- those are different facts.
102///
103/// Shared with this module's tests, which need `ktmw32.dll`'s transaction
104/// *creation* entry points to exercise the non-empty path.
105pub(crate) fn system_proc(dll: &str, name: &[u8]) -> Option<unsafe extern "system" fn() -> isize> {
106 debug_assert_eq!(
107 name.last(),
108 Some(&0),
109 "a symbol name must be NUL-terminated for GetProcAddress"
110 );
111 let wide: Vec<u16> = dll.encode_utf16().chain(std::iter::once(0)).collect();
112 // SAFETY: `wide` is NUL-terminated and outlives the call. The handle is
113 // intentionally never freed; see the module documentation.
114 let module = unsafe { LoadLibraryW(wide.as_ptr()) };
115 if module.is_null() {
116 return None;
117 }
118 // SAFETY: `module` came from `LoadLibraryW` and is still loaded, and `name`
119 // is NUL-terminated.
120 unsafe { GetProcAddress(module as HMODULE, name.as_ptr()) }
121}
122
123fn ktm() -> Option<&'static Ktm> {
124 KTM.get_or_init(|| {
125 // The documented ktmw32 names are header inlines and are not exported;
126 // these ntdll entry points are what they call.
127 let get = system_proc("ntdll.dll", b"RtlGetCurrentTransaction\0")?;
128 let set = system_proc("ntdll.dll", b"RtlSetCurrentTransaction\0")?;
129 // SAFETY: both symbols are transmuted to the signatures ntdll declares
130 // for them, including `RtlSetCurrentTransaction`'s single-byte BOOLEAN.
131 unsafe {
132 Some(Ktm {
133 get_current: std::mem::transmute::<
134 unsafe extern "system" fn() -> isize,
135 GetCurrentTransactionFn,
136 >(get),
137 set_current: std::mem::transmute::<
138 unsafe extern "system" fn() -> isize,
139 SetCurrentTransactionFn,
140 >(set),
141 })
142 }
143 })
144 .as_ref()
145}
146
147/// Whether this system offers the thread-transaction entry points at all.
148#[must_use]
149pub fn is_supported() -> bool {
150 #[cfg(test)]
151 if crate::test_injection::hit(crate::test_injection::FaultPoint::TransactionSupport).is_some() {
152 return false;
153 }
154
155 ktm().is_some()
156}
157
158/// Read the calling thread's current transaction, if any.
159fn current_raw() -> Option<HANDLE> {
160 let ktm = ktm()?;
161 // SAFETY: the call takes no arguments and has no preconditions.
162 let raw = unsafe { (ktm.get_current)() };
163 Some(raw)
164}
165
166/// Is `raw` the "no transaction" sentinel?
167fn is_none_sentinel(raw: HANDLE) -> bool {
168 raw.is_null() || raw == INVALID_HANDLE_VALUE
169}
170
171/// An owned duplicate of a thread's current transaction.
172#[derive(Debug)]
173pub struct TransactionContext(OwnedHandle);
174
175impl TransactionContext {
176 /// The duplicated handle, for a consumer that must reach the raw object.
177 #[must_use]
178 pub fn as_raw(&self) -> HANDLE {
179 self.0.as_raw_handle().cast::<c_void>()
180 }
181}
182
183/// Why a transaction could not be captured or applied.
184#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
185#[non_exhaustive]
186pub enum TransactionFailure {
187 /// `ktmw32.dll` or its thread-transaction entry points are unavailable.
188 ///
189 /// Distinct from "the thread had no transaction": one says the platform
190 /// cannot answer, the other is an answer.
191 Unsupported,
192 /// The transaction handle could not be duplicated.
193 Duplicate,
194 /// Windows refused to install or restore a thread transaction.
195 Install,
196}
197
198/// A transaction aspect operation failed.
199#[derive(Debug)]
200pub struct TransactionError {
201 failure: TransactionFailure,
202 source: Option<io::Error>,
203}
204
205impl TransactionError {
206 #[cfg(test)]
207 pub(crate) fn for_test(failure: TransactionFailure, code: Option<i32>) -> Self {
208 Self {
209 failure,
210 source: code.map(io::Error::from_raw_os_error),
211 }
212 }
213
214 /// Which stage failed.
215 #[must_use]
216 pub const fn failure(&self) -> TransactionFailure {
217 self.failure
218 }
219
220 /// The underlying Win32 code, if there was one.
221 #[must_use]
222 pub fn raw_os_error(&self) -> Option<i32> {
223 self.source.as_ref().and_then(io::Error::raw_os_error)
224 }
225
226 /// Builds a `TransactionError` reporting a specific outcome, for tests
227 /// elsewhere in the crate that need one without provoking a real ktmw32
228 /// or `RtlSetCurrentTransaction` failure.
229 #[cfg(test)]
230 pub(crate) fn synthetic(failure: TransactionFailure, os_error: Option<i32>) -> Self {
231 Self {
232 failure,
233 source: os_error.map(io::Error::from_raw_os_error),
234 }
235 }
236}
237
238impl fmt::Display for TransactionError {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 let what = match self.failure {
241 TransactionFailure::Unsupported => {
242 "ktmw32.dll does not offer the thread-transaction entry points"
243 }
244 TransactionFailure::Duplicate => "the transaction handle could not be duplicated",
245 TransactionFailure::Install => "the thread transaction could not be set",
246 };
247 match &self.source {
248 Some(source) => write!(f, "{what}: {source}"),
249 None => f.write_str(what),
250 }
251 }
252}
253
254impl std::error::Error for TransactionError {
255 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
256 self.source
257 .as_ref()
258 .map(|source| source as &(dyn std::error::Error + 'static))
259 }
260}
261
262fn error(failure: TransactionFailure) -> TransactionError {
263 TransactionError {
264 failure,
265 source: Some(io::Error::last_os_error()),
266 }
267}
268
269/// Capture the calling thread's current transaction.
270///
271/// Returns [`Captured::Absent`] when the thread carries no transaction, which is
272/// the ordinary case, and [`Captured::Present`] holding an **owned duplicate**
273/// otherwise, so the value does not depend on the caller keeping its own handle
274/// open.
275///
276/// # Errors
277///
278/// Returns [`TransactionFailure::Unsupported`] if the entry points are
279/// unavailable, and [`TransactionFailure::Duplicate`] if the handle could not be
280/// duplicated. Neither is reported as "no transaction": a platform that cannot
281/// answer is not the same as an answer.
282pub fn capture() -> Result<Captured<TransactionContext>, TransactionError> {
283 let raw = current_raw().ok_or(TransactionError {
284 failure: TransactionFailure::Unsupported,
285 source: None,
286 })?;
287 if is_none_sentinel(raw) {
288 return Ok(Captured::Absent);
289 }
290 let mut duplicate: HANDLE = std::ptr::null_mut();
291 // SAFETY: `raw` is the live handle Windows just reported for this thread,
292 // `duplicate` is a valid writable destination, and both process handles are
293 // pseudo-handles needing no cleanup.
294 let ok = unsafe {
295 DuplicateHandle(
296 GetCurrentProcess(),
297 raw,
298 GetCurrentProcess(),
299 &mut duplicate,
300 0,
301 FALSE,
302 DUPLICATE_SAME_ACCESS,
303 )
304 };
305 if ok == 0 {
306 return Err(error(TransactionFailure::Duplicate));
307 }
308 // SAFETY: `DuplicateHandle` succeeded, so this handle is owned solely here.
309 let owned = unsafe { OwnedHandle::from_raw_handle(duplicate.cast()) };
310 Ok(Captured::Present(TransactionContext(owned)))
311}
312
313/// Install `raw` as the calling thread's transaction.
314fn set_current(raw: HANDLE) -> Result<(), TransactionError> {
315 #[cfg(test)]
316 if let Some(source) =
317 crate::test_injection::hit(crate::test_injection::FaultPoint::TransactionSet)
318 {
319 return Err(TransactionError {
320 failure: TransactionFailure::Install,
321 source: Some(source),
322 });
323 }
324
325 let ktm = ktm().ok_or(TransactionError {
326 failure: TransactionFailure::Unsupported,
327 source: None,
328 })?;
329 // SAFETY: `raw` is either null (clearing the transaction) or a live handle.
330 let ok = unsafe { (ktm.set_current)(raw) };
331 if ok == 0 {
332 return Err(error(TransactionFailure::Install));
333 }
334 Ok(())
335}
336
337/// Run `operation` under `captured`.
338///
339/// # `Absent` clears rather than leaving alone
340///
341/// This aspect is the first where [`Captured::Absent`] is reachable, so the
342/// distinction between the two empty states has teeth here.
343/// [`Captured::NotCaptured`] leaves the running thread's own transaction alone,
344/// because the caller never asked. [`Captured::Absent`] *installs* "no
345/// transaction", because the caller did ask and the answer was none -- and a
346/// worker that happened to carry a transaction would otherwise silently enlist
347/// the caller's work in it.
348///
349/// # Errors
350///
351/// Returns a [`TransactionError`] if the transaction could not be installed, in
352/// which case `operation` did not run, or if the thread's entry transaction
353/// could not be restored afterwards, in which case it did.
354///
355/// # Panics
356///
357/// Does not panic. Restore failure is reported rather than fatal, unlike
358/// impersonation, whose fail-fast restore exists because an unknown *identity*
359/// on a shared worker is a process-wide security failure. A stale transaction is
360/// a real contamination but not that one.
361pub fn with_applied<F, T>(
362 captured: &Captured<TransactionContext>,
363 operation: F,
364) -> Result<T, TransactionError>
365where
366 F: FnOnce() -> T,
367{
368 let guard = install(captured)?;
369 let outcome = operation();
370 guard.release().map(|()| outcome)
371}
372
373/// Install `captured` on the calling thread until the guard is released.
374///
375/// This is the form the composite uses, because it keeps the operation's value
376/// and the restoration outcome separable; [`with_applied`] has no room for both
377/// and discards the value if restoration fails.
378///
379/// # Errors
380///
381/// Returns a [`TransactionError`] if the transaction could not be installed, in
382/// which case the thread is untouched.
383pub fn install<'captured>(
384 captured: &'captured Captured<TransactionContext>,
385) -> Result<TransactionGuard<'captured>, TransactionError> {
386 let desired = match captured {
387 Captured::NotCaptured => {
388 return Ok(TransactionGuard {
389 previous: None,
390 released: false,
391 captured: PhantomData,
392 });
393 }
394 Captured::Absent => std::ptr::null_mut(),
395 Captured::Present(context) => context.as_raw(),
396 };
397
398 let previous = current_raw().ok_or(TransactionError {
399 failure: TransactionFailure::Unsupported,
400 source: None,
401 })?;
402 set_current(desired)?;
403 Ok(TransactionGuard {
404 previous: Some(previous),
405 released: false,
406 captured: PhantomData,
407 })
408}
409
410/// Holds an installed thread transaction until released.
411///
412/// Not `Send`: it restores the thread it was created on. Moving one to
413/// another thread would restore *that* thread's transaction to a value
414/// captured on a different one, corrupting both.
415///
416/// That property is currently a consequence of a field type rather than of an
417/// explicit bound -- `previous: Option<HANDLE>` is a raw pointer, and raw
418/// pointers are not `Send`. Nothing would notice if `HANDLE` were later
419/// replaced by an integer newtype, at which point the guard would silently
420/// become `Send` and this paragraph would become false. So the claim is
421/// pinned by a test rather than left as prose:
422///
423/// ```compile_fail,E0277
424/// fn assert_send<T: Send>() {}
425/// assert_send::<windows_thread_ambient_sys::transaction::TransactionGuard<'static>>();
426/// ```
427///
428/// # Why it borrows the captured context
429///
430/// The installed value is a raw `HANDLE` owned by the
431/// [`TransactionContext`] the guard was built from. Windows recycles handle
432/// values aggressively, so if that context were dropped while the guard were
433/// still alive, the thread would be left enlisting work in whatever kernel
434/// object had since inherited the value -- silently, and reachable from safe
435/// code.
436///
437/// The lifetime is what forbids it. Owning a duplicate fixes the *capture*
438/// side of that problem (see this module's documentation); this fixes the
439/// *installed* side, which is the same hazard one step later.
440///
441/// # Examples
442///
443/// Keeping the context alive alongside the guard is the correct shape:
444///
445/// ```no_run
446/// use windows_thread_ambient_sys::transaction;
447///
448/// let captured = transaction::capture()?;
449/// let guard = transaction::install(&captured)?;
450/// // ... transacted work happens here ...
451/// guard.release()?;
452/// # Ok::<(), Box<dyn std::error::Error>>(())
453/// ```
454///
455/// Dropping the context while its handle is still installed would leave the
456/// thread enlisting work in whatever kernel object Windows had since recycled
457/// that handle value onto. The borrow is what forbids it, so this does not
458/// compile:
459///
460/// ```compile_fail,E0597
461/// use windows_thread_ambient_sys::transaction;
462///
463/// let guard = {
464/// let captured = transaction::capture().expect("capture");
465/// transaction::install(&captured).expect("install")
466/// // `captured` is dropped here, closing the handle the guard installed.
467/// };
468/// let _ = guard.release();
469/// ```
470#[must_use = "dropping the guard restores the transaction but discards any failure to do so"]
471#[derive(Debug)]
472pub struct TransactionGuard<'captured> {
473 /// `None` when nothing was installed, so nothing is restored either.
474 previous: Option<HANDLE>,
475 released: bool,
476 /// Keeps the installed handle's owner alive for as long as it is
477 /// installed. Carries no data.
478 captured: PhantomData<&'captured Captured<TransactionContext>>,
479}
480
481impl TransactionGuard<'_> {
482 /// Restore the thread's entry transaction, including "none".
483 ///
484 /// # Errors
485 ///
486 /// Returns a [`TransactionError`] if the entry transaction could not be
487 /// restored, leaving the thread contaminated.
488 pub fn release(mut self) -> Result<(), TransactionError> {
489 self.released = true;
490 Self::restore(self.previous)
491 }
492
493 fn restore(previous: Option<HANDLE>) -> Result<(), TransactionError> {
494 let Some(previous) = previous else {
495 return Ok(());
496 };
497 set_current(if is_none_sentinel(previous) {
498 std::ptr::null_mut()
499 } else {
500 previous
501 })
502 }
503}
504
505impl Drop for TransactionGuard<'_> {
506 fn drop(&mut self) {
507 if !self.released {
508 // Best effort: a destructor has no caller to report to.
509 let _ = Self::restore(self.previous);
510 }
511 }
512}
513
514#[cfg(test)]
515mod tests;