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    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;