Repository navigation
Constants can contain references that are not Sync #49206
Copy link
Copy link
Open
Labels
A-const-evalArea: Constant evaluation, covers all const contexts (static, const fn, ...)Area: Constant evaluation, covers all const contexts (static, const fn, ...)A-constant-promotionArea: constant promotionArea: constant promotionC-bugCategory: This is a bug.Category: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-mediumMedium priorityMedium priorityS-bug-has-testStatus: This bug is tracked inside the repo by a `known-bug` test.Status: 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.Relevant to the compiler team, which will review and decide on the PR/issue.T-opsemRelevant to the opsem teamRelevant to the opsem teamT-typesRelevant to the types team, which will review and decide on the PR/issue.Relevant to the types team, which will review and decide on the PR/issue.
Description
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.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
A-const-evalArea: Constant evaluation, covers all const contexts (static, const fn, ...)Area: Constant evaluation, covers all const contexts (static, const fn, ...)A-constant-promotionArea: constant promotionArea: constant promotionC-bugCategory: This is a bug.Category: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-mediumMedium priorityMedium priorityS-bug-has-testStatus: This bug is tracked inside the repo by a `known-bug` test.Status: 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.Relevant to the compiler team, which will review and decide on the PR/issue.T-opsemRelevant to the opsem teamRelevant to the opsem teamT-typesRelevant to the types team, which will review and decide on the PR/issue.Relevant to the types team, which will review and decide on the PR/issue.
Type
Projects
- StatusShow more project fieldsopen/unblocked
Activity
hanna-kruppe commentedon Mar 20, 2018
cc @eddyb
eddyb commentedon Mar 23, 2018
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
constwith a reference in it, not likestatic. Actually,staticitself 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.
eddyb commentedon Mar 23, 2018
cc @nikomatsakis
hanna-kruppe commentedon Mar 23, 2018
It seems in pretty clear violation of the
Synccontract. 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-Syncdata being truly not shared across threads.Edit: Hmmm come to think of it, this is complicated by the fact that
Syncis an "at any given time thing", sinceMutexand the like can make thingsSync. Still seems bad to muddy the waters here.cc @RalfJung
nikomatsakis commentedon Mar 23, 2018
For better or worse (worse, I guess), this also builds:
RalfJung commentedon Mar 25, 2018
Yeah I think this is a problem.
I imagine some (rather silly) pseudo-code like this, based on @rkruppe's idea of using
Fooas a "token":Playground version
Given that
Foois non-Sync, which guarantees that all pointers to the same&Foolive 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 addressa. However, the code above could actually have exactly that outcome, withabeing the value ofF.EDIT: See here for further elaboration.
eddyb commentedon Mar 27, 2018
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-
Sendtypes in the compiler with specialconstvalues that happen to usable everywhere, regardless of thread-local state (e.g.Span'sDUMMY_SP)RalfJung commentedon Mar 27, 2018
I think my
Fooabove 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.That's a different thing. Even types that are not in general
Sendcan have elements that areSend-- just likeVechas an element that isCopy.RalfJung commentedon Mar 30, 2018
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 ofstaticapply, including the type having to beSync. 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.arielb1 commentedon Apr 19, 2018
With my current understanding of the semantics, there's nothing wrong with several different zero-sized values having the same address.
hanna-kruppe commentedon Apr 19, 2018
I think that's true, but this issue applies to non-ZSTs as well.
arielb1 commentedon Apr 19, 2018
@rkruppe
Sure. Missed that.
56 remaining items
LegionMammal978 commentedon Jan 13, 2024
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 fncall, then an external user cannot create a static-promoted instance of it, and this bug does not apply.I don't see how
&mut Foo→&Foois relevant here? There's no&mutreference created in this example. Instead, this seems closer to&binding Foo→&Foo. That is, const-eval is translatingconst F: &'static Foo = &Foo(0);into the moral equivalent of:Most bindings (
letbindings, argument bindings, pattern bindings, owned captures, etc.) are "owned" by a particular execution of a scope, which belongs to a particular thread. A&binding Tcan only be created inside this scope, so&binding T→&Tis 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,
staticbindings are the exception to this rule, being accessible by&bindingfrom any thread. Thus, since&binding T→&Tis still a safe operation, but&binding Thas no scope-ownership,&Tcannot imply any kind of thread-ownership in general. This is why we requireT: Syncto create astaticbinding. But with this bug, const-eval is allowed to create an internal&binding Tsafely accessible from any thread, breaking&T's thread-ownership that one would expect fromT: !Sync.RalfJung commentedon Jan 26, 2024
Sadly
const fncalls do get promoted in const/static initializer bodies. (Cc #80619)Otherwise, maybe there'd be some trick hiding here to rationalize the current behavior...
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).xnever actually gets mutated of course, but given that&mut T → &Tis possible, it is convenient to only have a single way of creating a shared reference, rather than two.LegionMammal978 commentedon Jan 26, 2024
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 Tno longer imply exclusivity just by its existence?RalfJung commentedon Jan 27, 2024
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.
lcnr commentedon Feb 14, 2026
RalfJung commentedon Feb 15, 2026
lcnr commentedon Feb 15, 2026
RalfJung commentedon Feb 16, 2026