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}