Skip to content

Keeping references to #[thread_local] statics is allowed across yields. #49682

Open
@eddyb

Description

@eddyb
Member

This does not affect stabilization of async fn unless #[thread_local] is also stabilized

Try on playground:

#![feature(generators, generator_trait, thread_local)]

use std::ops::Generator;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;

#[thread_local]
static FOO: AtomicUsize = AtomicUsize::new(0);

fn foo() -> impl Generator<Yield = (String, usize), Return = ()> {
    static || {
        let x = &FOO;
        loop {
            let s = format!("{:p}", x);
            yield (s, x.fetch_add(1, Ordering::SeqCst));
        }
    }
}

fn main() {
    unsafe {
        let mut g = thread::spawn(|| {
            let mut g = foo();
            println!("&FOO = {:p}; resume() = {:?}", &FOO, g.resume());
            g
        }).join().unwrap();
        println!("&FOO = {:p}; resume() = {:?}", &FOO, g.resume());
        thread::spawn(move || {
            println!("&FOO = {:p}; resume() = {:?}", &FOO, g.resume());
        }).join().unwrap();
    }
}

Sample output:

&FOO = 0x7f48f9bff688; resume() = Yielded(("0x7f48f9bff688", 0))
&FOO = 0x7f48fafd37c8; resume() = Yielded(("0x7f48f9bff688", 1))
&FOO = 0x7f48f9bff688; resume() = Yielded(("0x7f48f9bff688", 0))

You can see by the pointer addresses and values inside FOO that the same location was reused for the second child thread (it's a bit harder to show a crash) - this is clearly an use-after-free.
If we had in-language async, the same problem could be demonstrated using those.

In non-generator functions, such references have function-local lifetimes and cannot escape.
With the stable thread_local! from libstd, user code gets access to the reference in a (non-generator/async) closure, which also doesn't allow escaping the reference.

cc @alexcrichton @withoutboats @Zoxc

Activity

added
T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
on Apr 5, 2018
comex

comex commented on Apr 5, 2018

@comex
Contributor

Am I right in assuming the same issue applies to implementations of thread locals in external crates, which aren't limited to unstable? e.g. https://amanieu.github.io/thread_local-rs/thread_local/index.html

edit: I'm wrong. From that page:

Per-thread objects are not destroyed when a thread exits. Instead, objects are only destroyed when the ThreadLocal containing them is destroyed.

Otherwise the API would be blatantly unsound even without generators.

edit2: ignore me; I misunderstood the scope of the problem. For the record, it doesn't apply to thread_local!, or anything else that uses a callback.

nikomatsakis

nikomatsakis commented on Apr 12, 2018

@nikomatsakis
Contributor

triage: P-medium

Should fix, not urgent.

added
C-enhancementCategory: An issue proposing an enhancement or a PR with one.
on May 8, 2018
bstrie

bstrie commented on Feb 4, 2020

@bstrie
Contributor

If we had in-language async, the same problem could be demonstrated using those.

Now that async/await is stable, can anyone reproduce this bug using only those? I gave it a whack (playground here) and got the following error message:

error[E0712]: thread-local variable borrowed past end of function
  --> src/main.rs:10:5
   |
10 |     &FOO
   |     ^^^^ thread-local variables cannot be borrowed beyond the end of the function
11 | }
   | - end of enclosing function is here

...which suggests at least some degree of mitigation is already implemented, but it's possible that my translation (which obviously can't yield directly) did not represent the spirit of the original bug.

10 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-borrow-checkerArea: The borrow checkerA-coroutinesArea: CoroutinesC-enhancementCategory: An issue proposing an enhancement or a PR with one.F-coroutines`#![feature(coroutines)]`F-thread_local`#![feature(thread_local)]`I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-mediumMedium priorityS-bug-has-testStatus: This bug is tracked inside the repo by a `known-bug` test.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.requires-nightlyThis issue requires a nightly compiler in some way.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @Zoxc@jkordish@comex@eddyb@nikomatsakis

        Issue actions

          Keeping references to #[thread_local] statics is allowed across yields. · Issue #49682 · rust-lang/rust