Skip to content

Constants can contain references that are not Sync #49206

Open

Description

@jDomantas

This compiles (playground):

#![feature(negative_impls)]

#[derive(Debug)]
struct Foo {
    value: u32,
}

impl !std::marker::Sync for Foo {}

fn inspect() {
    let foo: &'static Foo = &Foo { value: 1 };
    println!(
        "I am in thread {:?}, address: {:p}",
        std::thread::current().id(),
        foo as *const Foo,
    );
}

fn main() {
    let handle = std::thread::spawn(inspect);
    inspect();
    handle.join().unwrap();
}

And prints this (addresses vary, but always the same):

I am in thread ThreadId(0), address: 0x5590409f7adc
I am in thread ThreadId(1), address: 0x5590409f7adc

Which shows that two threads get the same 'static reference to non-Sync struct.

The problem is that promotion to static does not check if the type is Sync, while doing it manually does (this does not compile):

static PROMOTED: Foo = Foo { value: 1 };
let foo = &PROMOTED;

Reading the RFC, it seems that the relevant condition is about containing UnsafeCell, but does not account for other !Sync types, which are also not supposed to be shared across treads.

Activity

  1. hanna-kruppe commented on Mar 20, 2018

    @hanna-kruppe
    Contributor
  2. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    C-bugCategory: This is a bug.
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    on Mar 21, 2018
  3. eddyb commented on Mar 23, 2018

    @eddyb
    Contributor

    If you can make the constant, and it's immutable, I'm not sure what you can do with it that's unsound - only unsafe code relying on it not being possible for one reason or another.

    Rvalue promotion is like const with a reference in it, not like static. Actually, static itself could be relaxed too when interior mutability is not involved.

    I'm tempted to close as "working as intended", unless there is more code to look at, which gets broken in one way or another by this capability.

  4. eddyb commented on Mar 23, 2018

    @eddyb
    Contributor
  5. hanna-kruppe commented on Mar 23, 2018

    @hanna-kruppe
    Contributor

    It seems in pretty clear violation of the Sync contract. While unsoundness seems unlikely in practice (you'd need a set up where the non-Sync values are used as "tokens" for something thread-local or thread-unsafe), at least in theory unsafe code should really be allowed to rely on non-Sync data being truly not shared across threads.

    Edit: Hmmm come to think of it, this is complicated by the fact that Sync is an "at any given time thing", since Mutex and the like can make things Sync. Still seems bad to muddy the waters here.

    cc @RalfJung

  6. nikomatsakis commented on Mar 23, 2018

    @nikomatsakis
    Contributor

    For better or worse (worse, I guess), this also builds:

    #![feature(optin_builtin_traits)]
    
    #[derive(Debug)]
    struct Foo {
        value: u32,
    }
    
    impl !std::marker::Sync for Foo {}
    
    const F: &'static Foo = &Foo { value: 1 };
    
    fn main() {
    }
  7. RalfJung commented on Mar 25, 2018

    @RalfJung
    Member

    Yeah I think this is a problem.

    I imagine some (rather silly) pseudo-code like this, based on @rkruppe's idea of using Foo as a "token":

    struct Event {
      address: usize,
      kind: EventKind
    }
    
    enum EventKind { Acquire, Release }
    
    // A global event registry thing
    static mut EVENT_LIST : Mutex<Vec<Event>> = Mutex::new(Vec::new());
    
    fn add_event(e: Event) {
      unsafe { EVENT_LIST.lock().push(e) }
    }
    
    fn get_events() -> Vec<Event> {
      unsafe { EVENT_LIST.lock().clone() }
    }
    
    // Now comes the bad code
    struct Foo(u32);
    
    impl !Sync for Foo {}
    
    impl Foo {
      fn acquire_release(&self) {
        add_event(Event { address: self as *const _ as usize, EventKind::Acquire });
        add_event(Event { address: self as *const _ as usize, EventKind::Release });
      }
    }
    
    const F: &'static Foo = &Foo(0);
    
    fn main() {
      std::thread::spawn(|| F.acquire_release());
      F.acquire_release();
    }

    Playground version

    Given that Foo is non-Sync, which guarantees that all pointers to the same &Foo live in the same thread, I think unsafe code should be allowed to rely (for UB) on the fact that the event list will never, ever contain something like (a, Acquire) (a, Acquire) (a, Release) (a, Release) for any address a. However, the code above could actually have exactly that outcome, with a being the value of F.

    EDIT: See here for further elaboration.

  8. eddyb commented on Mar 27, 2018

    @eddyb
    Contributor

    Given that Foo is non-Sync, which guarantees that all pointers to the same &Foo live in the same thread

    Do we guarantee this anywhere? My expectation is that the fields of such an abstraction must be private, and that means you can only create the problematic global references in the same module.
    And you might want those references, for a small set of special values that are valid cross-thread.

    As another example, there are non-Send types in the compiler with special const values that happen to usable everywhere, regardless of thread-local state (e.g. Span's DUMMY_SP)

  9. RalfJung commented on Mar 27, 2018

    @RalfJung
    Member

    Do we guarantee this anywhere? My expectation is that the fields of such an abstraction must be private, and that means you can only create the problematic global references in the same module.

    I think my Foo above is a counterexample. The only field does not matter at all, it can be public. It could also be entirely removed, just making it a unit struct.

    As another example, there are non-Send types in the compiler with special const values that happen to usable everywhere, regardless of thread-local state (e.g. Span's DUMMY_SP)

    That's a different thing. Even types that are not in general Send can have elements that are Send -- just like Vec has an element that is Copy.

  10. RalfJung commented on Mar 30, 2018

    @RalfJung
    Member

    So the conclusion from the unsafe code guidelines meeting yesterday (as far as I understood) is that we want to disallow this. Rvalue promotion should be thought of as implicitly generating additional statics, but with loser guarantees about address uniqueness. All restrictions of static apply, including the type having to be Sync. The open question is if we can do this breaking change, it is not clear how much code relies on this. There will be a crater run.

  11. arielb1 commented on Apr 19, 2018

    @arielb1
    Contributor

    With my current understanding of the semantics, there's nothing wrong with several different zero-sized values having the same address.

    #![feature(optin_builtin_traits)]
    
    struct NotSync;
    impl !Sync for NotSync {}
    
    fn main() {
        let mut u = NotSync;
        let mut v = NotSync;
        println!("{:?}", &mut u as *mut NotSync);
        println!("{:?}", &mut v as *mut NotSync);
    }
  12. hanna-kruppe commented on Apr 19, 2018

    @hanna-kruppe
    Contributor

    I think that's true, but this issue applies to non-ZSTs as well.

  13. arielb1 commented on Apr 19, 2018

    @arielb1
    Contributor

    @rkruppe

    Sure. Missed that.

  14. 56 remaining items

  15. LegionMammal978 commented on Jan 13, 2024

    @LegionMammal978
    Contributor

    Oh you are right, somehow I thought the value of the field was relevant. But the field only exists to make this type non-zero-sized.

    Ah, I thought you were referring to the rules for infallible promotion. If a type has public fields, then an external user can create a static-promoted instance of it. But if a type has private fields and can only be created with a pub const fn call, then an external user cannot create a static-promoted instance of it, and this bug does not apply.

    Okay then, what happens when the &'static Foo is created? Const-eval creates a Foo somewhere and then needs to use the "&mut Foo to &Foo" axiom to justify creating the shared reference... but that axiom is always executed in some thread!

    I don't see how &mut Foo → &Foo is relevant here? There's no &mut reference created in this example. Instead, this seems closer to &binding Foo → &Foo. That is, const-eval is translating const F: &'static Foo = &Foo(0); into the moral equivalent of:

    const F: &'static Foo = {
        static FOO: Foo = Foo(0); // doesn't actually compile, since `Foo: !Sync`
        &FOO // also doesn't actually compile, since consts can't refer to statics
    };

    Most bindings (let bindings, argument bindings, pattern bindings, owned captures, etc.) are "owned" by a particular execution of a scope, which belongs to a particular thread. A &binding T can only be created inside this scope, so &binding T → &T is always safe, trading the scope-ownership for the thread-ownership. (Here, I'm considering borrowed closure/coroutine captures as just syntax sugar for references created when the closure/coroutine instance is created.)

    However, static bindings are the exception to this rule, being accessible by &binding from any thread. Thus, since &binding T → &T is still a safe operation, but &binding T has no scope-ownership, &T cannot imply any kind of thread-ownership in general. This is why we require T: Sync to create a static binding. But with this bug, const-eval is allowed to create an internal &binding T safely accessible from any thread, breaking &T's thread-ownership that one would expect from T: !Sync.

  16. RalfJung commented on Jan 26, 2024

    @RalfJung
    Member

    Ah, I thought you were referring to the rules for infallible promotion. If a type has public fields, then an external user can create a static-promoted instance of it. But if a type has private fields and can only be created with a pub const fn call, then an external user cannot create a static-promoted instance of it, and this bug does not apply.

    Sadly const fn calls do get promoted in const/static initializer bodies. (Cc #80619)

    Otherwise, maybe there'd be some trick hiding here to rationalize the current behavior...

    I don't see how &mut Foo → &Foo is relevant here?

    It is relevant insofar as in my model (RustBelt), that is the only way to create a shared reference. It always starts with a mutable reference that is then used to initialize "sharing for a given lifetime". The usual problem when defining an invariant is to ensure that the initial state satisfies the invariant; in this case, that lemma represents the initial state.

    So in my model, when you have let x = ...; &x, what logically happens is &(*&mut x). x never actually gets mutated of course, but given that &mut T → &T is possible, it is convenient to only have a single way of creating a shared reference, rather than two.

  17. LegionMammal978 commented on Jan 26, 2024

    @LegionMammal978
    Contributor

    So in my model, when you have let x = ...; &x, what logically happens is &(*&mut x). x never actually gets mutated of course, but given that &mut T → &T is possible, it is convenient to only have a single way of creating a shared reference, rather than two.

    Given that you can take multiple shared references to a binding at the same time, isn't that only a valid transformation given Tree Borrows–like mutable references, which aren't active until written to? And in that case, won't &mut T no longer imply exclusivity just by its existence?

  18. RalfJung commented on Jan 27, 2024

    @RalfJung
    Member

    The idea is that these multiple shared references all derive from a single root reference, even if that reference may not be visible in the source. The moment this transition happens is purely a "ghost step" in the proof; it doesn't have to coincide with retagging in Stacked/Tree borrows.

  19. moved this from unknown to future compat lint in T-types unsound issueson Jan 6, 2025
  20. moved this from future compat lint to unknown in T-types unsound issueson Jan 6, 2025
  21. lcnr commented on Feb 14, 2026

    @lcnr
  22. RalfJung commented on Feb 15, 2026

    @RalfJung
  23. lcnr commented on Feb 15, 2026

    @lcnr
  24. RalfJung commented on Feb 16, 2026

    @RalfJung
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-const-evalArea: Constant evaluation, covers all const contexts (static, const fn, ...)A-constant-promotionArea: constant promotionC-bugCategory: This is a bug.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.T-opsemRelevant to the opsem teamT-typesRelevant to the types team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions