blob: fa52b036ce948f8f81e1dfc7afa3d0f7967c0dbc [file] [log] [blame]
//! Implement thread-local storage.
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
use std::task::Poll;
use rustc_middle::ty;
use rustc_target::abi::{HasDataLayout, Size};
use rustc_target::spec::abi::Abi;
use crate::*;
pub type TlsKey = u128;
#[derive(Clone, Debug)]
pub struct TlsEntry<'tcx> {
/// The data for this key. None is used to represent NULL.
/// (We normalize this early to avoid having to do a NULL-ptr-test each time we access the data.)
data: BTreeMap<ThreadId, Scalar<Provenance>>,
dtor: Option<ty::Instance<'tcx>>,
}
#[derive(Default, Debug)]
struct RunningDtorState {
/// The last TlsKey used to retrieve a TLS destructor. `None` means that we
/// have not tried to retrieve a TLS destructor yet or that we already tried
/// all keys.
last_key: Option<TlsKey>,
}
#[derive(Debug)]
pub struct TlsData<'tcx> {
/// The Key to use for the next thread-local allocation.
next_key: TlsKey,
/// pthreads-style thread-local storage.
keys: BTreeMap<TlsKey, TlsEntry<'tcx>>,
/// A single per thread destructor of the thread local storage (that's how
/// things work on macOS) with a data argument.
macos_thread_dtors: BTreeMap<ThreadId, (ty::Instance<'tcx>, Scalar<Provenance>)>,
}
impl<'tcx> Default for TlsData<'tcx> {
fn default() -> Self {
TlsData {
next_key: 1, // start with 1 as we must not use 0 on Windows
keys: Default::default(),
macos_thread_dtors: Default::default(),
}
}
}
impl<'tcx> TlsData<'tcx> {
/// Generate a new TLS key with the given destructor.
/// `max_size` determines the integer size the key has to fit in.
#[allow(clippy::arithmetic_side_effects)]
pub fn create_tls_key(
&mut self,
dtor: Option<ty::Instance<'tcx>>,
max_size: Size,
) -> InterpResult<'tcx, TlsKey> {
let new_key = self.next_key;
self.next_key += 1;
self.keys.try_insert(new_key, TlsEntry { data: Default::default(), dtor }).unwrap();
trace!("New TLS key allocated: {} with dtor {:?}", new_key, dtor);
if max_size.bits() < 128 && new_key >= (1u128 << max_size.bits()) {
throw_unsup_format!("we ran out of TLS key space");
}
Ok(new_key)
}
pub fn delete_tls_key(&mut self, key: TlsKey) -> InterpResult<'tcx> {
match self.keys.remove(&key) {
Some(_) => {
trace!("TLS key {} removed", key);
Ok(())
}
None => throw_ub_format!("removing a nonexistent TLS key: {}", key),
}
}
pub fn load_tls(
&self,
key: TlsKey,
thread_id: ThreadId,
cx: &impl HasDataLayout,
) -> InterpResult<'tcx, Scalar<Provenance>> {
match self.keys.get(&key) {
Some(TlsEntry { data, .. }) => {
let value = data.get(&thread_id).copied();
trace!("TLS key {} for thread {:?} loaded: {:?}", key, thread_id, value);
Ok(value.unwrap_or_else(|| Scalar::null_ptr(cx)))
}
None => throw_ub_format!("loading from a non-existing TLS key: {}", key),
}
}
pub fn store_tls(
&mut self,
key: TlsKey,
thread_id: ThreadId,
new_data: Scalar<Provenance>,
cx: &impl HasDataLayout,
) -> InterpResult<'tcx> {
match self.keys.get_mut(&key) {
Some(TlsEntry { data, .. }) => {
if new_data.to_target_usize(cx)? != 0 {
trace!("TLS key {} for thread {:?} stored: {:?}", key, thread_id, new_data);
data.insert(thread_id, new_data);
} else {
trace!("TLS key {} for thread {:?} removed", key, thread_id);
data.remove(&thread_id);
}
Ok(())
}
None => throw_ub_format!("storing to a non-existing TLS key: {}", key),
}
}
/// Set the thread wide destructor of the thread local storage for the given
/// thread. This function is used to implement `_tlv_atexit` shim on MacOS.
///
/// Thread wide dtors are available only on MacOS. There is one destructor
/// per thread as can be guessed from the following comment in the
/// [`_tlv_atexit`
/// implementation](https://github.com/opensource-apple/dyld/blob/195030646877261f0c8c7ad8b001f52d6a26f514/src/threadLocalVariables.c#L389):
///
/// NOTE: this does not need locks because it only operates on current thread data
pub fn set_macos_thread_dtor(
&mut self,
thread: ThreadId,
dtor: ty::Instance<'tcx>,
data: Scalar<Provenance>,
) -> InterpResult<'tcx> {
if self.macos_thread_dtors.insert(thread, (dtor, data)).is_some() {
throw_unsup_format!(
"setting more than one thread local storage destructor for the same thread is not supported"
);
}
Ok(())
}
/// Returns a dtor, its argument and its index, if one is supposed to run.
/// `key` is the last dtors that was run; we return the *next* one after that.
///
/// An optional destructor function may be associated with each key value.
/// At thread exit, if a key value has a non-NULL destructor pointer,
/// and the thread has a non-NULL value associated with that key,
/// the value of the key is set to NULL, and then the function pointed
/// to is called with the previously associated value as its sole argument.
/// **The order of destructor calls is unspecified if more than one destructor
/// exists for a thread when it exits.**
///
/// If, after all the destructors have been called for all non-NULL values
/// with associated destructors, there are still some non-NULL values with
/// associated destructors, then the process is repeated.
/// If, after at least {PTHREAD_DESTRUCTOR_ITERATIONS} iterations of destructor
/// calls for outstanding non-NULL values, there are still some non-NULL values
/// with associated destructors, implementations may stop calling destructors,
/// or they may continue calling destructors until no non-NULL values with
/// associated destructors exist, even though this might result in an infinite loop.
fn fetch_tls_dtor(
&mut self,
key: Option<TlsKey>,
thread_id: ThreadId,
) -> Option<(ty::Instance<'tcx>, Scalar<Provenance>, TlsKey)> {
use std::ops::Bound::*;
let thread_local = &mut self.keys;
let start = match key {
Some(key) => Excluded(key),
None => Unbounded,
};
// We interpret the documentation above (taken from POSIX) as saying that we need to iterate
// over all keys and run each destructor at least once before running any destructor a 2nd
// time. That's why we have `key` to indicate how far we got in the current iteration. If we
// return `None`, `schedule_next_pthread_tls_dtor` will re-try with `ket` set to `None` to
// start the next round.
// TODO: In the future, we might consider randomizing destructor order, but we still have to
// uphold this requirement.
for (&key, TlsEntry { data, dtor }) in thread_local.range_mut((start, Unbounded)) {
match data.entry(thread_id) {
BTreeEntry::Occupied(entry) => {
if let Some(dtor) = dtor {
// Set TLS data to NULL, and call dtor with old value.
let data_scalar = entry.remove();
let ret = Some((*dtor, data_scalar, key));
return ret;
}
}
BTreeEntry::Vacant(_) => {}
}
}
None
}
/// Delete all TLS entries for the given thread. This function should be
/// called after all TLS destructors have already finished.
fn delete_all_thread_tls(&mut self, thread_id: ThreadId) {
for TlsEntry { data, .. } in self.keys.values_mut() {
data.remove(&thread_id);
}
}
}
impl VisitProvenance for TlsData<'_> {
fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
let TlsData { keys, macos_thread_dtors, next_key: _ } = self;
for scalar in keys.values().flat_map(|v| v.data.values()) {
scalar.visit_provenance(visit);
}
for (_, scalar) in macos_thread_dtors.values() {
scalar.visit_provenance(visit);
}
}
}
#[derive(Debug, Default)]
pub struct TlsDtorsState<'tcx>(TlsDtorsStatePriv<'tcx>);
#[derive(Debug, Default)]
enum TlsDtorsStatePriv<'tcx> {
#[default]
Init,
PthreadDtors(RunningDtorState),
/// For Windows Dtors, we store the list of functions that we still have to call.
/// These are functions from the magic `.CRT$XLB` linker section.
WindowsDtors(Vec<ImmTy<'tcx, Provenance>>),
Done,
}
impl<'tcx> TlsDtorsState<'tcx> {
pub fn on_stack_empty(
&mut self,
this: &mut MiriInterpCx<'tcx>,
) -> InterpResult<'tcx, Poll<()>> {
use TlsDtorsStatePriv::*;
let new_state = 'new_state: {
match &mut self.0 {
Init => {
match this.tcx.sess.target.os.as_ref() {
"macos" => {
// The macOS thread wide destructor runs "before any TLS slots get
// freed", so do that first.
this.schedule_macos_tls_dtor()?;
// When that destructor is done, go on with the pthread dtors.
break 'new_state PthreadDtors(Default::default());
}
_ if this.target_os_is_unix() => {
// All other Unixes directly jump to running the pthread dtors.
break 'new_state PthreadDtors(Default::default());
}
"windows" => {
// Determine which destructors to run.
let dtors = this.lookup_windows_tls_dtors()?;
// And move to the next state, that runs them.
break 'new_state WindowsDtors(dtors);
}
_ => {
// No TLS dtor support.
// FIXME: should we do something on wasi?
break 'new_state Done;
}
}
}
PthreadDtors(state) => {
match this.schedule_next_pthread_tls_dtor(state)? {
Poll::Pending => return Ok(Poll::Pending), // just keep going
Poll::Ready(()) => break 'new_state Done,
}
}
WindowsDtors(dtors) => {
if let Some(dtor) = dtors.pop() {
this.schedule_windows_tls_dtor(dtor)?;
return Ok(Poll::Pending); // we stay in this state (but `dtors` got shorter)
} else {
// No more destructors to run.
break 'new_state Done;
}
}
Done => {
this.machine.tls.delete_all_thread_tls(this.active_thread());
return Ok(Poll::Ready(()));
}
}
};
self.0 = new_state;
Ok(Poll::Pending)
}
}
impl<'tcx> EvalContextPrivExt<'tcx> for crate::MiriInterpCx<'tcx> {}
trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
/// Schedule TLS destructors for Windows.
/// On windows, TLS destructors are managed by std.
fn lookup_windows_tls_dtors(&mut self) -> InterpResult<'tcx, Vec<ImmTy<'tcx, Provenance>>> {
let this = self.eval_context_mut();
// Windows has a special magic linker section that is run on certain events.
// We don't support most of that, but just enough to make thread-local dtors in `std` work.
Ok(this.lookup_link_section(".CRT$XLB")?)
}
fn schedule_windows_tls_dtor(&mut self, dtor: ImmTy<'tcx, Provenance>) -> InterpResult<'tcx> {
let this = self.eval_context_mut();
let dtor = dtor.to_scalar().to_pointer(this)?;
let thread_callback = this.get_ptr_fn(dtor)?.as_instance()?;
// FIXME: Technically, the reason should be `DLL_PROCESS_DETACH` when the main thread exits
// but std treats both the same.
let reason = this.eval_windows("c", "DLL_THREAD_DETACH");
// The signature of this function is `unsafe extern "system" fn(h: c::LPVOID, dwReason: c::DWORD, pv: c::LPVOID)`.
// FIXME: `h` should be a handle to the current module and what `pv` should be is unknown
// but both are ignored by std.
this.call_function(
thread_callback,
Abi::System { unwind: false },
&[Scalar::null_ptr(this).into(), reason.into(), Scalar::null_ptr(this).into()],
None,
StackPopCleanup::Root { cleanup: true },
)?;
Ok(())
}
/// Schedule the MacOS thread destructor of the thread local storage to be
/// executed.
fn schedule_macos_tls_dtor(&mut self) -> InterpResult<'tcx> {
let this = self.eval_context_mut();
let thread_id = this.active_thread();
if let Some((instance, data)) = this.machine.tls.macos_thread_dtors.remove(&thread_id) {
trace!("Running macos dtor {:?} on {:?} at {:?}", instance, data, thread_id);
this.call_function(
instance,
Abi::C { unwind: false },
&[data.into()],
None,
StackPopCleanup::Root { cleanup: true },
)?;
}
Ok(())
}
/// Schedule a pthread TLS destructor. Returns `true` if found
/// a destructor to schedule, and `false` otherwise.
fn schedule_next_pthread_tls_dtor(
&mut self,
state: &mut RunningDtorState,
) -> InterpResult<'tcx, Poll<()>> {
let this = self.eval_context_mut();
let active_thread = this.active_thread();
// Fetch next dtor after `key`.
let dtor = match this.machine.tls.fetch_tls_dtor(state.last_key, active_thread) {
dtor @ Some(_) => dtor,
// We ran each dtor once, start over from the beginning.
None => this.machine.tls.fetch_tls_dtor(None, active_thread),
};
if let Some((instance, ptr, key)) = dtor {
state.last_key = Some(key);
trace!("Running TLS dtor {:?} on {:?} at {:?}", instance, ptr, active_thread);
assert!(
ptr.to_target_usize(this).unwrap() != 0,
"data can't be NULL when dtor is called!"
);
this.call_function(
instance,
Abi::C { unwind: false },
&[ptr.into()],
None,
StackPopCleanup::Root { cleanup: true },
)?;
return Ok(Poll::Pending);
}
Ok(Poll::Ready(()))
}
}