miri/shims/
tls.rs

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