Skip to main content

std\sys\thread_local\guard/
windows.rs

1//! Support for Windows TLS destructors.
2//!
3//! Windows has an API to provide a destructor for a FLS (fiber local storage) variable,
4//! which behaves similarly to a TLS variable for our purpose [1].
5//!
6//! All TLS destructors are tracked by *us*, not the Windows runtime.
7//! This means that we have a global list of destructors for
8//! each TLS key or variable that we know about.
9//!
10//! [1]: https://devblogs.microsoft.com/oldnewthing/20191011-00/?p=102989
11
12use core::ffi::c_void;
13use core::sync::atomic::{AtomicBool, AtomicU32, Ordering, fence};
14
15use crate::cell::Cell;
16use crate::ptr;
17use crate::sys::c::{self, FLS_OUT_OF_INDEXES};
18
19pub type Key = u32;
20
21unsafe fn create(dtor: c::PFLS_CALLBACK_FUNCTION) -> Key {
22    let key_result = unsafe { c::FlsAlloc(dtor) };
23
24    if key_result == c::FLS_OUT_OF_INDEXES {
25        rtabort!("out of FLS keys");
26    }
27
28    key_result
29}
30
31unsafe fn set(key: Key, ptr: *const c_void) {
32    let result = unsafe { c::FlsSetValue(key, ptr) };
33
34    if result == c::FALSE {
35        rtabort!("failed to set FLS value");
36    }
37}
38
39fn is_thread_a_fiber() -> bool {
40    let res = unsafe { c::IsThreadAFiber() };
41    res == c::TRUE
42}
43
44static KEY: AtomicU32 = AtomicU32::new(FLS_OUT_OF_INDEXES);
45
46/// Used to track whether we are currently in the critical section of `enable`.
47/// For miri, these atomic operations cause synchronization that can mask user bugs,
48/// and they are not needed as `atexit` is anyway not supported, so we can skip them.
49struct EnableGuard;
50static AT_EXIT_HOOK_CALLED: AtomicBool = AtomicBool::new(false);
51static ACTIVE_ENABLE_CALLS: AtomicU32 = AtomicU32::new(0);
52
53impl EnableGuard {
54    // Mark the start of an `enable` call, returning whether the `atexit` hook has already been called or not.
55    fn new() -> (Self, bool) {
56        if cfg!(miri) {
57            return (Self, false);
58        }
59        ACTIVE_ENABLE_CALLS.fetch_add(1, Ordering::Relaxed);
60
61        // Both `new` and `start_exit` publish state to one atomic and inspect the other.
62        // `AcqRel` is insufficient because neither read is required to observe the other's publication,
63        // so we could create the guard but `start_exit` would not see any active enable calls.
64        // `SeqCst` ensures that there's a single global order between the publish and check,
65        // so at least one side must observe the other and bail.
66        fence(Ordering::SeqCst);
67
68        let at_exit_called = AT_EXIT_HOOK_CALLED.load(Ordering::Relaxed);
69
70        (Self, at_exit_called)
71    }
72
73    /// Mark the start of process exit, returning whether we should free the FLS key or not.
74    fn start_exit() -> bool {
75        // After this hook starts, new destructor registration will be skipped,
76        // causing TLS destructors initialized after this point to leak.
77        if AT_EXIT_HOOK_CALLED.swap(true, Ordering::Relaxed) {
78            // Cleanup already started, there is nothing else to do.
79            return false;
80        }
81
82        fence(Ordering::SeqCst);
83
84        let any_active_enabled_called = ACTIVE_ENABLE_CALLS.load(Ordering::Relaxed) != 0;
85
86        if any_active_enabled_called {
87            // If another thread is currently in `enable`, it may already have loaded this key and may be about to call `FlsSetValue`.
88            // So we must *not* call free the FLS key.
89            //
90            // During real process exit this is harmless because the `cleanup` hook is always available,
91            // and the FLS callback will be triggered normally by the OS.
92            //
93            // During DLL unload, the unloader cannot safely have threads running code from the DLL except for the destructors,
94            // so there must not be any `enable` calls active anyway.
95            return false;
96        }
97
98        return true;
99    }
100}
101
102#[cfg(not(miri))]
103impl Drop for EnableGuard {
104    fn drop(&mut self) {
105        ACTIVE_ENABLE_CALLS.fetch_sub(1, Ordering::Relaxed);
106    }
107}
108
109/// Set up the current thread to invoke `cleanup` when it finishes.
110pub fn enable() {
111    let registered = if cfg!(target_thread_local) {
112        #[thread_local]
113        static REGISTERED: Cell<bool> = Cell::new(false);
114        REGISTERED.replace(true)
115    } else {
116        // `#[thread_local]` is unavailable on windows-gnu (`target_thread_local` is off),
117        // but setting the FLS key's value is about as expensive as `TlsGet`, so we don't bother tracking registration separately.
118        false
119    };
120
121    if !registered {
122        // We are in a critical section where we are trying to register a destructor for the current thread.
123        // We need to avoid racing with the `atexit` hook that frees the FLS slot, which would cause us to call `FlsSetValue` on a freed key,
124        // or calling `atexit` during process shutdown, which would cause a deadlock.
125        let (_guard, at_exit_called) = EnableGuard::new();
126
127        if at_exit_called {
128            // We are exiting and don't want to race with the `atexit` hook, so we won't be able to run the destructors for this thread.
129            return;
130        }
131
132        let current_key = KEY.load(Ordering::Acquire);
133
134        // If we already allocated a key, we only need to set it to a non-null value so that the destructors hook is run for this thread.
135        let key = if current_key != FLS_OUT_OF_INDEXES {
136            current_key
137        } else {
138            // Otherwise, we try to allocate a key.
139            let new_key = unsafe { create(Some(cleanup)) };
140
141            // Now we need to set this key to be used by everyone else.
142            // If we won the race, our key is the right one and we can set it to non-null value.
143            // If we lost, we'll use the winning key and free our losing key.
144            match KEY.compare_exchange(current_key, new_key, Ordering::Release, Ordering::Acquire) {
145                Ok(_) => {
146                    // If the current DLL is unloaded, the registered `cleanup` hook will not be available later during thread exit,
147                    // triggering a `STATUS_ACCESS_VIOLATION`. To avoid this, we use the `atexit` hook, which is called during DLL unload
148                    // to manually free the FLS slot, triggering the destructors.
149                    //
150                    // However, calling `atexit` during process exit can cause a deadlock.
151                    // In a Rust binary, `enable` is called during the main thread startup and before any user code,
152                    // and we checked using `at_exit_called` that we aren't in process shutdown.
153                    //
154                    // In a Rust DLL, dynamic unloading can only happen safely when no other threads are
155                    // concurrently executing Rust code, so if we are here we cannot be unloading yet.
156                    //
157                    // If a main non-Rust binary is exiting, it must not be trigger the `enable` guard
158                    // for the first time during process shutdown.
159                    //
160                    // Miri has no DLL unloading so we can skip this step here.
161                    if !cfg!(miri) {
162                        if cleanup_is_unloadable() {
163                            let res = unsafe { c::atexit(free_fls_key_at_exit) };
164                            if res != 0 {
165                                rtabort!("failed to register fls atexit hook");
166                            }
167                        }
168                    }
169
170                    new_key
171                }
172                Err(other_key) => {
173                    unsafe { c::FlsFree(new_key) };
174                    other_key
175                }
176            }
177        };
178
179        // Setting the key's value to non-zero will cause the dtor callback to be called when the thread exits.
180        unsafe { set(key, ptr::without_provenance(1)) };
181    }
182}
183
184/// Checks if `cleanup` is in a different module from the main executable,
185/// using `GetModuleHandleExW(FLAG_FROM_ADDRESS, cleanup) != GetModuleHandleW(ptr::null())`.
186///
187/// If `cleanup` lives in the main executable, its code cannot be unmapped
188/// before process exit, so no unload hook is needed.
189///
190/// If it lives in a DLL, the DLL may be unloaded while the process keeps
191/// running, so the FLS callback must be unregistered before that image is
192/// unmapped.
193///
194/// On failure, return true, which assumes it can be unloaded.
195fn cleanup_is_unloadable() -> bool {
196    // Get a handle to the module of `cleanup`.
197    let cleanup_module = {
198        let mut handle: c::HMODULE = ptr::null_mut();
199
200        let res = unsafe {
201            c::GetModuleHandleExW(
202                c::GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
203                    | c::GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
204                cleanup as *const () as c::PCWSTR,
205                &mut handle,
206            )
207        };
208
209        if res == c::FALSE || handle.is_null() {
210            return true;
211        }
212
213        handle
214    };
215
216    // Get a handle to the file used to create the calling process (.exe file).
217    let main_exe_module = unsafe { c::GetModuleHandleW(ptr::null()) };
218
219    if main_exe_module.is_null() {
220        return true;
221    }
222
223    cleanup_module != main_exe_module
224}
225
226extern "C" fn free_fls_key_at_exit() {
227    // The main purpose of this hook is to free the FLS slot during DLL unload.
228    // However, this hook will also be called during normal process exit, while other Rust threads are still running,
229    // so we must be careful to avoid races with `enable`.
230    let should_free_key = EnableGuard::start_exit();
231    if !should_free_key {
232        return;
233    }
234
235    let current_key = KEY.swap(c::FLS_OUT_OF_INDEXES, Ordering::AcqRel);
236    if current_key != c::FLS_OUT_OF_INDEXES {
237        // Calling `FlsFree` will cause the OS to call the `cleanup` hook, in the current thread, *for each thread* (or fiber) with a value in this FLS slot.
238        // `cleanup` is safe to run repeatedly: it only drains the current thread's TLS destructor list, and we check that we are not running in a fiber before doing so.
239        // We only call this when no `enable` call is active, so it cannot race with `FlsSetValue` using this key.
240        // Destructors of thread locals in other threads will not run and therefore leak, which is allowed since we are exiting or unloading.
241        unsafe { c::FlsFree(current_key) };
242    }
243}
244
245unsafe extern "system" fn cleanup(_ptr: *const c_void) {
246    // Avoid running the hook if we are in a fiber.
247    // This will cause destructors of thread locals to not run, leaking them.
248    // Thread-local runtime state will not be cleaned.
249    //
250    // We need to verify that we won't run the destructors *before* the thread exits,
251    // but if the fiber that registered the callback is deleted, the thread might still be running other fibers.
252    //
253    // By checking that we are not running in a fiber here, we are guaranteed that the hook is only running during the thread's exit.
254    // See also the `fiber_does_not_trigger_dtor` test.
255    if is_thread_a_fiber() {
256        return;
257    }
258
259    unsafe {
260        #[cfg(target_thread_local)]
261        super::super::destructors::run();
262        #[cfg(not(target_thread_local))]
263        super::super::key::run_dtors();
264    }
265
266    crate::rt::thread_cleanup();
267}