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}