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 /// `key_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 key_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 new_key > key_size.unsigned_int_max() {
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(
271 RunningWindowsDtorState {
272 last_key: None,
273 remaining_keys: fls_keys_with_dtors,
274 },
275 dtors,
276 );
277 }
278 _ => {
279 // No TLS dtor support.
280 break 'new_state Done;
281 }
282 }
283 }
284 MacOsDtors => {
285 match this.schedule_macos_tls_dtor()? {
286 Poll::Pending => return interp_ok(Poll::Pending),
287 // After all macOS destructors are run, the system switches
288 // to destroying the pthread destructors.
289 Poll::Ready(()) => break 'new_state PthreadDtors(Default::default()),
290 }
291 }
292 PthreadDtors(state) => {
293 match this.schedule_next_pthread_tls_dtor(state)? {
294 Poll::Pending => return interp_ok(Poll::Pending), // just keep going
295 Poll::Ready(()) => break 'new_state Done,
296 }
297 }
298 WindowsDtors(state, dtors) => {
299 // Fls destructors are scheduled before the tls callback.
300 match this.schedule_next_windows_fls_dtor(state)? {
301 Poll::Pending => return interp_ok(Poll::Pending), // just keep going
302 Poll::Ready(()) => {}
303 }
304
305 if let Some((dtor, span)) = dtors.pop() {
306 this.schedule_windows_tls_dtor(dtor, span)?;
307 return interp_ok(Poll::Pending); // we stay in this state (but `dtors` got shorter)
308 } else {
309 // No more destructors to run.
310 break 'new_state Done;
311 }
312 }
313 Done => {
314 this.machine.tls.delete_all_thread_tls(this.active_thread());
315 return interp_ok(Poll::Ready(()));
316 }
317 }
318 };
319
320 self.0 = new_state;
321 interp_ok(Poll::Pending)
322 }
323}
324
325impl<'tcx> EvalContextPrivExt<'tcx> for crate::MiriInterpCx<'tcx> {}
326trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
327 /// Schedule TLS destructors for Windows.
328 /// On windows, TLS destructors are managed by std.
329 fn lookup_windows_tls_dtors(&mut self) -> InterpResult<'tcx, Vec<(ImmTy<'tcx>, Span)>> {
330 let this = self.eval_context_mut();
331
332 // Windows has a special magic linker section that is run on certain events.
333 // We don't support most of that, but just enough to make thread-local dtors in `std` work.
334 interp_ok(this.lookup_link_section(|section| section == ".CRT$XLB")?)
335 }
336
337 /// Lookup all the FLS keys (which are stored as TLS keys) that have a destructor.
338 /// See also: `schedule_next_windows_fls_dtor`.
339 fn lookup_windows_fls_keys_with_dtors(&mut self) -> InterpResult<'tcx, VecDeque<TlsKey>> {
340 let this = self.eval_context_mut();
341
342 interp_ok(
343 this.machine
344 .tls
345 .keys
346 .iter()
347 .filter(|(_, data)| data.dtor.is_some())
348 .map(|(key, _)| *key)
349 .collect(),
350 )
351 }
352
353 fn schedule_windows_tls_dtor(&mut self, dtor: ImmTy<'tcx>, span: Span) -> InterpResult<'tcx> {
354 let this = self.eval_context_mut();
355
356 let dtor = dtor.to_scalar().to_pointer(this)?;
357 let thread_callback = this.get_ptr_fn(dtor)?.as_instance()?;
358
359 // FIXME: Technically, the reason should be `DLL_PROCESS_DETACH` when the main thread exits
360 // but std treats both the same.
361 let reason = this.eval_windows("c", "DLL_THREAD_DETACH");
362 let null_ptr =
363 ImmTy::from_scalar(Scalar::null_ptr(this), this.machine.layouts.const_raw_ptr);
364
365 // The signature of this function is `unsafe extern "system" fn(h: c::LPVOID, dwReason: c::DWORD, pv: c::LPVOID)`.
366 // FIXME: `h` should be a handle to the current module and what `pv` should be is unknown
367 // but both are ignored by std.
368 this.call_thread_root_function(
369 thread_callback,
370 ExternAbi::System { unwind: false },
371 &[null_ptr.clone(), ImmTy::from_scalar(reason, this.machine.layouts.u32), null_ptr],
372 None,
373 span,
374 )?;
375 interp_ok(())
376 }
377
378 /// Schedule the macOS thread local storage destructors to be executed.
379 fn schedule_macos_tls_dtor(&mut self) -> InterpResult<'tcx, Poll<()>> {
380 let this = self.eval_context_mut();
381 let thread_id = this.active_thread();
382 // macOS keeps track of TLS destructors in a stack. If a destructor
383 // registers another destructor, it will be run next.
384 // See https://github.com/apple-oss-distributions/dyld/blob/d552c40cd1de105f0ec95008e0e0c0972de43456/dyld/DyldRuntimeState.cpp#L2277
385 let dtor = this.machine.tls.macos_thread_dtors.get_mut(&thread_id).and_then(Vec::pop);
386 if let Some((instance, data, span)) = dtor {
387 trace!("Running macos dtor {:?} on {:?} at {:?}", instance, data, thread_id);
388
389 this.call_thread_root_function(
390 instance,
391 ExternAbi::C { unwind: false },
392 &[ImmTy::from_scalar(data, this.machine.layouts.mut_raw_ptr)],
393 None,
394 span,
395 )?;
396
397 return interp_ok(Poll::Pending);
398 }
399
400 interp_ok(Poll::Ready(()))
401 }
402
403 /// Schedule a pthread TLS destructor. Returns `true` if found
404 /// a destructor to schedule, and `false` otherwise.
405 fn schedule_next_pthread_tls_dtor(
406 &mut self,
407 state: &mut RunningPthreadDtorState,
408 ) -> InterpResult<'tcx, Poll<()>> {
409 let this = self.eval_context_mut();
410 let active_thread = this.active_thread();
411
412 // Fetch next dtor after `key`.
413 let dtor = match this.machine.tls.fetch_tls_dtor(state.last_key, active_thread) {
414 dtor @ Some(_) => dtor,
415 // We ran each dtor once, start over from the beginning.
416 None => this.machine.tls.fetch_tls_dtor(None, active_thread),
417 };
418 if let Some((instance, ptr, key, span)) = dtor {
419 state.last_key = Some(key);
420 trace!("Running TLS dtor {:?} on {:?} at {:?}", instance, ptr, active_thread);
421 assert!(
422 ptr.to_target_usize(this).unwrap() != 0,
423 "data can't be NULL when dtor is called!"
424 );
425
426 this.call_thread_root_function(
427 instance,
428 ExternAbi::C { unwind: false },
429 &[ImmTy::from_scalar(ptr, this.machine.layouts.mut_raw_ptr)],
430 None,
431 span,
432 )?;
433
434 return interp_ok(Poll::Pending);
435 }
436
437 interp_ok(Poll::Ready(()))
438 }
439
440 /// Schedule a Windows FLS destructor, if one is found.
441 fn schedule_next_windows_fls_dtor(
442 &mut self,
443 state: &mut RunningWindowsDtorState,
444 ) -> InterpResult<'tcx, Poll<()>> {
445 let this = self.eval_context_mut();
446 let active_thread = this.active_thread();
447
448 // According to [PflsCallbackFunction's docs],
449 // > If the FLS slot is in use, `FlsCallback`` is called on .. thread exit ..
450 // However, the exact order and semantics are not defined.
451 // We use the following implementation, which matches both observed behavior and
452 // Wine's implementation of the same logic in the [`RtlProcessFlsData`] function.
453 // 1. Fetch all the keys once (in `lookup_windows_fls_keys_with_dtors`).
454 // 2. Go over them one by one, in order, skipping keys without dtors.
455 // 3. Fetch the value associated with the key.
456 // 4. If it is non-zero, call the registered dtor.
457 // 5. After the dtor is called, clear the key's value, setting it to zero.
458 // New keys registered during thread exit are ignored, but values set before the dtor is scheduled are visible.
459 // Keys without dtors will not be set to zero.
460 // [PflsCallbackFunction's docs]: https://learn.microsoft.com/en-us/windows/win32/api/winnt/nc-winnt-pfls_callback_function
461 // [`RtlProcessFlsData`]: https://github.com/wine-mirror/wine/blob/wine-11.0/dlls/ntdll/thread.c#L679
462
463 // We are done running the previous key's destructor, so set it's value to zero.
464 if let Some(last_key) = state.last_key.take() {
465 if let Some(TlsEntry { data, .. }) = this.machine.tls.keys.get_mut(&last_key) {
466 data.remove(&active_thread);
467 };
468 }
469
470 while let Some(key) = state.remaining_keys.pop_front() {
471 // Fetch dtor for this `key`.
472 // If the key doesn't have a dtor or does not exist any more, move on to the next key.
473 let Some(TlsEntry { data, dtor: Some(dtor) }) = this.machine.tls.keys.get(&key) else {
474 continue;
475 };
476
477 let (instance, span) = dtor.to_owned();
478
479 // If the key has no value in this thread, move on to the next key.
480 let Some(&ptr) = data.get(&active_thread) else { continue };
481
482 assert!(
483 ptr.to_target_usize(this).unwrap() != 0,
484 "TLS key's value can't be null (should be absent instead)"
485 );
486
487 trace!("Running TLS dtor {:?} on {:?} at {:?}", instance, ptr, active_thread);
488
489 // We'll clear this key's value next time we are called.
490 state.last_key = Some(key);
491
492 this.call_thread_root_function(
493 instance,
494 ExternAbi::System { unwind: false },
495 &[ImmTy::from_scalar(ptr, this.machine.layouts.mut_raw_ptr)],
496 None,
497 span,
498 )?;
499
500 return interp_ok(Poll::Pending);
501 }
502
503 // We are done scheduling all the keys.
504 interp_ok(Poll::Ready(()))
505 }
506}