authorbors <bors@rust-lang.org> 2026-06-03 16:26:56 UTC
committerbors <bors@rust-lang.org> 2026-06-03 16:26:56 UTC
log9c963eecaaa5e9ef270e235a8b35f05e33b597ed
tree551617384479b62c6a484c37640c26b19d6ba4c4
parente16420030e6020d6a38db26d3ed5911a8818b550
parent336dc57c296f3d999d28b797aaf0333cf152e75e

Auto merge of #148799 - ohadravid:windows-thread-local-dtors-using-fls, r=ChrisDenton

Switch the destructors implementation for thread locals on Windows to use FLS ## Summary Switch the thread local **destructors** implementation on Windows to use the _Fiber Local Storage_ APIs, which provide native support for setting a callback to be called on thread termination, replacing the current `tls_callback` symbol-based implementation. _Except for some spellchecking, no LLMs were used to produce code / comments / text in this PR._ ## Current Implementation On Windows, in order to support thread locals with destructors, the standard library uses a special `tls_callback` symbol that is used to call the `destructors::run()` hook on thread termination. This has two downsides: 1. It is not well documented, and seems to cause some problems [1] [2] [3]. 2. It disallows some synchronization operations, as mentioned in [`LocalKey`'s documentation](https://doc.rust-lang.org/std/thread/struct.LocalKey.html#synchronization-in-thread-local-destructors). [1]: https://github.com/rust-lang/rust/pull/144234 [2]: https://github.com/rust-lang/rust/issues/145154 [3]: https://github.com/rust-lang/rust/issues/140798 as an example of point 2, this code, which uses `JoinHandle::join` in a thread local Drop impl, will deadlock on stable: <details> <summary>Join-on-Drop Deadlock Example</summary> ```rust struct JoinOnDrop(Option<JoinHandle<()>>); impl Drop for JoinOnDrop { fn drop(&mut self) { self.0.take().unwrap().join().unwrap(); } } thread_local! { static HANDLE: JoinOnDrop = { let thread = std::thread::spawn(|| { println!("Starting..."); // std::thread::sleep(Duration::from_secs(3)); println!("Done"); }); JoinOnDrop(Some(thread)) }; } fn main() { let thread = std::thread::spawn(|| { HANDLE.with(|_| { println!("Some other thread"); }) }); thread.join().unwrap(); println!("Done"); } ``` </details> ## Proposed Change We can use the `Fls{Alloc,Set,Get,Free}` functions (see https://devblogs.microsoft.com/oldnewthing/20191011-00/?p=102989) to implement the dtor callback needed for thread locals that have a Drop implementation. We allocate a single key, and use its destructor callback to run all the registered destructors when a thread is shutting down. With this implementation, the above code sample will not deadlock (but it still might not be a good idea to do this!). ## Safety and Compatibility We use the common `thread_local` + atomic pattern to only set a single FLS key. The destructor callback is only called when that value is non-zero. Destructors will only run at thread exit: we verify that we are not running in a fiber during the destructors callback. **This means that using fibers (which is very rare) will result in thread locals being leaked**, unless the fiber is converted back to a thread using `ConvertFiberToThread` before thread termination. This is not ideal, but should be OK as destructors are not guaranteed to run, but it needs to be documented. It might be possible for the user to use something like the current `tls_callback` to observe an already-freed thread locals, which is something that can also happen in the current implementation. ~Destructors will only run on the correct thread: Fibers cannot be moved between threads.~ Destructors will only run on the correct thread: the hook uses a `#[thread_local]` list, so fiber movement between threads does not change which which thread executes the destructors. Destructors will only run once: even if the hook is called multiple time, the `#[thread_local]` list is cleared after the first run. Users cannot observe different locals because they are using fibers: because we only use an Fls local marker to trigger the destructors callback, we don't change anything about how users interact with "normal" thread locals and fiber locals. ### DLL Unloading It is possible to build a `cdylib` which uses thread locals and unload it dynamically using `FreeLibrary`. This can cause the OS to call into an unmapped cleanup hook, so we use `atexit` to manually free the special FLS key, which will also trigger the cleanup hook for each registered thread. This is safe because similar to thread shutdown, no user code can ran after this point, and only the destructors of the running thread will run. see `tests/run-make/dynamic-loading-cdylib/load_and_unload.rs`. ## Other Notes The implementation is based on the `key::racy` and `guard::apple` code, because we need a `LazyKey`-like racey static and an `enable` function. While TLS slots are [limited to 1088](https://devblogs.microsoft.com/oldnewthing/20170712-00/?p=96585), FLS slots are currently [limited to 4000](https://devblogs.microsoft.com/windows-music-dev/effectively-removing-the-fls-slot-allocation-limit-in-windows-10/) per process. ### Miri Because miri is aware to the thread local implementation, I also implemented these functions and support for them in the interpreter here: https://github.com/rust-lang/miri/compare/master...ohadravid:miri:windows-fls-support?expand=1 I guess that this will need to be merged before this PR (if this is accepted) - let me know and I'll open that PR as well. ### Targets without `target_thread_local` In `*-gnu` Windows targets, the `target_thread_local` feature is unavailable. We could also change the "key" (non-`target_thread_local`) Windows impl at `library\std\src\sys\thread_local\key\windows.rs` to be based on the Fls functions. I can add it to this PR, or as a separate PR, if you think this is preferable. `Cell` in a `#[thread_local]` is used to store the resulting key, like the other implementations. When `target_thread_local` isn't available, we always fetch the atomic and set the FLS key's value.

15 files changed, 875 insertions(+), 113 deletions(-)

library/std/src/sys/pal/windows/c.rs+5-1
......@@ -5,7 +5,7 @@
55#![unstable(issue = "none", feature = "windows_c")]
66#![allow(clippy::style)]
77
8use core::ffi::{CStr, c_uint, c_ulong, c_ushort, c_void};
8use core::ffi::{CStr, c_int, c_uint, c_ulong, c_ushort, c_void};
99use core::ptr;
1010
1111mod windows_sys;
......@@ -241,3 +241,7 @@ cfg_select! {
241241// Only available starting with Windows 8.
242242#[cfg(not(target_vendor = "win7"))]
243243windows_link::link!("ws2_32.dll" "system" fn GetHostNameW(name : PWSTR, namelen : i32) -> i32);
244
245unsafe extern "C" {
246 pub fn atexit(cb: unsafe extern "C" fn()) -> c_int;
247}
library/std/src/sys/pal/windows/c/bindings.txt+7
......@@ -2130,6 +2130,11 @@ FindExSearchNameMatch
21302130FindFirstFileExW
21312131FindNextFileW
21322132FIONBIO
2133FLS_OUT_OF_INDEXES
2134FlsAlloc
2135FlsFree
2136FlsGetValue
2137FlsSetValue
21332138FlushFileBuffers
21342139FORMAT_MESSAGE_ALLOCATE_BUFFER
21352140FORMAT_MESSAGE_ARGUMENT_ARRAY
......@@ -2258,6 +2263,7 @@ IPV6_DROP_MEMBERSHIP
22582263IPV6_MREQ
22592264IPV6_MULTICAST_LOOP
22602265IPV6_V6ONLY
2266IsThreadAFiber
22612267LINGER
22622268listen
22632269LocalFree
......@@ -2318,6 +2324,7 @@ OPEN_ALWAYS
23182324OPEN_EXISTING
23192325OpenProcessToken
23202326OVERLAPPED
2327PFLS_CALLBACK_FUNCTION
23212328PIPE_ACCEPT_REMOTE_CLIENTS
23222329PIPE_ACCESS_DUPLEX
23232330PIPE_ACCESS_INBOUND
library/std/src/sys/pal/windows/c/windows_sys.rs+8
......@@ -27,6 +27,10 @@ windows_link::link!("kernel32.dll" "system" fn ExitProcess(uexitcode : u32) -> !
2727windows_link::link!("kernel32.dll" "system" fn FindClose(hfindfile : HANDLE) -> BOOL);
2828windows_link::link!("kernel32.dll" "system" fn FindFirstFileExW(lpfilename : PCWSTR, finfolevelid : FINDEX_INFO_LEVELS, lpfindfiledata : *mut core::ffi::c_void, fsearchop : FINDEX_SEARCH_OPS, lpsearchfilter : *const core::ffi::c_void, dwadditionalflags : FIND_FIRST_EX_FLAGS) -> HANDLE);
2929windows_link::link!("kernel32.dll" "system" fn FindNextFileW(hfindfile : HANDLE, lpfindfiledata : *mut WIN32_FIND_DATAW) -> BOOL);
30windows_link::link!("kernel32.dll" "system" fn FlsAlloc(lpcallback : PFLS_CALLBACK_FUNCTION) -> u32);
31windows_link::link!("kernel32.dll" "system" fn FlsFree(dwflsindex : u32) -> BOOL);
32windows_link::link!("kernel32.dll" "system" fn FlsGetValue(dwflsindex : u32) -> *mut core::ffi::c_void);
33windows_link::link!("kernel32.dll" "system" fn FlsSetValue(dwflsindex : u32, lpflsdata : *const core::ffi::c_void) -> BOOL);
3034windows_link::link!("kernel32.dll" "system" fn FlushFileBuffers(hfile : HANDLE) -> BOOL);
3135windows_link::link!("kernel32.dll" "system" fn FormatMessageW(dwflags : FORMAT_MESSAGE_OPTIONS, lpsource : *const core::ffi::c_void, dwmessageid : u32, dwlanguageid : u32, lpbuffer : PWSTR, nsize : u32, arguments : *const *const i8) -> u32);
3236windows_link::link!("kernel32.dll" "system" fn FreeEnvironmentStringsW(penv : PCWSTR) -> BOOL);
......@@ -67,6 +71,7 @@ windows_link::link!("kernel32.dll" "system" fn GetWindowsDirectoryW(lpbuffer : P
6771windows_link::link!("kernel32.dll" "system" fn InitOnceBeginInitialize(lpinitonce : *mut INIT_ONCE, dwflags : u32, fpending : *mut BOOL, lpcontext : *mut *mut core::ffi::c_void) -> BOOL);
6872windows_link::link!("kernel32.dll" "system" fn InitOnceComplete(lpinitonce : *mut INIT_ONCE, dwflags : u32, lpcontext : *const core::ffi::c_void) -> BOOL);
6973windows_link::link!("kernel32.dll" "system" fn InitializeProcThreadAttributeList(lpattributelist : LPPROC_THREAD_ATTRIBUTE_LIST, dwattributecount : u32, dwflags : u32, lpsize : *mut usize) -> BOOL);
74windows_link::link!("kernel32.dll" "system" fn IsThreadAFiber() -> BOOL);
7075windows_link::link!("kernel32.dll" "system" fn LocalFree(hmem : HLOCAL) -> HLOCAL);
7176windows_link::link!("kernel32.dll" "system" fn LockFileEx(hfile : HANDLE, dwflags : LOCK_FILE_FLAGS, dwreserved : u32, nnumberofbytestolocklow : u32, nnumberofbytestolockhigh : u32, lpoverlapped : *mut OVERLAPPED) -> BOOL);
7277windows_link::link!("kernel32.dll" "system" fn MoveFileExW(lpexistingfilename : PCWSTR, lpnewfilename : PCWSTR, dwflags : MOVE_FILE_FLAGS) -> BOOL);
......@@ -2667,6 +2672,7 @@ impl Default for FLOATING_SAVE_AREA {
26672672 unsafe { core::mem::zeroed() }
26682673 }
26692674}
2675pub const FLS_OUT_OF_INDEXES: u32 = 4294967295u32;
26702676pub const FORMAT_MESSAGE_ALLOCATE_BUFFER: FORMAT_MESSAGE_OPTIONS = 256u32;
26712677pub const FORMAT_MESSAGE_ARGUMENT_ARRAY: FORMAT_MESSAGE_OPTIONS = 8192u32;
26722678pub const FORMAT_MESSAGE_FROM_HMODULE: FORMAT_MESSAGE_OPTIONS = 2048u32;
......@@ -3037,6 +3043,8 @@ pub struct OVERLAPPED_0_0 {
30373043}
30383044pub type PCSTR = *const u8;
30393045pub type PCWSTR = *const u16;
3046pub type PFLS_CALLBACK_FUNCTION =
3047 Option<unsafe extern "system" fn(lpflsdata: *const core::ffi::c_void)>;
30403048pub type PIO_APC_ROUTINE = Option<
30413049 unsafe extern "system" fn(
30423050 apccontext: *mut core::ffi::c_void,
library/std/src/sys/thread_local/guard/windows.rs+200-85
......@@ -1,103 +1,218 @@
11//! Support for Windows TLS destructors.
22//!
3//! Unfortunately, Windows does not provide a nice API to provide a destructor
4//! for a TLS variable. Thus, the solution here ended up being a little more
5//! obscure, but fear not, the internet has informed me [1][2] that this solution
6//! is not unique (no way I could have thought of it as well!). The key idea is
7//! to insert some hook somewhere to run arbitrary code on thread termination.
8//! With this in place we'll be able to run anything we like, including all
9//! TLS destructors!
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].
105//!
11//! In order to realize this, all TLS destructors are tracked by *us*, not the
12//! Windows runtime. This means that we have a global list of destructors for
6//! All TLS destructors are tracked by *us*, not the Windows runtime.
7//! This means that we have a global list of destructors for
138//! each TLS key or variable that we know about.
149//!
15//! # What's up with CRT$XLB?
16//!
17//! For anything about TLS destructors to work on Windows, we have to be able
18//! to run *something* when a thread exits. To do so, we place a very special
19//! static in a very special location. If this is encoded in just the right
20//! way, the kernel's loader is apparently nice enough to run some function
21//! of ours whenever a thread exits! How nice of the kernel!
22//!
23//! Lots of detailed information can be found in source [1] above, but the
24//! gist of it is that this is leveraging a feature of Microsoft's PE format
25//! (executable format) which is not actually used by any compilers today.
26//! This apparently translates to any callbacks in the ".CRT$XLB" section
27//! being run on certain events.
28//!
29//! So after all that, we use the compiler's `#[link_section]` feature to place
30//! a callback pointer into the magic section so it ends up being called.
31//!
32//! # What's up with this callback?
33//!
34//! The callback specified receives a number of parameters from... someone!
35//! (the kernel? the runtime? I'm not quite sure!) There are a few events that
36//! this gets invoked for, but we're currently only interested on when a
37//! thread or a process "detaches" (exits). The process part happens for the
38//! last thread and the thread part happens for any normal thread.
39//!
40//! # The article mentions weird stuff about "/INCLUDE"?
41//!
42//! It sure does! Specifically we're talking about this quote:
43//!
44//! ```quote
45//! The Microsoft run-time library facilitates this process by defining a
46//! memory image of the TLS Directory and giving it the special name
47//! “__tls_used” (Intel x86 platforms) or “_tls_used” (other platforms). The
48//! linker looks for this memory image and uses the data there to create the
49//! TLS Directory. Other compilers that support TLS and work with the
50//! Microsoft linker must use this same technique.
51//! ```
52//!
53//! Basically what this means is that if we want support for our TLS
54//! destructors/our hook being called then we need to make sure the linker does
55//! not omit this symbol. Otherwise it will omit it and our callback won't be
56//! wired up.
57//!
58//! We don't actually use the `/INCLUDE` linker flag here like the article
59//! mentions because the Rust compiler doesn't propagate linker flags, but
60//! instead we use a shim function which performs a volatile 1-byte load from
61//! the address of the _tls_used symbol to ensure it sticks around.
62//!
63//! [1]: https://www.codeproject.com/Articles/8113/Thread-Local-Storage-The-C-Way
64//! [2]: https://github.com/ChromiumWebApps/chromium/blob/master/base/threading/thread_local_storage_win.cc#L42
10//! [1]: https://devblogs.microsoft.com/oldnewthing/20191011-00/?p=102989
6511
6612use core::ffi::c_void;
13use core::sync::atomic::{AtomicBool, AtomicU32, Ordering, fence};
6714
15use crate::cell::Cell;
6816use crate::ptr;
69use crate::sys::c;
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 }
7027
71unsafe extern "C" {
72 #[link_name = "_tls_used"]
73 static TLS_USED: u8;
28 key_result
7429}
75pub fn enable() {
76 // When destructors are used, we need to add a reference to the _tls_used
77 // symbol provided by the CRT, otherwise the TLS support code will get
78 // GC'd by the linker and our callback won't be called.
79 unsafe { ptr::from_ref(&TLS_USED).read_volatile() };
80 // We also need to reference CALLBACK to make sure it does not get GC'd
81 // by the compiler/LLVM. The callback will end up inside the TLS
82 // callback array pointed to by _TLS_USED through linker shenanigans,
83 // but as far as the compiler is concerned, it looks like the data is
84 // unused, so we need this hack to prevent it from disappearing.
85 unsafe { ptr::from_ref(&CALLBACK).read_volatile() };
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 }
86100}
87101
88#[unsafe(link_section = ".CRT$XLB")]
89#[cfg_attr(miri, used)] // Miri only considers explicitly `#[used]` statics for `lookup_link_section`
90pub static CALLBACK: unsafe extern "system" fn(*mut c_void, u32, *mut c_void) = tls_callback;
102#[cfg(not(miri))]
103impl Drop for EnableGuard {
104 fn drop(&mut self) {
105 ACTIVE_ENABLE_CALLS.fetch_sub(1, Ordering::Relaxed);
106 }
107}
91108
92unsafe extern "system" fn tls_callback(_h: *mut c_void, dw_reason: u32, _pv: *mut c_void) {
93 if dw_reason == c::DLL_THREAD_DETACH || dw_reason == c::DLL_PROCESS_DETACH {
94 unsafe {
95 #[cfg(target_thread_local)]
96 super::super::destructors::run();
97 #[cfg(not(target_thread_local))]
98 super::super::key::run_dtors();
109pub fn enable() {
110 let registered = if cfg!(target_thread_local) {
111 #[thread_local]
112 static REGISTERED: Cell<bool> = Cell::new(false);
113 REGISTERED.replace(true)
114 } else {
115 // `#[thread_local]` is unavailable on windows-gnu (`target_thread_local` is off),
116 // but setting the FLS key's value is about as expensive as `TlsGet`, so we don't bother tracking registration separately.
117 false
118 };
99119
100 crate::rt::thread_cleanup();
120 if !registered {
121 // We are in a critical section where we are trying to register a destructor for the current thread.
122 // 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,
123 // or calling `atexit` during process shutdown, which would cause a deadlock.
124 let (_guard, at_exit_called) = EnableGuard::new();
125
126 if at_exit_called {
127 // 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.
128 return;
101129 }
130
131 let current_key = KEY.load(Ordering::Acquire);
132
133 // 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.
134 let key = if current_key != FLS_OUT_OF_INDEXES {
135 current_key
136 } else {
137 // Otherwise, we try to allocate a key.
138 let new_key = unsafe { create(Some(cleanup)) };
139
140 // Now we need to set this key to be used by everyone else.
141 // If we won the race, our key is the right one and we can set it to non-null value.
142 // If we lost, we'll use the winning key and free our losing key.
143 match KEY.compare_exchange(current_key, new_key, Ordering::Release, Ordering::Acquire) {
144 Ok(_) => {
145 // If the current DLL is unloaded, the registered `cleanup` hook will not be available later during thread exit,
146 // triggering a `STATUS_ACCESS_VIOLATION`. To avoid this, we use the `atexit` hook, which is called during DLL unload
147 // to manually free the FLS slot, triggering the destructors.
148 //
149 // However, calling `atexit` during process exit can cause a deadlock.
150 // In a Rust binary, `enable` is called during the main thread startup and before any user code,
151 // and we checked using `at_exit_called` that we aren't in process shutdown.
152 //
153 // In a Rust DLL, dynamic unloading can only happen safely when no other threads are
154 // concurrently executing Rust code, so if we are here we cannot be unloading yet.
155 //
156 // If a main non-Rust binary is exiting, it must not be trigger the `enable` guard
157 // for the first time during process shutdown.
158 let res = unsafe { c::atexit(free_fls_key_at_exit) };
159 if res != 0 {
160 rtabort!("failed to register fls atexit hook");
161 }
162
163 new_key
164 }
165 Err(other_key) => {
166 unsafe { c::FlsFree(new_key) };
167 other_key
168 }
169 }
170 };
171
172 // Setting the key's value to non-zero will cause the dtor callback to be called when the thread exits.
173 unsafe { set(key, ptr::without_provenance(1)) };
174 }
175}
176
177extern "C" fn free_fls_key_at_exit() {
178 // The main purpose of this hook is to free the FLS slot during DLL unload.
179 // However, this hook will also be called during normal process exit, while other Rust threads are still running,
180 // so we must be careful to avoid races with `enable`.
181 let should_free_key = EnableGuard::start_exit();
182 if !should_free_key {
183 return;
184 }
185
186 let current_key = KEY.swap(c::FLS_OUT_OF_INDEXES, Ordering::AcqRel);
187 if current_key != c::FLS_OUT_OF_INDEXES {
188 // 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.
189 // `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.
190 // We only call this when no `enable` call is active, so it cannot race with `FlsSetValue` using this key.
191 // Destructors of thread locals in other threads will not run and therefore leak, which is allowed since we are exiting or unloading.
192 unsafe { c::FlsFree(current_key) };
193 }
194}
195
196unsafe extern "system" fn cleanup(_ptr: *const c_void) {
197 // Avoid running the hook if we are in a fiber.
198 // This will cause destructors of thread locals to not run, leaking them.
199 // Thread-local runtime state will not be cleaned.
200 //
201 // We need to verify that we won't run the destructors *before* the thread exits,
202 // but if the fiber that registered the callback is deleted, the thread might still be running other fibers.
203 //
204 // 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.
205 // See also the `fiber_does_not_trigger_dtor` test.
206 if is_thread_a_fiber() {
207 return;
102208 }
209
210 unsafe {
211 #[cfg(target_thread_local)]
212 super::super::destructors::run();
213 #[cfg(not(target_thread_local))]
214 super::super::key::run_dtors();
215 }
216
217 crate::rt::thread_cleanup();
103218}
library/std/src/thread/local.rs+9-8
......@@ -98,17 +98,18 @@ use crate::fmt;
9898/// run on the thread that causes the process to exit. This is because the
9999/// other threads may be forcibly terminated.
100100///
101/// ## Synchronization in thread-local destructors
101/// If a thread is [converted into a fiber], destructors will not be run unless
102/// the fiber is [converted back into a thread] before the underlying thread exits.
102103///
103/// On Windows, synchronization operations (such as [`JoinHandle::join`]) in
104/// thread local destructors are prone to deadlocks and so should be avoided.
105/// This is because the [loader lock] is held while a destructor is run. The
106/// lock is acquired whenever a thread starts or exits or when a DLL is loaded
107/// or unloaded. Therefore these events are blocked for as long as a thread
108/// local destructor is running.
104/// If a process loads a Rust `cdylib`, it must not cause the Rust TLS destructor support
105// to be initialized for the first time during process shutdown.
109106///
107/// When dynamically unloading a Rust `cdylib`, threads with pending TLS destructors
108/// may run during the unload or may be leaked.
109///
110/// [converted into a fiber]: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-convertthreadtofiber
111/// [converted back into a thread]: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-convertfibertothread
110112/// [loader lock]: https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices
111/// [`JoinHandle::join`]: crate::thread::JoinHandle::join
112113/// [`with`]: LocalKey::with
113114#[cfg_attr(not(test), rustc_diagnostic_item = "LocalKey")]
114115#[stable(feature = "rust1", since = "1.0.0")]
library/std/tests/thread_local/tests.rs+55
......@@ -21,6 +21,12 @@ impl Signal {
2121 set = cvar.wait(set).unwrap();
2222 }
2323 }
24
25 fn is_set(&self) -> bool {
26 let (set, _cvar) = &*self.0;
27 let set = set.lock().unwrap();
28 *set
29 }
2430}
2531
2632struct NotifyOnDrop(Signal);
......@@ -97,6 +103,7 @@ fn smoke_dtor() {
97103 });
98104 });
99105 signal.wait();
106 assert!(signal.is_set());
100107 t.join().unwrap();
101108 }
102109}
......@@ -393,3 +400,51 @@ fn thread_current_in_dtor() {
393400 let name = name.as_ref().unwrap();
394401 assert_eq!(name, "test");
395402}
403
404// Test that if a thread uses fibers while the dtors callback is running,
405// we do NOT trigger dtors.
406// This prevents a UAF if a fiber is the first to use Tls and is deleted,
407// like in the example here:
408// https://github.com/rust-lang/rust/pull/148799#issuecomment-3731806901
409#[cfg(target_os = "windows")]
410#[test]
411fn fiber_does_not_trigger_dtor() {
412 use core::ffi::c_void;
413 use std::ptr;
414
415 unsafe extern "system" {
416 fn ConvertFiberToThread() -> i32;
417 fn ConvertThreadToFiber(lpParameter: *const c_void) -> *mut c_void;
418 }
419
420 thread_local!(static FOO: UnsafeCell<Option<NotifyOnDrop>> = UnsafeCell::new(None));
421 let signal = Signal::default();
422
423 let signal2 = signal.clone();
424 let t = thread::spawn(move || unsafe {
425 let mut signal = Some(signal2);
426 FOO.with(|f| {
427 *f.get() = Some(NotifyOnDrop(signal.take().unwrap()));
428 });
429 let _main = ConvertThreadToFiber(ptr::null());
430 // A user can then switch to a new fiber and delete the main fiber.
431 // If destructors are triggered when the fiber is deleted,
432 // the new fiber will be able to observe already-destructed values.
433 });
434 t.join().unwrap();
435 assert!(!signal.is_set());
436
437 // As long as we stop using fibers before thread teardown, everything works as expected.
438 let signal2 = signal.clone();
439 let t = thread::spawn(move || unsafe {
440 let mut signal = Some(signal2);
441 let _ = ConvertThreadToFiber(ptr::null());
442 FOO.with(|f| {
443 *f.get() = Some(NotifyOnDrop(signal.take().unwrap()));
444 });
445 let _ = ConvertFiberToThread();
446 });
447 signal.wait();
448 assert!(signal.is_set());
449 t.join().unwrap();
450}
src/tools/miri/src/shims/tls.rs+123-14
......@@ -1,7 +1,7 @@
11//! Implement thread-local storage.
22
3use std::collections::BTreeMap;
43use std::collections::btree_map::Entry as BTreeEntry;
4use std::collections::{BTreeMap, VecDeque};
55use std::task::Poll;
66
77use rustc_abi::{ExternAbi, HasDataLayout, Size};
......@@ -17,18 +17,27 @@ pub type TlsKey = u128;
1717pub struct TlsEntry<'tcx> {
1818 /// The data for this key. None is used to represent NULL.
1919 /// (We normalize this early to avoid having to do a NULL-ptr-test each time we access the data.)
20 data: BTreeMap<ThreadId, Scalar>,
21 dtor: Option<(ty::Instance<'tcx>, Span)>,
20 pub data: BTreeMap<ThreadId, Scalar>,
21 pub dtor: Option<(ty::Instance<'tcx>, Span)>,
2222}
2323
2424#[derive(Default, Debug)]
25struct RunningDtorState {
25struct RunningPthreadDtorState {
2626 /// The last TlsKey used to retrieve a TLS destructor. `None` means that we
2727 /// have not tried to retrieve a TLS destructor yet or that we already tried
2828 /// all keys.
2929 last_key: Option<TlsKey>,
3030}
3131
32#[derive(Default, Debug)]
33struct RunningWindowsDtorState {
34 /// The last TlsKey for which a TLS destructor ran. `None` means that we
35 /// have not run a TLS destructor yet. This is used to clear the TLS value after the dtor returned.
36 last_key: Option<TlsKey>,
37 // Keys that have destructors that we still need to run.
38 remaining_keys: VecDeque<TlsKey>,
39}
40
3241#[derive(Debug)]
3342pub struct TlsData<'tcx> {
3443 /// The Key to use for the next thread-local allocation.
......@@ -72,11 +81,11 @@ impl<'tcx> TlsData<'tcx> {
7281 interp_ok(new_key)
7382 }
7483
75 pub fn delete_tls_key(&mut self, key: TlsKey) -> InterpResult<'tcx> {
84 pub fn delete_tls_key(&mut self, key: TlsKey) -> InterpResult<'tcx, TlsEntry<'tcx>> {
7685 match self.keys.remove(&key) {
77 Some(_) => {
86 Some(entry) => {
7887 trace!("TLS key {} removed", key);
79 interp_ok(())
88 interp_ok(entry)
8089 }
8190 None => throw_ub_format!("removing a nonexistent TLS key: {}", key),
8291 }
......@@ -220,10 +229,12 @@ enum TlsDtorsStatePriv<'tcx> {
220229 #[default]
221230 Init,
222231 MacOsDtors,
223 PthreadDtors(RunningDtorState),
224 /// For Windows Dtors, we store the list of functions that we still have to call.
225 /// These are functions from the magic `.CRT$XLB` linker section.
226 WindowsDtors(Vec<(ImmTy<'tcx>, Span)>),
232 PthreadDtors(RunningPthreadDtorState),
233 /// For Windows, we support two different ways dtors can be registered.
234 /// 1. Functions that are registered via the `FlsAlloc` function, which are invoked one by one.
235 /// 2. Functions from the magic `.CRT$XLB` linker section.
236 /// We store these as a list of functions that we still have to call.
237 WindowsDtors(RunningWindowsDtorState, Vec<(ImmTy<'tcx>, Span)>),
227238 Done,
228239}
229240
......@@ -250,8 +261,13 @@ impl<'tcx> TlsDtorsState<'tcx> {
250261 Os::Windows => {
251262 // Determine which destructors to run.
252263 let dtors = this.lookup_windows_tls_dtors()?;
264
265 // Fetch fls keys that have destructors. Keys registered during thread exit will not have their destructor called.
266 // See also: `schedule_next_windows_fls_dtor`.
267 let fls_keys_with_dtors = this.lookup_windows_fls_keys_with_dtors()?;
268
253269 // And move to the next state, that runs them.
254 break 'new_state WindowsDtors(dtors);
270 break 'new_state WindowsDtors(RunningWindowsDtorState { last_key: None, remaining_keys: fls_keys_with_dtors }, dtors);
255271 }
256272 _ => {
257273 // No TLS dtor support.
......@@ -273,7 +289,13 @@ impl<'tcx> TlsDtorsState<'tcx> {
273289 Poll::Ready(()) => break 'new_state Done,
274290 }
275291 }
276 WindowsDtors(dtors) => {
292 WindowsDtors(state, dtors) => {
293 // Fls destructors are scheduled before the tls callback.
294 match this.schedule_next_windows_fls_dtor(state)? {
295 Poll::Pending => return interp_ok(Poll::Pending), // just keep going
296 Poll::Ready(()) => {}
297 }
298
277299 if let Some((dtor, span)) = dtors.pop() {
278300 this.schedule_windows_tls_dtor(dtor, span)?;
279301 return interp_ok(Poll::Pending); // we stay in this state (but `dtors` got shorter)
......@@ -306,6 +328,22 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
306328 interp_ok(this.lookup_link_section(|section| section == ".CRT$XLB")?)
307329 }
308330
331 /// Lookup all the FLS keys (which are stored as TLS keys) that have a destructor.
332 /// See also: `schedule_next_windows_fls_dtor`.
333 fn lookup_windows_fls_keys_with_dtors(&mut self) -> InterpResult<'tcx, VecDeque<TlsKey>> {
334 let this = self.eval_context_mut();
335
336 interp_ok(
337 this.machine
338 .tls
339 .keys
340 .iter()
341 .filter(|(_, data)| data.dtor.is_some())
342 .map(|(key, _)| *key)
343 .collect(),
344 )
345 }
346
309347 fn schedule_windows_tls_dtor(&mut self, dtor: ImmTy<'tcx>, span: Span) -> InterpResult<'tcx> {
310348 let this = self.eval_context_mut();
311349
......@@ -360,7 +398,7 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
360398 /// a destructor to schedule, and `false` otherwise.
361399 fn schedule_next_pthread_tls_dtor(
362400 &mut self,
363 state: &mut RunningDtorState,
401 state: &mut RunningPthreadDtorState,
364402 ) -> InterpResult<'tcx, Poll<()>> {
365403 let this = self.eval_context_mut();
366404 let active_thread = this.active_thread();
......@@ -392,4 +430,75 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
392430
393431 interp_ok(Poll::Ready(()))
394432 }
433
434 /// Schedule a Windows FLS destructor, if one is found.
435 fn schedule_next_windows_fls_dtor(
436 &mut self,
437 state: &mut RunningWindowsDtorState,
438 ) -> InterpResult<'tcx, Poll<()>> {
439 let this = self.eval_context_mut();
440 let active_thread = this.active_thread();
441
442 // According to [PflsCallbackFunction's docs],
443 // > If the FLS slot is in use, `FlsCallback`` is called on .. thread exit ..
444 // However, the exact order and semantics are not defined.
445 // We use the following implementation, which matches both observed behavior and
446 // Wine's implementation of the same logic in the [`RtlProcessFlsData`] function.
447 // 1. Fetch all the keys once (in `lookup_windows_fls_keys_with_dtors`).
448 // 2. Go over them one by one, in order, skipping keys without dtors.
449 // 3. Fetch the value associated with the key.
450 // 4. If it is non-zero, call the registered dtor.
451 // 5. After the dtor is called, clear the key's value, setting it to zero.
452 // New keys registered during thread exit are ignored, but values set before the dtor is scheduled are visible.
453 // Keys without dtors will not be set to zero.
454 // [PflsCallbackFunction's docs]: https://learn.microsoft.com/en-us/windows/win32/api/winnt/nc-winnt-pfls_callback_function
455 // [`RtlProcessFlsData`]: https://github.com/wine-mirror/wine/blob/wine-11.0/dlls/ntdll/thread.c#L679
456
457 // We are done running the previous key's destructor, so set it's value to zero.
458 if let Some(last_key) = state.last_key.take() {
459 if let Some(TlsEntry { data, .. }) = this.machine.tls.keys.get_mut(&last_key) {
460 data.remove(&active_thread);
461 };
462 }
463
464 while let Some(key) = state.remaining_keys.pop_front() {
465 // Fetch dtor for this `key`.
466 // If the key doesn't have a dtor or does not exist any more, move on to the next key.
467 let (data, dtor) = match this.machine.tls.keys.get(&key) {
468 Some(TlsEntry { data, dtor: Some(dtor) }) => (data, dtor),
469 _ => continue,
470 };
471
472 let (instance, span) = dtor.to_owned();
473
474 // If the key has no value in this thread, move on to the next key.
475 let ptr = match data.get(&active_thread) {
476 Some(data_scalar) => *data_scalar,
477 None => continue,
478 };
479
480 assert!(
481 ptr.to_target_usize(this).unwrap() != 0,
482 "TLS key's value can't be null (should be absent instead)"
483 );
484
485 trace!("Running TLS dtor {:?} on {:?} at {:?}", instance, ptr, active_thread);
486
487 // We'll clear this key's value next time we are called.
488 state.last_key = Some(key);
489
490 this.call_thread_root_function(
491 instance,
492 ExternAbi::System { unwind: false },
493 &[ImmTy::from_scalar(ptr, this.machine.layouts.mut_raw_ptr)],
494 None,
495 span,
496 )?;
497
498 return interp_ok(Poll::Pending);
499 }
500
501 // We are done scheduling all the keys.
502 interp_ok(Poll::Ready(()))
503 }
395504}
src/tools/miri/src/shims/windows/foreign_items.rs+95
......@@ -691,6 +691,89 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
691691 this.write_int(1, dest)?;
692692 }
693693
694 // Fiber-local storage - similar to TLS but supports destructors.
695 "FlsAlloc" => {
696 // Create key and return it.
697 let [dtor] = this.check_shim_sig(
698 shim_sig!(extern "system" fn(winapi::PFLS_CALLBACK_FUNCTION) -> u32),
699 link_name,
700 abi,
701 args,
702 )?;
703 let dtor = this.read_pointer(dtor)?;
704
705 // Extract the function type out of the signature (that seems easier than constructing it ourselves).
706 let dtor = if !this.ptr_is_null(dtor)? {
707 Some((
708 this.get_ptr_fn(dtor)?.as_instance()?,
709 this.machine.current_user_relevant_span(),
710 ))
711 } else {
712 None
713 };
714
715 let key = this.machine.tls.create_tls_key(dtor, dest.layout.size)?;
716 this.write_scalar(Scalar::from_uint(key, dest.layout.size), dest)?;
717 }
718 "FlsGetValue" => {
719 let [key] = this.check_shim_sig(
720 shim_sig!(extern "system" fn(u32) -> *mut _),
721 link_name,
722 abi,
723 args,
724 )?;
725 let key = u128::from(this.read_scalar(key)?.to_u32()?);
726 let active_thread = this.active_thread();
727 let ptr = this.machine.tls.load_tls(key, active_thread, this)?;
728 this.write_scalar(ptr, dest)?;
729 }
730 "FlsSetValue" => {
731 let [key, new_ptr] = this.check_shim_sig(
732 shim_sig!(extern "system" fn(u32, *mut _) -> winapi::BOOL),
733 link_name,
734 abi,
735 args,
736 )?;
737 let key = u128::from(this.read_scalar(key)?.to_u32()?);
738 let active_thread = this.active_thread();
739 let new_data = this.read_scalar(new_ptr)?;
740 this.machine.tls.store_tls(key, active_thread, new_data, &*this.tcx)?;
741
742 // Return success (`1`).
743 this.write_int(1, dest)?;
744 }
745 "FlsFree" => {
746 let [key] = this.check_shim_sig(
747 shim_sig!(extern "system" fn(u32) -> winapi::BOOL),
748 link_name,
749 abi,
750 args,
751 )?;
752 let key = u128::from(this.read_scalar(key)?.to_u32()?);
753 let tls_entry = this.machine.tls.delete_tls_key(key)?;
754
755 // FIXME: We should run the destructor here *for all threads*. But that's non-trivial and std doesn't need it so we bail out with an "unsupported" error.
756 if !tls_entry.data.is_empty() && tls_entry.dtor.is_some() {
757 throw_unsup_format!(
758 "calling `FlsFree` on a key with an associated dtor is not supported"
759 );
760 }
761
762 // Return success (`1`).
763 this.write_int(1, dest)?;
764 }
765 "IsThreadAFiber" => {
766 let [] = this.check_shim_sig(
767 shim_sig!(extern "system" fn() -> winapi::BOOL),
768 link_name,
769 abi,
770 args,
771 )?;
772
773 // Return FALSE, as Miri does not support fibers.
774 this.write_int(0, dest)?;
775 }
776
694777 // Access to command-line arguments
695778 "GetCommandLineW" => {
696779 // FIXME: This does not have a direct test (#3179).
......@@ -1317,6 +1400,18 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
13171400 // FIXME: this should return a nonzero value if this call does result in switching to another thread.
13181401 this.write_null(dest)?;
13191402 }
1403 "atexit" if this.frame_in_std() => {
1404 let [_value] = this.check_shim_sig(
1405 shim_sig!(extern "C" fn(*const _) -> winapi::c_int),
1406 link_name,
1407 abi,
1408 args,
1409 )?;
1410
1411 // We do not support registering atexit handlers, which is used by the thread-local destructor implementation in std.
1412 // But we also do not support manually unloading DLLs, so this has no visible effect.
1413 this.write_int(0, dest)?;
1414 }
13201415
13211416 _ => return interp_ok(EmulateItemResult::NotSupported),
13221417 }
src/tools/miri/tests/pass/tls/windows-tls.rs+192-5
......@@ -1,18 +1,205 @@
11//@only-target: windows # this directly tests windows-only functions
22
33use std::ffi::c_void;
4use std::ptr;
4use std::{ptr, thread};
5
6pub type BOOL = i32;
7pub const FALSE: BOOL = 0i32;
8pub const TRUE: BOOL = 1i32;
59
610extern "system" {
711 fn TlsAlloc() -> u32;
8 fn TlsSetValue(key: u32, val: *mut c_void) -> i32;
12 fn TlsSetValue(key: u32, val: *mut c_void) -> BOOL;
913 fn TlsGetValue(key: u32) -> *mut c_void;
10 fn TlsFree(key: u32) -> i32;
14 fn TlsFree(key: u32) -> BOOL;
15
16 fn FlsAlloc(lpcallback: Option<unsafe extern "system" fn(lpflsdata: *mut c_void)>) -> u32;
17 fn FlsSetValue(key: u32, val: *mut c_void) -> BOOL;
18 fn FlsGetValue(key: u32) -> *mut c_void;
19 fn FlsFree(key: u32) -> BOOL;
20
21 fn IsThreadAFiber() -> BOOL;
22}
23
24extern "system" fn dtor_unreachable(_val: *mut c_void) {
25 unreachable!()
26}
27
28fn fls_0_zero_value_does_not_run() {
29 let key = unsafe { FlsAlloc(Some(dtor_unreachable)) };
30
31 assert_eq!(unsafe { FlsSetValue(key, ptr::without_provenance_mut(1)) }, TRUE);
32 assert_eq!(unsafe { FlsGetValue(key).addr() }, 1);
33 assert_eq!(unsafe { FlsSetValue(key, ptr::without_provenance_mut(0)) }, TRUE);
34}
35
36fn fls_1_dtor_simple() {
37 extern "system" fn dtor(val: *mut c_void) {
38 assert!(!val.is_null());
39 println!("fls_1_dtor_simple");
40
41 // Keys are freed in-order. Without a dtor, the early key's value is not zeroed out.
42 let early_key = val as u32;
43 assert_eq!(unsafe { FlsGetValue(early_key).addr() }, 1);
44 }
45
46 let early_key = unsafe { FlsAlloc(None) };
47 let later_key = unsafe { FlsAlloc(Some(dtor)) };
48
49 assert_eq!(unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(1)) }, TRUE);
50 assert_eq!(unsafe { FlsGetValue(later_key).addr() }, 1);
51
52 assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(1)) }, TRUE);
53 // Will be used in the dtor to check early_key's value.
54 assert_eq!(
55 unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(early_key as usize)) },
56 TRUE
57 );
58}
59
60fn fls_2_dtor_update_value_ignored() {
61 extern "system" fn early_dtor(val: *mut c_void) {
62 assert!(!val.is_null());
63 println!("fls_2.1_early_dtor");
64 }
65
66 extern "system" fn later_dtor(val: *mut c_void) {
67 println!("fls_2.2_later_dtor");
68
69 // Updating a different fls slot's value doesn't cause their dtor to run, if it already did.
70 let early_key = val as u32;
71
72 // After the early key's dtor run, the key's value is zeroed out.
73 assert_eq!(unsafe { FlsGetValue(early_key).addr() }, 0);
74
75 assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(1)) }, TRUE);
76
77 // Registering new fls slots doesn't cause their dtor to run.
78 let a_new_key_in_dtor = unsafe { FlsAlloc(Some(dtor_unreachable)) };
79 assert_eq!(unsafe { FlsSetValue(a_new_key_in_dtor, ptr::without_provenance_mut(1)) }, TRUE);
80 }
81
82 let early_key = unsafe { FlsAlloc(Some(early_dtor)) };
83 let later_key = unsafe { FlsAlloc(Some(later_dtor)) };
84 assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(1)) }, TRUE);
85 assert_eq!(
86 unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(early_key as usize)) },
87 TRUE
88 );
89}
90
91fn fls_3_dtor_update_value_skipped_ignored() {
92 extern "system" fn later_dtor(val: *mut c_void) {
93 println!("fls_3_later_dtor");
94
95 // Updating a different fls slot's value doesn't cause their dtor to run, if it was already skipped.
96 let early_key = val as u32;
97 assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(1)) }, TRUE);
98 }
99
100 let early_key = unsafe { FlsAlloc(Some(dtor_unreachable)) };
101 let later_key = unsafe { FlsAlloc(Some(later_dtor)) };
102 assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(0)) }, TRUE);
103 assert_eq!(
104 unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(early_key as usize)) },
105 TRUE
106 );
107}
108
109fn fls_4_dtor_update_value_used() {
110 extern "system" fn early_dtor(val: *mut c_void) {
111 println!("fls_4.1_early_dtor");
112
113 // Updating a different fls slot's value affect their dtor, if it hasn't yet run.
114 let later_key = val as u32;
115 assert_eq!(unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(1)) }, TRUE);
116 }
117
118 extern "system" fn later_dtor(_val: *mut c_void) {
119 println!("fls_4.2_later_dtor");
120 }
121
122 let early_key = unsafe { FlsAlloc(Some(early_dtor)) };
123 let later_key = unsafe { FlsAlloc(Some(later_dtor)) };
124 assert_eq!(
125 unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(later_key as usize)) },
126 TRUE
127 );
128
129 // Setting to zero explicitly, dtor won't run unless `early_dtor` changes this value.
130 assert_eq!(unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(0)) }, TRUE);
131}
132
133fn fls_5_dtor_value() {
134 extern "system" fn dtor(val: *mut c_void) {
135 assert!(!val.is_null());
136 println!("fls_5_dtor_value");
137
138 // When the key's dtor run, the key's value equals the destructor argument.
139 let key = val as u32;
140 assert_eq!(unsafe { FlsGetValue(key) }, val);
141 }
142
143 let key = unsafe { FlsAlloc(Some(dtor)) };
144
145 assert_eq!(unsafe { FlsSetValue(key, ptr::without_provenance_mut(key as usize)) }, TRUE);
146}
147
148fn fls_6_no_dtor_does_not_zero_value() {
149 extern "system" fn later_dtor(val: *mut c_void) {
150 println!("fls_6_later_dtor");
151
152 // If an earlier fls slot doesn't have a dtor, the key's value is _not_ zeroed out.
153 let early_key = val as u32;
154 assert_ne!(unsafe { FlsGetValue(early_key).addr() }, 0);
155 }
156
157 let early_key = unsafe { FlsAlloc(None) };
158 let later_key = unsafe { FlsAlloc(Some(later_dtor)) };
159 assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(1)) }, TRUE);
160 assert_eq!(
161 unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(early_key as usize)) },
162 TRUE
163 );
164}
165
166fn fls_7_free() {
167 // This dtor is not expected to be called.
168 extern "system" fn dtor_for_freed_key(_val: *mut c_void) {
169 println!("dtor_for_freed_key");
170 }
171 let key_with_dtor = unsafe { FlsAlloc(Some(dtor_for_freed_key)) };
172 assert_eq!(unsafe { FlsSetValue(key_with_dtor, ptr::without_provenance_mut(0)) }, TRUE);
173 assert_eq!(unsafe { FlsFree(key_with_dtor) }, TRUE);
174
175 let key_without_dtor = unsafe { FlsAlloc(None) };
176 assert_eq!(unsafe { FlsSetValue(key_without_dtor, ptr::without_provenance_mut(1)) }, TRUE);
177 assert_eq!(unsafe { FlsFree(key_without_dtor) }, TRUE);
178}
179
180fn fls() {
181 assert_eq!(unsafe { IsThreadAFiber() }, FALSE);
182
183 fls_0_zero_value_does_not_run();
184 fls_1_dtor_simple();
185 fls_2_dtor_update_value_ignored();
186 fls_3_dtor_update_value_skipped_ignored();
187 fls_4_dtor_update_value_used();
188 fls_5_dtor_value();
189 fls_6_no_dtor_does_not_zero_value();
190 fls_7_free();
11191}
12192
13193fn main() {
14194 let key = unsafe { TlsAlloc() };
15 assert!(unsafe { TlsSetValue(key, ptr::without_provenance_mut(1)) != 0 });
195 assert_eq!(unsafe { TlsSetValue(key, ptr::without_provenance_mut(1)) }, TRUE);
16196 assert_eq!(unsafe { TlsGetValue(key).addr() }, 1);
17 assert!(unsafe { TlsFree(key) != 0 });
197 assert_eq!(unsafe { TlsFree(key) }, TRUE);
198
199 thread::spawn(|| {
200 fls();
201 println!("exiting thread");
202 })
203 .join()
204 .unwrap();
18205}
src/tools/miri/tests/pass/tls/windows-tls.stdout created+9
......@@ -0,0 +1,9 @@
1exiting thread
2fls_1_dtor_simple
3fls_2.1_early_dtor
4fls_2.2_later_dtor
5fls_3_later_dtor
6fls_4.1_early_dtor
7fls_4.2_later_dtor
8fls_5_dtor_value
9fls_6_later_dtor
tests/run-make/dynamic-loading-cdylib/foo.rs created+23
......@@ -0,0 +1,23 @@
1#![crate_type = "cdylib"]
2
3#[no_mangle]
4pub extern "C" fn extern_fn_1(a: u32, b: u32) -> u32 {
5 println!("extern_fn_1");
6 a + b
7}
8
9struct NotifyOnDrop;
10
11impl Drop for NotifyOnDrop {
12 fn drop(&mut self) {
13 println!("drop");
14 }
15}
16
17#[no_mangle]
18pub extern "C" fn extern_fn_2(a: u32, b: u32) -> u32 {
19 thread_local!(static FOO: NotifyOnDrop = NotifyOnDrop);
20 FOO.with(|_foo| {});
21 println!("extern_fn_2");
22 a * b
23}
tests/run-make/dynamic-loading-cdylib/load_and_unload.rs created+106
......@@ -0,0 +1,106 @@
1/// Verify that we can load a cdylib, call functions from it, and then unload it.
2
3#[cfg(windows)]
4mod libloading {
5 type BOOL = i32;
6 type DWORD = u32;
7 type HANDLE = isize;
8 pub type HMODULE = isize;
9 type FARPROC = *mut core::ffi::c_void;
10
11 #[link(name = "kernel32")]
12 unsafe extern "system" {
13 fn LoadLibraryExW(filename: *const u16, file: HANDLE, flags: DWORD) -> HMODULE;
14 fn FreeLibrary(module: HMODULE) -> BOOL;
15 fn GetProcAddress(module: HMODULE, procname: *const u8) -> FARPROC;
16 }
17
18 fn wide_null(s: &str) -> Vec<u16> {
19 s.encode_utf16().chain(core::iter::once(0)).collect()
20 }
21
22 fn ansi_null(s: &str) -> Vec<u8> {
23 assert!(!s.as_bytes().contains(&0), "symbol name must not contain interior NUL");
24 s.as_bytes().iter().copied().chain(core::iter::once(0)).collect()
25 }
26
27 pub fn load(lib_name: &str) -> Option<HMODULE> {
28 let filename = wide_null(&format!("{}.dll", lib_name));
29 let handle = unsafe { LoadLibraryExW(filename.as_ptr(), 0, 0) };
30 if handle == 0 { None } else { Some(handle) }
31 }
32
33 pub fn get_symbol(handle: HMODULE, name: &str) -> Option<FARPROC> {
34 let symbol_name = ansi_null(name);
35 let symbol = unsafe { GetProcAddress(handle, symbol_name.as_ptr()) };
36 if symbol.is_null() { None } else { Some(symbol) }
37 }
38
39 pub fn unload(handle: HMODULE) {
40 unsafe {
41 FreeLibrary(handle);
42 }
43 }
44}
45
46#[cfg(unix)]
47mod libloading {
48 use std::ffi::{c_char, c_int, c_void};
49
50 const RTLD_NOW: c_int = 2;
51
52 fn cstr_null(s: &str) -> Vec<c_char> {
53 assert!(!s.as_bytes().contains(&0), "string must not contain interior NUL");
54 s.as_bytes().iter().copied().chain(core::iter::once(0)).map(|b| b as c_char).collect()
55 }
56
57 pub fn load(lib_name: &str) -> Option<*mut c_void> {
58 #[cfg(target_os = "macos")]
59 let filename = cstr_null(&format!("lib{}.dylib", lib_name));
60
61 #[cfg(not(target_os = "macos"))]
62 let filename = cstr_null(&format!("./lib{}.so", lib_name));
63
64 let handle = unsafe { dlopen(filename.as_ptr(), RTLD_NOW) };
65 if handle.is_null() { None } else { Some(handle) }
66 }
67
68 pub fn get_symbol(handle: *mut c_void, name: &str) -> Option<*mut c_void> {
69 let symbol_name = cstr_null(name);
70 let symbol = unsafe { dlsym(handle, symbol_name.as_ptr()) };
71 if symbol.is_null() { None } else { Some(symbol) }
72 }
73
74 pub fn unload(handle: *mut c_void) {
75 unsafe {
76 dlclose(handle);
77 }
78 }
79
80 #[cfg_attr(any(target_os = "linux", target_os = "android"), link(name = "dl"))]
81 extern "C" {
82 fn dlopen(filename: *const c_char, flags: core::ffi::c_int) -> *mut c_void;
83 fn dlclose(handle: *mut c_void) -> core::ffi::c_int;
84 fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
85 }
86}
87
88type ExternFn = unsafe extern "C" fn(u32, u32) -> u32;
89
90fn main() {
91 let foo = libloading::load("foo").expect("Failed to load library");
92 println!("loaded library");
93
94 let extern_fn_1 = libloading::get_symbol(foo, "extern_fn_1").expect("Failed to find symbol");
95 let extern_fn_1: ExternFn = unsafe { std::mem::transmute(extern_fn_1) };
96 let result = unsafe { extern_fn_1(2, 3) };
97 println!("result of extern_fn_1(2, 3): {}", result);
98
99 let extern_fn_2 = libloading::get_symbol(foo, "extern_fn_2").expect("Failed to find symbol");
100 let extern_fn_2: ExternFn = unsafe { std::mem::transmute(extern_fn_2) };
101 let result = unsafe { extern_fn_2(2, 3) };
102 println!("result of extern_fn_2(2, 3): {}", result);
103
104 libloading::unload(foo);
105 println!("unloaded library");
106}
tests/run-make/dynamic-loading-cdylib/output_unix.txt created+7
......@@ -0,0 +1,7 @@
1loaded library
2extern_fn_1
3result of extern_fn_1(2, 3): 5
4extern_fn_2
5result of extern_fn_2(2, 3): 6
6unloaded library
7drop
tests/run-make/dynamic-loading-cdylib/output_windows.txt created+7
......@@ -0,0 +1,7 @@
1loaded library
2extern_fn_1
3result of extern_fn_1(2, 3): 5
4extern_fn_2
5result of extern_fn_2(2, 3): 6
6drop
7unloaded library
tests/run-make/dynamic-loading-cdylib/rmake.rs created+29
......@@ -0,0 +1,29 @@
1// Checks that dynamically loading and unloading cdylib libraries works,
2// both for simple functions and for functions that rely on the Rust runtime
3// (that use thread local storage with destructors).
4//
5// - `foo.rs` is a cdylib.
6// - `load_and_unload.rs` is a binary that loads and unloads the cdylib.
7
8//@ ignore-cross-compile
9
10use run_make_support::{diff, run, rustc};
11
12fn main() {
13 rustc().input("foo.rs").run();
14
15 rustc().crate_type("bin").crate_name("load_and_unload_bin").input("load_and_unload.rs").run();
16
17 let out_raw = run("load_and_unload_bin").stdout_utf8();
18
19 #[cfg(windows)]
20 let output_filename = "output_windows.txt";
21 #[cfg(unix)]
22 let output_filename = "output_unix.txt";
23
24 diff()
25 .expected_file(output_filename)
26 .actual_text("actual", out_raw)
27 .normalize(r#"\r"#, "")
28 .run();
29}