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, HasDataLayout, 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. None 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, Scalar>,
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 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    /// On macOS, each thread holds a list of destructor functions with their
50    /// respective data arguments.
51    macos_thread_dtors: BTreeMap<ThreadId, Vec<(ty::Instance<'tcx>, Scalar, Span)>>,
52}
53
54impl<'tcx> Default for TlsData<'tcx> {
55    fn default() -> Self {
56        TlsData {
57            next_key: 1, // start with 1 as we must not use 0 on Windows
58            keys: Default::default(),
59            macos_thread_dtors: Default::default(),
60        }
61    }
62}
63
64impl<'tcx> TlsData<'tcx> {
65    /// Generate a new TLS key with the given destructor.
66    /// `max_size` determines the integer size the key has to fit in.
67    #[expect(clippy::arithmetic_side_effects)]
68    pub fn create_tls_key(
69        &mut self,
70        dtor: Option<(ty::Instance<'tcx>, Span)>,
71        max_size: Size,
72    ) -> InterpResult<'tcx, TlsKey> {
73        let new_key = self.next_key;
74        self.next_key += 1;
75        self.keys.try_insert(new_key, TlsEntry { data: Default::default(), dtor }).unwrap();
76        trace!("New TLS key allocated: {} with dtor {:?}", new_key, dtor);
77
78        if max_size.bits() < 128 && new_key >= (1u128 << max_size.bits()) {
79            throw_unsup_format!("we ran out of TLS key space");
80        }
81        interp_ok(new_key)
82    }
83
84    pub fn delete_tls_key(&mut self, key: TlsKey) -> InterpResult<'tcx, TlsEntry<'tcx>> {
85        match self.keys.remove(&key) {
86            Some(entry) => {
87                trace!("TLS key {} removed", key);
88                interp_ok(entry)
89            }
90            None => throw_ub_format!("removing a nonexistent TLS key: {}", key),
91        }
92    }
93
94    pub fn load_tls(
95        &self,
96        key: TlsKey,
97        thread_id: ThreadId,
98        cx: &impl HasDataLayout,
99    ) -> InterpResult<'tcx, Scalar> {
100        match self.keys.get(&key) {
101            Some(TlsEntry { data, .. }) => {
102                let value = data.get(&thread_id).copied();
103                trace!("TLS key {} for thread {:?} loaded: {:?}", key, thread_id, value);
104                interp_ok(value.unwrap_or_else(|| Scalar::null_ptr(cx)))
105            }
106            None => throw_ub_format!("loading from a non-existing TLS key: {}", key),
107        }
108    }
109
110    pub fn store_tls(
111        &mut self,
112        key: TlsKey,
113        thread_id: ThreadId,
114        new_data: Scalar,
115        cx: &impl HasDataLayout,
116    ) -> InterpResult<'tcx> {
117        match self.keys.get_mut(&key) {
118            Some(TlsEntry { data, .. }) => {
119                if new_data.to_target_usize(cx)? != 0 {
120                    trace!("TLS key {} for thread {:?} stored: {:?}", key, thread_id, new_data);
121                    data.insert(thread_id, new_data);
122                } else {
123                    trace!("TLS key {} for thread {:?} removed", key, thread_id);
124                    data.remove(&thread_id);
125                }
126                interp_ok(())
127            }
128            None => throw_ub_format!("storing to a non-existing TLS key: {}", key),
129        }
130    }
131
132    /// Add a thread local storage destructor for the given thread. This function
133    /// is used to implement the `_tlv_atexit` shim on MacOS.
134    pub fn add_macos_thread_dtor(
135        &mut self,
136        thread: ThreadId,
137        dtor: ty::Instance<'tcx>,
138        data: Scalar,
139        span: Span,
140    ) -> InterpResult<'tcx> {
141        self.macos_thread_dtors.entry(thread).or_default().push((dtor, data, span));
142        interp_ok(())
143    }
144
145    /// Returns a dtor, its argument and its index, if one is supposed to run.
146    /// `key` is the last dtors that was run; we return the *next* one after that.
147    ///
148    /// An optional destructor function may be associated with each key value.
149    /// At thread exit, if a key value has a non-NULL destructor pointer,
150    /// and the thread has a non-NULL value associated with that key,
151    /// the value of the key is set to NULL, and then the function pointed
152    /// to is called with the previously associated value as its sole argument.
153    /// **The order of destructor calls is unspecified if more than one destructor
154    /// exists for a thread when it exits.**
155    ///
156    /// If, after all the destructors have been called for all non-NULL values
157    /// with associated destructors, there are still some non-NULL values with
158    /// associated destructors, then the process is repeated.
159    /// If, after at least {PTHREAD_DESTRUCTOR_ITERATIONS} iterations of destructor
160    /// calls for outstanding non-NULL values, there are still some non-NULL values
161    /// with associated destructors, implementations may stop calling destructors,
162    /// or they may continue calling destructors until no non-NULL values with
163    /// associated destructors exist, even though this might result in an infinite loop.
164    fn fetch_tls_dtor(
165        &mut self,
166        key: Option<TlsKey>,
167        thread_id: ThreadId,
168    ) -> Option<(ty::Instance<'tcx>, Scalar, TlsKey, Span)> {
169        use std::ops::Bound::*;
170
171        let thread_local = &mut self.keys;
172        let start = match key {
173            Some(key) => Excluded(key),
174            None => Unbounded,
175        };
176        // We interpret the documentation above (taken from POSIX) as saying that we need to iterate
177        // over all keys and run each destructor at least once before running any destructor a 2nd
178        // time. That's why we have `key` to indicate how far we got in the current iteration. If we
179        // return `None`, `schedule_next_pthread_tls_dtor` will re-try with `ket` set to `None` to
180        // start the next round.
181        // TODO: In the future, we might consider randomizing destructor order, but we still have to
182        // uphold this requirement.
183        for (&key, TlsEntry { data, dtor }) in thread_local.range_mut((start, Unbounded)) {
184            match data.entry(thread_id) {
185                BTreeEntry::Occupied(entry) => {
186                    if let Some((dtor, span)) = dtor {
187                        // Set TLS data to NULL, and call dtor with old value.
188                        let data_scalar = entry.remove();
189                        return Some((*dtor, data_scalar, key, *span));
190                    }
191                }
192                BTreeEntry::Vacant(_) => {}
193            }
194        }
195        None
196    }
197
198    /// Delete all TLS entries for the given thread. This function should be
199    /// called after all TLS destructors have already finished.
200    fn delete_all_thread_tls(&mut self, thread_id: ThreadId) {
201        for TlsEntry { data, .. } in self.keys.values_mut() {
202            data.remove(&thread_id);
203        }
204
205        if let Some(dtors) = self.macos_thread_dtors.remove(&thread_id) {
206            assert!(dtors.is_empty(), "the destructors should have already been run");
207        }
208    }
209}
210
211impl VisitProvenance for TlsData<'_> {
212    fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
213        let TlsData { keys, macos_thread_dtors, next_key: _ } = self;
214
215        for scalar in keys.values().flat_map(|v| v.data.values()) {
216            scalar.visit_provenance(visit);
217        }
218        for (_, scalar, _) in macos_thread_dtors.values().flatten() {
219            scalar.visit_provenance(visit);
220        }
221    }
222}
223
224#[derive(Debug, Default)]
225pub struct TlsDtorsState<'tcx>(TlsDtorsStatePriv<'tcx>);
226
227#[derive(Debug, Default)]
228enum TlsDtorsStatePriv<'tcx> {
229    #[default]
230    Init,
231    MacOsDtors,
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)>),
238    Done,
239}
240
241impl<'tcx> TlsDtorsState<'tcx> {
242    pub fn on_stack_empty(
243        &mut self,
244        this: &mut MiriInterpCx<'tcx>,
245    ) -> InterpResult<'tcx, Poll<()>> {
246        use TlsDtorsStatePriv::*;
247        let new_state = 'new_state: {
248            match &mut self.0 {
249                Init => {
250                    match this.tcx.sess.target.os {
251                        Os::MacOs => {
252                            // macOS has a _tlv_atexit function that allows
253                            // registering destructors without associated keys.
254                            // These are run first.
255                            break 'new_state MacOsDtors;
256                        }
257                        _ if this.target_os_is_unix() => {
258                            // All other Unixes directly jump to running the pthread dtors.
259                            break 'new_state PthreadDtors(Default::default());
260                        }
261                        Os::Windows => {
262                            // Determine which destructors to run.
263                            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
269                            // And move to the next state, that runs them.
270                            break 'new_state WindowsDtors(RunningWindowsDtorState { last_key: None, remaining_keys: fls_keys_with_dtors }, dtors);
271                        }
272                        _ => {
273                            // No TLS dtor support.
274                            break 'new_state Done;
275                        }
276                    }
277                }
278                MacOsDtors => {
279                    match this.schedule_macos_tls_dtor()? {
280                        Poll::Pending => return interp_ok(Poll::Pending),
281                        // After all macOS destructors are run, the system switches
282                        // to destroying the pthread destructors.
283                        Poll::Ready(()) => break 'new_state PthreadDtors(Default::default()),
284                    }
285                }
286                PthreadDtors(state) => {
287                    match this.schedule_next_pthread_tls_dtor(state)? {
288                        Poll::Pending => return interp_ok(Poll::Pending), // just keep going
289                        Poll::Ready(()) => break 'new_state Done,
290                    }
291                }
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
299                    if let Some((dtor, span)) = dtors.pop() {
300                        this.schedule_windows_tls_dtor(dtor, span)?;
301                        return interp_ok(Poll::Pending); // we stay in this state (but `dtors` got shorter)
302                    } else {
303                        // No more destructors to run.
304                        break 'new_state Done;
305                    }
306                }
307                Done => {
308                    this.machine.tls.delete_all_thread_tls(this.active_thread());
309                    return interp_ok(Poll::Ready(()));
310                }
311            }
312        };
313
314        self.0 = new_state;
315        interp_ok(Poll::Pending)
316    }
317}
318
319impl<'tcx> EvalContextPrivExt<'tcx> for crate::MiriInterpCx<'tcx> {}
320trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
321    /// Schedule TLS destructors for Windows.
322    /// On windows, TLS destructors are managed by std.
323    fn lookup_windows_tls_dtors(&mut self) -> InterpResult<'tcx, Vec<(ImmTy<'tcx>, Span)>> {
324        let this = self.eval_context_mut();
325
326        // Windows has a special magic linker section that is run on certain events.
327        // We don't support most of that, but just enough to make thread-local dtors in `std` work.
328        interp_ok(this.lookup_link_section(|section| section == ".CRT$XLB")?)
329    }
330
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
347    fn schedule_windows_tls_dtor(&mut self, dtor: ImmTy<'tcx>, span: Span) -> InterpResult<'tcx> {
348        let this = self.eval_context_mut();
349
350        let dtor = dtor.to_scalar().to_pointer(this)?;
351        let thread_callback = this.get_ptr_fn(dtor)?.as_instance()?;
352
353        // FIXME: Technically, the reason should be `DLL_PROCESS_DETACH` when the main thread exits
354        // but std treats both the same.
355        let reason = this.eval_windows("c", "DLL_THREAD_DETACH");
356        let null_ptr =
357            ImmTy::from_scalar(Scalar::null_ptr(this), this.machine.layouts.const_raw_ptr);
358
359        // The signature of this function is `unsafe extern "system" fn(h: c::LPVOID, dwReason: c::DWORD, pv: c::LPVOID)`.
360        // FIXME: `h` should be a handle to the current module and what `pv` should be is unknown
361        // but both are ignored by std.
362        this.call_thread_root_function(
363            thread_callback,
364            ExternAbi::System { unwind: false },
365            &[null_ptr.clone(), ImmTy::from_scalar(reason, this.machine.layouts.u32), null_ptr],
366            None,
367            span,
368        )?;
369        interp_ok(())
370    }
371
372    /// Schedule the macOS thread local storage destructors to be executed.
373    fn schedule_macos_tls_dtor(&mut self) -> InterpResult<'tcx, Poll<()>> {
374        let this = self.eval_context_mut();
375        let thread_id = this.active_thread();
376        // macOS keeps track of TLS destructors in a stack. If a destructor
377        // registers another destructor, it will be run next.
378        // See https://github.com/apple-oss-distributions/dyld/blob/d552c40cd1de105f0ec95008e0e0c0972de43456/dyld/DyldRuntimeState.cpp#L2277
379        let dtor = this.machine.tls.macos_thread_dtors.get_mut(&thread_id).and_then(Vec::pop);
380        if let Some((instance, data, span)) = dtor {
381            trace!("Running macos dtor {:?} on {:?} at {:?}", instance, data, thread_id);
382
383            this.call_thread_root_function(
384                instance,
385                ExternAbi::C { unwind: false },
386                &[ImmTy::from_scalar(data, this.machine.layouts.mut_raw_ptr)],
387                None,
388                span,
389            )?;
390
391            return interp_ok(Poll::Pending);
392        }
393
394        interp_ok(Poll::Ready(()))
395    }
396
397    /// Schedule a pthread TLS destructor. Returns `true` if found
398    /// a destructor to schedule, and `false` otherwise.
399    fn schedule_next_pthread_tls_dtor(
400        &mut self,
401        state: &mut RunningPthreadDtorState,
402    ) -> InterpResult<'tcx, Poll<()>> {
403        let this = self.eval_context_mut();
404        let active_thread = this.active_thread();
405
406        // Fetch next dtor after `key`.
407        let dtor = match this.machine.tls.fetch_tls_dtor(state.last_key, active_thread) {
408            dtor @ Some(_) => dtor,
409            // We ran each dtor once, start over from the beginning.
410            None => this.machine.tls.fetch_tls_dtor(None, active_thread),
411        };
412        if let Some((instance, ptr, key, span)) = dtor {
413            state.last_key = Some(key);
414            trace!("Running TLS dtor {:?} on {:?} at {:?}", instance, ptr, active_thread);
415            assert!(
416                ptr.to_target_usize(this).unwrap() != 0,
417                "data can't be NULL when dtor is called!"
418            );
419
420            this.call_thread_root_function(
421                instance,
422                ExternAbi::C { unwind: false },
423                &[ImmTy::from_scalar(ptr, this.machine.layouts.mut_raw_ptr)],
424                None,
425                span,
426            )?;
427
428            return interp_ok(Poll::Pending);
429        }
430
431        interp_ok(Poll::Ready(()))
432    }
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    }
504}