Skip to main content

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;