Skip to main content

miri/shims/
tls.rs

1//! Implement thread-local storage.
2
3use std::collections::btree_map::Entry as BTreeEntry;
4use std::collections::{BTreeMap, VecDeque};
5use std::task::Poll;
6
7use rustc_abi::{ExternAbi, Size};
8use rustc_middle::ty;
9use rustc_span::Span;
10use rustc_target::spec::Os;
11
12use crate::*;
13
14pub type TlsKey = u128;
15
16#[derive(Clone, Debug)]
17pub struct TlsEntry<'tcx> {
18    /// The data for this key. Absence of data for a thread ID is used to represent NULL.
19    /// (We normalize this early to avoid having to do a NULL-ptr-test each time we access the data.)
20    pub data: BTreeMap<ThreadId, Pointer>,
21    pub dtor: Option<(ty::Instance<'tcx>, Span)>,
22}
23
24#[derive(Default, Debug)]
25struct RunningPthreadDtorState {
26    /// The last TlsKey used to retrieve a TLS destructor. `None` means that we
27    /// have not tried to retrieve a TLS destructor yet or that we already tried
28    /// all keys.
29    last_key: Option<TlsKey>,
30}
31
32#[derive(Default, Debug)]
33struct RunningWindowsDtorState {
34    /// The last TlsKey for which a TLS (really, FLS) 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
41#[derive(Debug)]
42pub struct TlsData<'tcx> {
43    /// The Key to use for the next thread-local allocation.
44    next_key: TlsKey,
45
46    /// pthreads-style thread-local storage.
47    keys: BTreeMap<TlsKey, TlsEntry<'tcx>>,
48
49    /// Linux and macOS allow registering callbacks that are run when the current thread exits.
50    thread_atexit: BTreeMap<ThreadId, Vec<(ty::Instance<'tcx>, Scalar, Span)>>,
51}
52
53impl<'tcx> Default for TlsData<'tcx> {
54    fn default() -> Self {
55        TlsData {
56            next_key: 1, // start with 1 as we must not use 0 on Windows
57            keys: Default::default(),
58            thread_atexit: Default::default(),
59        }
60    }
61}
62
63impl<'tcx> TlsData<'tcx> {
64    /// Generate a new TLS key with the given destructor.
65    /// `key_size` determines the integer size the key has to fit in.
66    #[expect(clippy::arithmetic_side_effects)]
67    pub fn create_tls_key(
68        &mut self,
69        dtor: Option<(ty::Instance<'tcx>, Span)>,
70        key_size: Size,
71    ) -> InterpResult<'tcx, TlsKey> {
72        let new_key = self.next_key;
73        self.next_key += 1;
74        self.keys.try_insert(new_key, TlsEntry { data: Default::default(), dtor }).unwrap();
75        trace!("New TLS key allocated: {} with dtor {:?}", new_key, dtor);
76
77        if new_key > key_size.unsigned_int_max() {
78            throw_unsup_format!("we ran out of TLS key space");
79        }
80        interp_ok(new_key)
81    }
82
83    pub fn delete_tls_key(&mut self, key: TlsKey) -> InterpResult<'tcx, TlsEntry<'tcx>> {
84        match self.keys.remove(&key) {
85            Some(entry) => {
86                trace!("TLS key {} removed", key);
87                interp_ok(entry)
88            }
89            None => throw_ub_format!("removing a nonexistent TLS key: {}", key),
90        }
91    }
92
93    pub fn load_tls_key(&self, key: TlsKey, thread_id: ThreadId) -> InterpResult<'tcx, Pointer> {
94        match self.keys.get(&key) {
95            Some(TlsEntry { data, .. }) => {
96                let value = data.get(&thread_id).copied();
97                trace!("TLS key {} for thread {:?} loaded: {:?}", key, thread_id, value);
98                interp_ok(value.unwrap_or_else(Pointer::null))
99            }
100            None => throw_ub_format!("loading from a non-existing TLS key: {}", key),
101        }
102    }
103
104    pub fn store_tls_key(
105        &mut self,
106        key: TlsKey,
107        thread_id: ThreadId,
108        new_data: Pointer,
109    ) -> InterpResult<'tcx> {
110        match self.keys.get_mut(&key) {
111            Some(TlsEntry { data, .. }) => {
112                if new_data.addr() != Size::ZERO {
113                    trace!("TLS key {} for thread {:?} stored: {:?}", key, thread_id, new_data);
114                    data.insert(thread_id, new_data);
115                } else {
116                    trace!("TLS key {} for thread {:?} removed", key, thread_id);
117                    data.remove(&thread_id);
118                }
119                interp_ok(())
120            }
121            None => throw_ub_format!("storing to a non-existing TLS key: {}", key),
122        }
123    }
124
125    /// Add a thread local storage destructor for the given thread. This function
126    /// is used to implement the `_tlv_atexit` shim on MacOS.
127    pub fn add_thread_atexit(
128        &mut self,
129        thread: ThreadId,
130        dtor: ty::Instance<'tcx>,
131        data: Scalar,
132        span: Span,
133    ) -> InterpResult<'tcx> {
134        self.thread_atexit.entry(thread).or_default().push((dtor, data, span));
135        interp_ok(())
136    }
137
138    /// Returns a dtor, its argument and its index, if one is supposed to run.
139    /// `key` is the last dtors that was run; we return the *next* one after that.
140    ///
141    /// An optional destructor function may be associated with each key value.
142    /// At thread exit, if a key value has a non-NULL destructor pointer,
143    /// and the thread has a non-NULL value associated with that key,
144    /// the value of the key is set to NULL, and then the function pointed
145    /// to is called with the previously associated value as its sole argument.
146    /// **The order of destructor calls is unspecified if more than one destructor
147    /// exists for a thread when it exits.**
148    ///
149    /// If, after all the destructors have been called for all non-NULL values
150    /// with associated destructors, there are still some non-NULL values with
151    /// associated destructors, then the process is repeated.
152    /// If, after at least {PTHREAD_DESTRUCTOR_ITERATIONS} iterations of destructor
153    /// calls for outstanding non-NULL values, there are still some non-NULL values
154    /// with associated destructors, implementations may stop calling destructors,
155    /// or they may continue calling destructors until no non-NULL values with
156    /// associated destructors exist, even though this might result in an infinite loop.
157    fn fetch_tls_key_dtor(
158        &mut self,
159        key: Option<TlsKey>,
160        thread_id: ThreadId,
161    ) -> Option<(ty::Instance<'tcx>, Pointer, TlsKey, Span)> {
162        use std::ops::Bound::*;
163
164        let thread_local = &mut self.keys;
165        let start = match key {
166            Some(key) => Excluded(key),
167            None => Unbounded,
168        };
169        // We interpret the documentation above (taken from POSIX) as saying that we need to iterate
170        // over all keys and run each destructor at least once before running any destructor a 2nd
171        // time. That's why we have `key` to indicate how far we got in the current iteration. If we
172        // return `None`, `schedule_next_pthread_tls_dtor` will re-try with `ket` set to `None` to
173        // start the next round.
174        // TODO: In the future, we might consider randomizing destructor order, but we still have to
175        // uphold this requirement.
176        for (&key, TlsEntry { data, dtor }) in thread_local.range_mut((start, Unbounded)) {
177            match data.entry(thread_id) {
178                BTreeEntry::Occupied(entry) => {
179                    if let Some((dtor, span)) = dtor {
180                        // Set TLS data to NULL, and call dtor with old value.
181                        let data = entry.remove();
182                        return Some((*dtor, data, key, *span));
183                    }
184                }
185                BTreeEntry::Vacant(_) => {}
186            }
187        }
188        None
189    }
190
191    /// Delete all TLS entries for the given thread. This function should be
192    /// called after all TLS destructors have already finished.
193    fn delete_all_thread_tls_keys(&mut self, thread_id: ThreadId) {
194        for TlsEntry { data, .. } in self.keys.values_mut() {
195            data.remove(&thread_id);
196        }
197
198        if let Some(dtors) = self.thread_atexit.remove(&thread_id) {
199            assert!(dtors.is_empty(), "the destructors should have already been run");
200        }
201    }
202
203    pub fn leak_all_thread_tls_keys(&self, thread_id: ThreadId, static_roots: &mut Vec<AllocId>) {
204        for TlsEntry { data, .. } in self.keys.values() {
205            if let Some(ptr) = data.get(&thread_id) {
206                if let Some(alloc) = ptr.provenance.and_then(|p| p.get_alloc_id()) {
207                    trace!("Thread-local key value leaked and stored as static root: {:?}", alloc);
208                    static_roots.push(alloc);
209                }
210            }
211        }
212    }
213}
214
215impl VisitProvenance for TlsData<'_> {
216    fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
217        let TlsData { keys, thread_atexit: macos_thread_dtors, next_key: _ } = self;
218
219        for scalar in keys.values().flat_map(|v| v.data.values()) {
220            scalar.visit_provenance(visit);
221        }
222        for (_, scalar, _) in macos_thread_dtors.values().flatten() {
223            scalar.visit_provenance(visit);
224        }
225    }
226}
227
228#[derive(Debug)]
229pub struct TlsDtorsState<'tcx> {
230    state: TlsDtorsStatePriv<'tcx>,
231    /// Whether this is for program exit vs thread finish.
232    for_exit: bool,
233}
234
235#[derive(Debug)]
236enum TlsDtorsStatePriv<'tcx> {
237    Init,
238    /// Running native thread destructors (`TlsData::thread_atexit`).
239    ThreadAtexit,
240    /// Running pthread key destructors.
241    PthreadDtors(RunningPthreadDtorState),
242    /// For Windows, we support two different ways dtors can be registered.
243    /// 1. Functions that are registered via the `FlsAlloc` function, which are invoked one by one.
244    /// 2. Functions from the magic `.CRT$XLB` linker section.
245    ///    We store these as a list of functions that we still have to call.
246    WindowsDtors(RunningWindowsDtorState, Vec<(ImmTy<'tcx>, Span)>),
247    Done,
248}
249
250impl<'tcx> TlsDtorsState<'tcx> {
251    /// Returns the state we should start running when the program is exiting.
252    pub fn new_for_exit() -> Self {
253        Self { state: TlsDtorsStatePriv::Init, for_exit: true }
254    }
255
256    /// Returns the state we should start running when a thread finished running.
257    pub fn new_for_thread_finish() -> Self {
258        Self { state: TlsDtorsStatePriv::Init, for_exit: false }
259    }
260
261    pub fn on_stack_empty(&mut self, ecx: &mut MiriInterpCx<'tcx>) -> InterpResult<'tcx, Poll<()>> {
262        use TlsDtorsStatePriv::*;
263        let new_state = 'new_state: {
264            match &mut self.state {
265                Init => {
266                    break 'new_state ThreadAtexit;
267                }
268                ThreadAtexit => {
269                    let Poll::Ready(()) = ecx.schedule_thread_atexit()? else {
270                        return interp_ok(Poll::Pending);
271                    };
272                    // Native thread_atexit handlers are done.
273                    // What's next? Depends on the target!
274                    match ecx.tcx.sess.target.os {
275                        _ if ecx.target_os_is_unix() => {
276                            // On thread finish, we run pthread dtors. But not on exit!
277                            if self.for_exit {
278                                // Leak pthread dtor contents, like thread-local statics.
279                                ecx.machine.tls.leak_all_thread_tls_keys(
280                                    ecx.active_thread(),
281                                    &mut ecx.machine.static_roots,
282                                );
283                                break 'new_state Done;
284                            } else {
285                                break 'new_state PthreadDtors(Default::default());
286                            }
287                        }
288                        Os::Windows => {
289                            // Determine which destructors to run.
290                            let dtors = ecx.lookup_windows_tls_dtors()?;
291
292                            // Fetch fls keys that have destructors. Keys registered during thread
293                            // exit may or may not have their destructor called. We decide not to
294                            // call them. See also: `schedule_next_windows_fls_dtor`.
295                            let fls_keys_with_dtors = ecx.lookup_windows_fls_keys_with_dtors()?;
296
297                            // And move to the next state, that runs them.
298                            break 'new_state WindowsDtors(
299                                RunningWindowsDtorState {
300                                    last_key: None,
301                                    remaining_keys: fls_keys_with_dtors,
302                                },
303                                dtors,
304                            );
305                        }
306                        _ => {
307                            break 'new_state Done;
308                        }
309                    }
310                }
311                PthreadDtors(state) => {
312                    let Poll::Ready(()) = ecx.schedule_next_pthread_tls_dtor(state)? else {
313                        return interp_ok(Poll::Pending);
314                    };
315                    break 'new_state Done;
316                }
317                WindowsDtors(state, dtors) => {
318                    // Fls destructors are scheduled before the tls callback.
319                    let Poll::Ready(()) = ecx.schedule_next_windows_fls_dtor(state)? else {
320                        return interp_ok(Poll::Pending);
321                    };
322
323                    // See if there's another callback to run
324                    if let Some((dtor, span)) = dtors.pop() {
325                        ecx.schedule_windows_tls_dtor(dtor, span)?;
326                        return interp_ok(Poll::Pending); // we stay in this state (but `dtors` got shorter)
327                    } else {
328                        // No more destructors to run.
329                        break 'new_state Done;
330                    }
331                }
332                Done => {
333                    // All destructors are called. Clean up remaining keys so they don't linger in
334                    // the interpreter state.
335                    ecx.machine.tls.delete_all_thread_tls_keys(ecx.active_thread());
336                    return interp_ok(Poll::Ready(()));
337                }
338            }
339        };
340
341        self.state = new_state;
342        interp_ok(Poll::Pending)
343    }
344}
345
346impl<'tcx> EvalContextPrivExt<'tcx> for crate::MiriInterpCx<'tcx> {}
347trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
348    /// Schedule TLS destructors for Windows.
349    /// On windows, TLS destructors are managed by std.
350    fn lookup_windows_tls_dtors(&mut self) -> InterpResult<'tcx, Vec<(ImmTy<'tcx>, Span)>> {
351        let ecx = self.eval_context_mut();
352
353        // Windows has a special magic linker section that is run on certain events.
354        // We don't support most of that, but just enough to make thread-local dtors in `std` work.
355        interp_ok(ecx.lookup_link_section(|section| section == ".CRT$XLB")?)
356    }
357
358    /// Lookup all the FLS keys (which are stored as TLS keys) that have a destructor.
359    /// See also: `schedule_next_windows_fls_dtor`.
360    fn lookup_windows_fls_keys_with_dtors(&self) -> InterpResult<'tcx, VecDeque<TlsKey>> {
361        let ecx = self.eval_context_ref();
362
363        interp_ok(
364            ecx.machine
365                .tls
366                .keys
367                .iter()
368                .filter(|(_, data)| data.dtor.is_some())
369                .map(|(key, _)| *key)
370                .collect(),
371        )
372    }
373
374    fn schedule_windows_tls_dtor(&mut self, dtor: ImmTy<'tcx>, span: Span) -> InterpResult<'tcx> {
375        let ecx = self.eval_context_mut();
376
377        let dtor = dtor.to_scalar().to_pointer(ecx);
378        let thread_callback = ecx.get_ptr_fn(dtor)?.as_instance()?;
379
380        // FIXME: Technically, the reason should be `DLL_PROCESS_DETACH` when the main thread exits
381        // but std treats both the same.
382        let reason = ecx.eval_windows("c", "DLL_THREAD_DETACH");
383        let null_ptr =
384            ImmTy::from_scalar(Scalar::null_ptr(ecx), ecx.machine.layouts.unit_ptr_const);
385
386        // The signature of this function is `unsafe extern "system" fn(h: c::LPVOID, dwReason: c::DWORD, pv: c::LPVOID)`.
387        // FIXME: `h` should be a handle to the current module and what `pv` should be is unknown
388        // but both are ignored by std.
389        ecx.call_thread_root_function(
390            thread_callback,
391            ExternAbi::System { unwind: false },
392            &[null_ptr.clone(), ImmTy::from_scalar(reason, ecx.machine.layouts.u32), null_ptr],
393            None,
394            span,
395        )?;
396        interp_ok(())
397    }
398
399    /// Schedule the macOS thread local storage destructors to be executed.
400    fn schedule_thread_atexit(&mut self) -> InterpResult<'tcx, Poll<()>> {
401        let ecx = self.eval_context_mut();
402        let thread_id = ecx.active_thread();
403        // macOS keeps track of TLS destructors in a stack. If a destructor
404        // registers another destructor, it will be run next.
405        // See https://github.com/apple-oss-distributions/dyld/blob/d552c40cd1de105f0ec95008e0e0c0972de43456/dyld/DyldRuntimeState.cpp#L2277
406        let dtor = ecx.machine.tls.thread_atexit.get_mut(&thread_id).and_then(Vec::pop);
407        if let Some((instance, data, span)) = dtor {
408            trace!("Running thread_atexit hook {:?} on {:?} at {:?}", instance, data, thread_id);
409
410            ecx.call_thread_root_function(
411                instance,
412                ExternAbi::C { unwind: false },
413                &[ImmTy::from_scalar(data, ecx.machine.layouts.unit_ptr_mut)],
414                None,
415                span,
416            )?;
417
418            return interp_ok(Poll::Pending);
419        }
420
421        interp_ok(Poll::Ready(()))
422    }
423
424    /// Schedule a pthread TLS destructor. Returns `true` if found
425    /// a destructor to schedule, and `false` otherwise.
426    fn schedule_next_pthread_tls_dtor(
427        &mut self,
428        state: &mut RunningPthreadDtorState,
429    ) -> InterpResult<'tcx, Poll<()>> {
430        let ecx = self.eval_context_mut();
431        let active_thread = ecx.active_thread();
432
433        // Fetch next dtor after `key`.
434        let dtor = match ecx.machine.tls.fetch_tls_key_dtor(state.last_key, active_thread) {
435            dtor @ Some(_) => dtor,
436            // We ran each dtor once, start over from the beginning.
437            None => ecx.machine.tls.fetch_tls_key_dtor(None, active_thread),
438        };
439        if let Some((instance, ptr, key, span)) = dtor {
440            state.last_key = Some(key);
441            trace!("Running TLS dtor {:?} on {:?} at {:?}", instance, ptr, active_thread);
442            assert!(ptr.addr() != Size::ZERO, "data can't be NULL when dtor is called!");
443
444            ecx.call_thread_root_function(
445                instance,
446                ExternAbi::C { unwind: false },
447                &[ImmTy::from_scalar(
448                    Scalar::from_maybe_pointer(ptr, ecx),
449                    ecx.machine.layouts.unit_ptr_mut,
450                )],
451                None,
452                span,
453            )?;
454
455            return interp_ok(Poll::Pending);
456        }
457
458        interp_ok(Poll::Ready(()))
459    }
460
461    /// Schedule a Windows FLS destructor, if one is found.
462    fn schedule_next_windows_fls_dtor(
463        &mut self,
464        state: &mut RunningWindowsDtorState,
465    ) -> InterpResult<'tcx, Poll<()>> {
466        let ecx = self.eval_context_mut();
467        let active_thread = ecx.active_thread();
468
469        // According to [PflsCallbackFunction's docs],
470        // > If the FLS slot is in use, `FlsCallback`` is called on .. thread exit ..
471        // However, the exact order and semantics are not defined.
472        // We use the following implementation, which matches both observed behavior and
473        // Wine's implementation of the same logic in the [`RtlProcessFlsData`] function.
474        // 1. Fetch all the keys once (in `lookup_windows_fls_keys_with_dtors`).
475        // 2. Go over them one by one, in order, skipping keys without dtors.
476        // 3. Fetch the value associated with the key.
477        // 4. If it is non-zero, call the registered dtor.
478        // 5. After the dtor is called, clear the key's value, setting it to zero.
479        // New keys registered during thread exit can be ignored (but sometimes they do run
480        // natively, just not reliably), but values set before the dtor is scheduled are visible.
481        // Keys without dtors will not be set to zero.
482        // [PflsCallbackFunction's docs]: https://learn.microsoft.com/en-us/windows/win32/api/winnt/nc-winnt-pfls_callback_function
483        // [`RtlProcessFlsData`]: https://github.com/wine-mirror/wine/blob/wine-11.0/dlls/ntdll/thread.c#L679
484
485        // We are done running the previous key's destructor, so set it's value to zero.
486        if let Some(last_key) = state.last_key.take() {
487            if let Some(TlsEntry { data, .. }) = ecx.machine.tls.keys.get_mut(&last_key) {
488                data.remove(&active_thread);
489            };
490        }
491
492        while let Some(key) = state.remaining_keys.pop_front() {
493            // Fetch dtor for this `key`.
494            // If the key doesn't have a dtor or does not exist any more, move on to the next key.
495            let Some(TlsEntry { data, dtor: Some(dtor) }) = ecx.machine.tls.keys.get(&key) else {
496                continue;
497            };
498
499            let (instance, span) = dtor.to_owned();
500
501            // If the key has no value in this thread, move on to the next key.
502            let Some(&ptr) = data.get(&active_thread) else { continue };
503
504            assert!(
505                ptr.addr() != Size::ZERO,
506                "TLS key's value can't be null (should be absent instead)"
507            );
508
509            trace!("Running TLS dtor {:?} on {:?} at {:?}", instance, ptr, active_thread);
510
511            // We'll clear this key's value next time we are called.
512            state.last_key = Some(key);
513
514            ecx.call_thread_root_function(
515                instance,
516                ExternAbi::System { unwind: false },
517                &[ImmTy::from_scalar(
518                    Scalar::from_maybe_pointer(ptr, ecx),
519                    ecx.machine.layouts.unit_ptr_mut,
520                )],
521                None,
522                span,
523            )?;
524
525            return interp_ok(Poll::Pending);
526        }
527
528        // We are done scheduling all the keys.
529        interp_ok(Poll::Ready(()))
530    }
531}