Skip to content

Commit e559e78

Browse files
committed
Only run the destructors hook when not running in a fiber
1 parent 0327bec commit e559e78

5 files changed

Lines changed: 81 additions & 14 deletions

File tree

library/std/src/sys/pal/windows/c/bindings.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2264,6 +2264,7 @@ IPV6_DROP_MEMBERSHIP
22642264
IPV6_MREQ
22652265
IPV6_MULTICAST_LOOP
22662266
IPV6_V6ONLY
2267+
IsThreadAFiber
22672268
LINGER
22682269
listen
22692270
LocalFree

library/std/src/sys/pal/windows/c/windows_sys.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ windows_targets::link!("kernel32.dll" "system" fn GetWindowsDirectoryW(lpbuffer
7272
windows_targets::link!("kernel32.dll" "system" fn InitOnceBeginInitialize(lpinitonce : *mut INIT_ONCE, dwflags : u32, fpending : *mut BOOL, lpcontext : *mut *mut core::ffi::c_void) -> BOOL);
7373
windows_targets::link!("kernel32.dll" "system" fn InitOnceComplete(lpinitonce : *mut INIT_ONCE, dwflags : u32, lpcontext : *const core::ffi::c_void) -> BOOL);
7474
windows_targets::link!("kernel32.dll" "system" fn InitializeProcThreadAttributeList(lpattributelist : LPPROC_THREAD_ATTRIBUTE_LIST, dwattributecount : u32, dwflags : u32, lpsize : *mut usize) -> BOOL);
75+
windows_targets::link!("kernel32.dll" "system" fn IsThreadAFiber() -> BOOL);
7576
windows_targets::link!("kernel32.dll" "system" fn LocalFree(hmem : HLOCAL) -> HLOCAL);
7677
windows_targets::link!("kernel32.dll" "system" fn LockFileEx(hfile : HANDLE, dwflags : LOCK_FILE_FLAGS, dwreserved : u32, nnumberofbytestolocklow : u32, nnumberofbytestolockhigh : u32, lpoverlapped : *mut OVERLAPPED) -> BOOL);
7778
windows_targets::link!("kernel32.dll" "system" fn MoveFileExW(lpexistingfilename : PCWSTR, lpnewfilename : PCWSTR, dwflags : MOVE_FILE_FLAGS) -> BOOL);

library/std/src/sys/thread_local/guard/windows.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ unsafe fn set(key: Key, ptr: *const c_void) {
3636
}
3737
}
3838

39+
fn is_thread_a_fiber() -> bool {
40+
let res = unsafe { c::IsThreadAFiber() };
41+
res == c::TRUE
42+
}
43+
3944
static KEY: AtomicU32 = AtomicU32::new(FLS_OUT_OF_INDEXES);
4045

4146
pub fn enable() {
@@ -63,25 +68,24 @@ pub fn enable() {
6368
}
6469
};
6570

66-
// Setting the key's value to non-zero will cause the dtor callback to be called when the thread or fiber exits.
67-
// We only set the key once per thread, so the destructors are guaranteed to run at most once.
68-
// We still need to verify that we won't run the destructors _before_ the thread exits,
69-
// so we need to check what happens when a fiber terminates (fibers cannot be moved between threads).
70-
//
71-
// According to [Fibers entry in MSDN](https://learn.microsoft.com/en-us/windows/win32/procthread/fibers):
72-
// > If your fiber function returns, the thread running the fiber exits.
73-
// > ..
74-
// > If the currently running fiber calls DeleteFiber, its thread calls ExitThread and terminates.
75-
// > However, if the selected fiber of a thread is deleted by a fiber running in another thread,
76-
// > the thread with the deleted fiber is likely to terminate abnormally because the fiber stack has been freed.
77-
//
78-
// Meaning a fiber's termination results in thread termination or unrecoverable failure,
79-
// so destructors cannot run while the thread is still executing user code.
71+
// Setting the key's value to non-zero will cause the dtor callback to be called when the thread exits.
72+
// We only set the key once per thread, so the destructors are guaranteed to run at most once (fibers cannot be moved between threads).
8073
unsafe { set(key, ptr::without_provenance(1)) };
8174
}
8275
}
8376

8477
unsafe extern "system" fn cleanup(_ptr: *const c_void) {
78+
// Avoid running the hook if we are in a fiber.
79+
//
80+
// We need to verify that we won't run the destructors *before* the thread exits,
81+
// but if the fiber that registered the callback is deleted, other fibers can still be running in the thread.
82+
//
83+
// By checking that we are not running in a fiber here, we are guaranteed that the hook is only running at the thread teardown.
84+
// See also the `fiber_does_not_trigger_dtor` test.
85+
if is_thread_a_fiber() {
86+
return;
87+
}
88+
8589
unsafe {
8690
#[cfg(target_thread_local)]
8791
super::super::destructors::run();

library/std/tests/thread_local/tests.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

2632
struct 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]
411+
fn 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+
FOO.with(|f| {
442+
*f.get() = Some(NotifyOnDrop(signal.take().unwrap()));
443+
});
444+
let _ = ConvertThreadToFiber(ptr::null());
445+
let _ = ConvertFiberToThread();
446+
});
447+
signal.wait();
448+
assert!(signal.is_set());
449+
t.join().unwrap();
450+
}

src/tools/miri/src/shims/windows/foreign_items.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
531531
// Return success (`1`).
532532
this.write_int(1, dest)?;
533533
}
534+
"IsThreadAFiber" => {
535+
let [] = this.check_shim_sig_lenient(abi, sys_conv, link_name, args)?;
536+
537+
// Return FALSE, as this thread is not a fiber.
538+
this.write_int(0, dest)?;
539+
}
534540

535541
// Access to command-line arguments
536542
"GetCommandLineW" => {

0 commit comments

Comments
 (0)