Why thread_local! statics can only be accessed inside `with`

When instantiating global variables with thread_local! macro values end up wrapped in LocalKey struct which gives access to a data through with function.
However, if I understand correctly, if the trait doesn't implement Sync, then taking the value out of with is safe, or is it not?

Here is an example:

Found this answer based on which !Send also needs to be specified, so that you couldn't share the data with another thread as well.

It is unsound to take a &'static reference to a thread-local value, even if it is !Sync, because the value is dropped when the thread terminates, and while the destructor of a different thread-local is running, the reference could be used after its referent has been freed.

1 Like

It's not okay if the thread local API gives out static references to the contents, because they could be moved to another thread and then used after the thread exits. If that happens you have a use after free.

And using a scope like how with works is the only way for it to not hand out a static reference. The scope defines the lifetime of the reference, and we know that the lifetime is not too long as the thread can't exit during the scope.

But this does not compile:

Just thinking about on what should I write to break this example.

Sorry, I didn't understand this example. The reference can only be used in a single thread, implied by !Sync, how can another thread get a reference to this value then?

This does.

(And even if it was non-Sync all the way down, the other problems like access after destruction still apply.)

1 Like

Thanks!

the other problems like access after destruction still apply

Can you please provide an example on this (or maybe describe how it can be done, if the example will be involved)? Because in my context Wrapper contains a pointer to which you cannot get access, and all functions on the Wrapper return owned values. So it is non-Sync all the way down.

The problem is simply that thread-local "statics" aren't actually static. They only live as long as the creating thread. So if:

  1. You declare some global &'static T
  2. You then spawn a thread and create a local
  3. You then obtain a &'static T reference from it
  4. Which you assign to the global
  5. Then the thread terminates
  6. Then you observe the newly-set, now-dangling reference in the main thread

then you have instant Undefined Behavior.

Sorry if I sound like a broken record, but steps 1 and 3 are impossible, because T: !Sync.

Something like...

#[derive(Debug)]
struct Snoop<T: std::fmt::Debug>(T);
impl<T: std::fmt::Debug> Drop for Snoop<T> {
    fn drop(&mut self) {
        println!("{self:?}");
    }
}

thread_local! {
    pub static CACHE: LazyCell<Snoop<&'static Wrapper>> = LazyCell::new(|| {
        Snoop(get_local_wrapper())
    })
}

You don't know which destructor runs first when the thread ends.

3 Likes

Thanks! Was able to cause the example to crash. (miri complains as well in case it will not reproduce)

Says who? You absolutely can put Sync types into a thread_local!.

An API is sound not if you can use it correctly, but if you can't use it incorrectly.

Demo: this crashes.

I’m not sure I understand what you mean here, in the context of OP’s question. They were talking about a !Sync type. Perhaps you can give a code example?

If T: !Sync, then you cannot put a &'static T into a global variable. And if your mention of thread_local was supposed to show some type of “global” that supports this, anyways, then your last step of “observe the newly-set, now-dangling reference in the main thread” won’t work, because you cannot observe the value set to a thread_local on one thread in another thread.


Edit: Bit of a race condition… you did add a code example now. However it features a thread_local of type String which is not a !Sync type (mind the exclamation mark).

Of course @quinedot in the meantime already posted the relevant example that does show unsoundness after all, but it is somewhat different.

1 Like

But how does it help the API of LocalKey? Unless I'm mistaken, it's not possible to restrict the set of types that work with LocalKey to !Sync types only. (And at the very least, the current API does not do that at the moment.)

Therefore, the API must anticipate being used with a type to which references cross thread boundaries.

No, it wouldn’t help the API at all indeed. You can’t ask for !Sync in a function signature, and a !Sync type can contain Sync types, anyways.

I think OP’s quest for understanding the nuances might have gone beyond just the possibilities of alternative API design, but only in terms of API design, you’re right, the !Sync argument doesn’t help much at all.

Even without a general API, one could still use unsafe for a concrete known-!Sync type[1] and wonder if that’s sound; which is kind-of what OP did here.


  1. maybe even with no public Sync data that can be re-borrowed from it, either ↩︎

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.