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 ktm().is_some()
151}
152
153/// Read the calling thread's current transaction, if any.
154fn current_raw() -> Option<HANDLE> {
155 let ktm = ktm()?;
156 // SAFETY: the call takes no arguments and has no preconditions.
157 let raw = unsafe { (ktm.get_current)() };
158 Some(raw)
159}
160
161/// Is `raw` the "no transaction" sentinel?
162fn is_none_sentinel(raw: HANDLE) -> bool {
163 raw.is_null() || raw == INVALID_HANDLE_VALUE
164}
165
166/// An owned duplicate of a thread's current transaction.
167#[derive(Debug)]
168pub struct TransactionContext(OwnedHandle);
169
170impl TransactionContext {
171 /// The duplicated handle, for a consumer that must reach the raw object.
172 #[must_use]
173 pub fn as_raw(&self) -> HANDLE {
174 self.0.as_raw_handle().cast::<c_void>()
175 }
176}
177
178/// Why a transaction could not be captured or applied.
179#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
180#[non_exhaustive]
181pub enum TransactionFailure {
182 /// `ktmw32.dll` or its thread-transaction entry points are unavailable.
183 ///
184 /// Distinct from "the thread had no transaction": one says the platform
185 /// cannot answer, the other is an answer.
186 Unsupported,
187 /// The transaction handle could not be duplicated.
188 Duplicate,
189 /// Windows refused to install or restore a thread transaction.
190 Install,
191}
192
193/// A transaction aspect operation failed.
194#[derive(Debug)]
195pub struct TransactionError {
196 failure: TransactionFailure,
197 source: Option<io::Error>,
198}
199
200impl TransactionError {
201 /// Which stage failed.
202 #[must_use]
203 pub const fn failure(&self) -> TransactionFailure {
204 self.failure
205 }
206
207 /// The underlying Win32 code, if there was one.
208 #[must_use]
209 pub fn raw_os_error(&self) -> Option<i32> {
210 self.source.as_ref().and_then(io::Error::raw_os_error)
211 }
212}
213
214impl fmt::Display for TransactionError {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 let what = match self.failure {
217 TransactionFailure::Unsupported => {
218 "ktmw32.dll does not offer the thread-transaction entry points"
219 }
220 TransactionFailure::Duplicate => "the transaction handle could not be duplicated",
221 TransactionFailure::Install => "the thread transaction could not be set",
222 };
223 match &self.source {
224 Some(source) => write!(f, "{what}: {source}"),
225 None => f.write_str(what),
226 }
227 }
228}
229
230impl std::error::Error for TransactionError {
231 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
232 self.source
233 .as_ref()
234 .map(|source| source as &(dyn std::error::Error + 'static))
235 }
236}
237
238fn error(failure: TransactionFailure) -> TransactionError {
239 TransactionError {
240 failure,
241 source: Some(io::Error::last_os_error()),
242 }
243}
244
245/// Capture the calling thread's current transaction.
246///
247/// Returns [`Captured::Absent`] when the thread carries no transaction, which is
248/// the ordinary case, and [`Captured::Present`] holding an **owned duplicate**
249/// otherwise, so the value does not depend on the caller keeping its own handle
250/// open.
251///
252/// # Errors
253///
254/// Returns [`TransactionFailure::Unsupported`] if the entry points are
255/// unavailable, and [`TransactionFailure::Duplicate`] if the handle could not be
256/// duplicated. Neither is reported as "no transaction": a platform that cannot
257/// answer is not the same as an answer.
258pub fn capture() -> Result<Captured<TransactionContext>, TransactionError> {
259 let raw = current_raw().ok_or(TransactionError {
260 failure: TransactionFailure::Unsupported,
261 source: None,
262 })?;
263 if is_none_sentinel(raw) {
264 return Ok(Captured::Absent);
265 }
266 let mut duplicate: HANDLE = std::ptr::null_mut();
267 // SAFETY: `raw` is the live handle Windows just reported for this thread,
268 // `duplicate` is a valid writable destination, and both process handles are
269 // pseudo-handles needing no cleanup.
270 let ok = unsafe {
271 DuplicateHandle(
272 GetCurrentProcess(),
273 raw,
274 GetCurrentProcess(),
275 &mut duplicate,
276 0,
277 FALSE,
278 DUPLICATE_SAME_ACCESS,
279 )
280 };
281 if ok == 0 {
282 return Err(error(TransactionFailure::Duplicate));
283 }
284 // SAFETY: `DuplicateHandle` succeeded, so this handle is owned solely here.
285 let owned = unsafe { OwnedHandle::from_raw_handle(duplicate.cast()) };
286 Ok(Captured::Present(TransactionContext(owned)))
287}
288
289/// Install `raw` as the calling thread's transaction.
290fn set_current(raw: HANDLE) -> Result<(), TransactionError> {
291 let ktm = ktm().ok_or(TransactionError {
292 failure: TransactionFailure::Unsupported,
293 source: None,
294 })?;
295 // SAFETY: `raw` is either null (clearing the transaction) or a live handle.
296 let ok = unsafe { (ktm.set_current)(raw) };
297 if ok == 0 {
298 return Err(error(TransactionFailure::Install));
299 }
300 Ok(())
301}
302
303/// Run `operation` under `captured`.
304///
305/// # `Absent` clears rather than leaving alone
306///
307/// This aspect is the first where [`Captured::Absent`] is reachable, so the
308/// distinction between the two empty states has teeth here.
309/// [`Captured::NotCaptured`] leaves the running thread's own transaction alone,
310/// because the caller never asked. [`Captured::Absent`] *installs* "no
311/// transaction", because the caller did ask and the answer was none -- and a
312/// worker that happened to carry a transaction would otherwise silently enlist
313/// the caller's work in it.
314///
315/// # Errors
316///
317/// Returns a [`TransactionError`] if the transaction could not be installed, in
318/// which case `operation` did not run, or if the thread's entry transaction
319/// could not be restored afterwards, in which case it did.
320///
321/// # Panics
322///
323/// Does not panic. Restore failure is reported rather than fatal, unlike
324/// impersonation, whose fail-fast restore exists because an unknown *identity*
325/// on a shared worker is a process-wide security failure. A stale transaction is
326/// a real contamination but not that one.
327pub fn with_applied<F, T>(
328 captured: &Captured<TransactionContext>,
329 operation: F,
330) -> Result<T, TransactionError>
331where
332 F: FnOnce() -> T,
333{
334 let guard = install(captured)?;
335 let outcome = operation();
336 guard.release().map(|()| outcome)
337}
338
339/// Install `captured` on the calling thread until the guard is released.
340///
341/// This is the form the composite uses, because it keeps the operation's value
342/// and the restoration outcome separable; [`with_applied`] has no room for both
343/// and discards the value if restoration fails.
344///
345/// # Errors
346///
347/// Returns a [`TransactionError`] if the transaction could not be installed, in
348/// which case the thread is untouched.
349pub fn install<'captured>(
350 captured: &'captured Captured<TransactionContext>,
351) -> Result<TransactionGuard<'captured>, TransactionError> {
352 let desired = match captured {
353 Captured::NotCaptured => {
354 return Ok(TransactionGuard {
355 previous: None,
356 released: false,
357 captured: PhantomData,
358 });
359 }
360 Captured::Absent => std::ptr::null_mut(),
361 Captured::Present(context) => context.as_raw(),
362 };
363
364 let previous = current_raw().ok_or(TransactionError {
365 failure: TransactionFailure::Unsupported,
366 source: None,
367 })?;
368 set_current(desired)?;
369 Ok(TransactionGuard {
370 previous: Some(previous),
371 released: false,
372 captured: PhantomData,
373 })
374}
375
376/// Holds an installed thread transaction until released.
377///
378/// Not `Send`: it restores the thread it was created on. Moving one to
379/// another thread would restore *that* thread's transaction to a value
380/// captured on a different one, corrupting both.
381///
382/// That property is currently a consequence of a field type rather than of an
383/// explicit bound -- `previous: Option<HANDLE>` is a raw pointer, and raw
384/// pointers are not `Send`. Nothing would notice if `HANDLE` were later
385/// replaced by an integer newtype, at which point the guard would silently
386/// become `Send` and this paragraph would become false. So the claim is
387/// pinned by a test rather than left as prose:
388///
389/// ```compile_fail,E0277
390/// fn assert_send<T: Send>() {}
391/// assert_send::<windows_thread_ambient_sys::transaction::TransactionGuard<'static>>();
392/// ```
393///
394/// # Why it borrows the captured context
395///
396/// The installed value is a raw `HANDLE` owned by the
397/// [`TransactionContext`] the guard was built from. Windows recycles handle
398/// values aggressively, so if that context were dropped while the guard were
399/// still alive, the thread would be left enlisting work in whatever kernel
400/// object had since inherited the value -- silently, and reachable from safe
401/// code.
402///
403/// The lifetime is what forbids it. Owning a duplicate fixes the *capture*
404/// side of that problem (see this module's documentation); this fixes the
405/// *installed* side, which is the same hazard one step later.
406///
407/// # Examples
408///
409/// Keeping the context alive alongside the guard is the correct shape:
410///
411/// ```no_run
412/// use windows_thread_ambient_sys::transaction;
413///
414/// let captured = transaction::capture()?;
415/// let guard = transaction::install(&captured)?;
416/// // ... transacted work happens here ...
417/// guard.release()?;
418/// # Ok::<(), Box<dyn std::error::Error>>(())
419/// ```
420///
421/// Dropping the context while its handle is still installed would leave the
422/// thread enlisting work in whatever kernel object Windows had since recycled
423/// that handle value onto. The borrow is what forbids it, so this does not
424/// compile:
425///
426/// ```compile_fail,E0597
427/// use windows_thread_ambient_sys::transaction;
428///
429/// let guard = {
430/// let captured = transaction::capture().expect("capture");
431/// transaction::install(&captured).expect("install")
432/// // `captured` is dropped here, closing the handle the guard installed.
433/// };
434/// let _ = guard.release();
435/// ```
436#[must_use = "dropping the guard restores the transaction but discards any failure to do so"]
437#[derive(Debug)]
438pub struct TransactionGuard<'captured> {
439 /// `None` when nothing was installed, so nothing is restored either.
440 previous: Option<HANDLE>,
441 released: bool,
442 /// Keeps the installed handle's owner alive for as long as it is
443 /// installed. Carries no data.
444 captured: PhantomData<&'captured Captured<TransactionContext>>,
445}
446
447impl TransactionGuard<'_> {
448 /// Restore the thread's entry transaction, including "none".
449 ///
450 /// # Errors
451 ///
452 /// Returns a [`TransactionError`] if the entry transaction could not be
453 /// restored, leaving the thread contaminated.
454 pub fn release(mut self) -> Result<(), TransactionError> {
455 self.released = true;
456 Self::restore(self.previous)
457 }
458
459 fn restore(previous: Option<HANDLE>) -> Result<(), TransactionError> {
460 let Some(previous) = previous else {
461 return Ok(());
462 };
463 set_current(if is_none_sentinel(previous) {
464 std::ptr::null_mut()
465 } else {
466 previous
467 })
468 }
469}
470
471impl Drop for TransactionGuard<'_> {
472 fn drop(&mut self) {
473 if !self.released {
474 // Best effort: a destructor has no caller to report to.
475 let _ = Self::restore(self.previous);
476 }
477 }
478}
479
480#[cfg(test)]
481mod tests;