From f1fdd4d197776f441690174642be311464efdd12 Mon Sep 17 00:00:00 2001 From: Orson Peters Date: Fri, 11 Jul 2025 02:03:05 +0200 Subject: [PATCH 1/3] Allow the global alloc one TLS slot with a destructor --- .../src/sys/thread_local/destructors/list.rs | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/library/std/src/sys/thread_local/destructors/list.rs b/library/std/src/sys/thread_local/destructors/list.rs index b9d5214c438d2..6292b4818b5ae 100644 --- a/library/std/src/sys/thread_local/destructors/list.rs +++ b/library/std/src/sys/thread_local/destructors/list.rs @@ -1,20 +1,26 @@ -use crate::cell::RefCell; +use crate::cell::{Cell, RefCell}; use crate::sys::thread_local::guard; +#[thread_local] +static REENTRANT_DTOR: Cell> = Cell::new(None); + #[thread_local] static DTORS: RefCell> = RefCell::new(Vec::new()); pub unsafe fn register(t: *mut u8, dtor: unsafe extern "C" fn(*mut u8)) { - let Ok(mut dtors) = DTORS.try_borrow_mut() else { - // This point can only be reached if the global allocator calls this - // function again. - // FIXME: maybe use the system allocator instead? - rtabort!("the global allocator may not use TLS with destructors"); - }; - - guard::enable(); - - dtors.push((t, dtor)); + // Borrow DTORS can only fail if the global allocator calls this + // function again. + if let Ok(mut dtors) = DTORS.try_borrow_mut() { + guard::enable(); + dtors.push((t, dtor)); + } else if REENTRANT_DTOR.get().is_none() { + guard::enable(); + REENTRANT_DTOR.set(Some((t, dtor))); + } else { + rtabort!( + "the global allocator may only create one thread-local variable with a destructor" + ); + } } /// The [`guard`] module contains platform-specific functions which will run this @@ -41,4 +47,10 @@ pub unsafe fn run() { } } } + + if let Some((t, dtor)) = REENTRANT_DTOR.replace(None) { + unsafe { + dtor(t); + } + } } From eae5cd9c10854fc80b6b95b339b8b0cc5d48bd26 Mon Sep 17 00:00:00 2001 From: Orson Peters Date: Fri, 11 Jul 2025 04:00:12 +0200 Subject: [PATCH 2/3] Add test --- .../threads-sendsync/tls-in-global-alloc.rs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/ui/threads-sendsync/tls-in-global-alloc.rs diff --git a/tests/ui/threads-sendsync/tls-in-global-alloc.rs b/tests/ui/threads-sendsync/tls-in-global-alloc.rs new file mode 100644 index 0000000000000..e5adea58edddf --- /dev/null +++ b/tests/ui/threads-sendsync/tls-in-global-alloc.rs @@ -0,0 +1,48 @@ +//@ run-pass +//@ needs-threads + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +static GLOBAL: AtomicUsize = AtomicUsize::new(0); + +struct Local; + +thread_local! { + static LOCAL: Local = { + GLOBAL.fetch_or(1, Ordering::Relaxed); + Local + }; +} + +impl Drop for Local { + fn drop(&mut self) { + GLOBAL.fetch_or(2, Ordering::Relaxed); + } +} + +#[global_allocator] +static ALLOC: Alloc = Alloc; +struct Alloc; + +unsafe impl GlobalAlloc for Alloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + LOCAL.with(|_local| {}); + unsafe { System.alloc(layout) } + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + LOCAL.with(|_local| {}); + unsafe { System.dealloc(ptr, layout) } + } +} + +fn main() { + std::thread::spawn(|| { + std::hint::black_box(vec![1, 2]); + assert!(GLOBAL.load(Ordering::Relaxed) == 1); + }) + .join() + .unwrap(); + assert!(GLOBAL.load(Ordering::Relaxed) == 3); +} From 9f769b0cdb6176c86b266b332cbabfdd78d5e64a Mon Sep 17 00:00:00 2001 From: Orson Peters Date: Fri, 11 Jul 2025 04:00:53 +0200 Subject: [PATCH 3/3] Fix drop order of last dtor w.r.t. GlobalAlloc TLS var --- .../src/sys/thread_local/destructors/list.rs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/library/std/src/sys/thread_local/destructors/list.rs b/library/std/src/sys/thread_local/destructors/list.rs index 6292b4818b5ae..3b2ce24c89daa 100644 --- a/library/std/src/sys/thread_local/destructors/list.rs +++ b/library/std/src/sys/thread_local/destructors/list.rs @@ -8,7 +8,7 @@ static REENTRANT_DTOR: Cell> = static DTORS: RefCell> = RefCell::new(Vec::new()); pub unsafe fn register(t: *mut u8, dtor: unsafe extern "C" fn(*mut u8)) { - // Borrow DTORS can only fail if the global allocator calls this + // Borrowing DTORS can only fail if the global allocator calls this // function again. if let Ok(mut dtors) = DTORS.try_borrow_mut() { guard::enable(); @@ -33,18 +33,21 @@ pub unsafe fn register(t: *mut u8, dtor: unsafe extern "C" fn(*mut u8)) { pub unsafe fn run() { loop { let mut dtors = DTORS.borrow_mut(); - match dtors.pop() { - Some((t, dtor)) => { - drop(dtors); - unsafe { - dtor(t); - } - } - None => { - // Free the list memory. - *dtors = Vec::new(); - break; - } + let Some((t, dtor)) = dtors.pop() else { break }; + + // If the global allocator has allocated a thread-local variable + // it will always be the first in the dtors list (if an alloc came + // before the first regular register()), or be the REENTRANT_DTOR + // (if a register() came before the first alloc). In the former + // case we have to make sure not to touch the global (de)allocator again + // on this thread, so free dtors now, before calling the last dtor. + if dtors.is_empty() { + *dtors = Vec::new(); + } + + drop(dtors); + unsafe { + dtor(t); } }