-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Constants can contain references that are not Sync #49206
Comments
cc @eddyb |
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 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. |
It seems in pretty clear violation of the Edit: Hmmm come to think of it, this is complicated by the fact that cc @RalfJung |
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() {
} |
Yeah I think this is a problem. I imagine some (rather silly) pseudo-code like this, based on @rkruppe's idea of using 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();
} Given that EDIT: See here for further elaboration. |
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. As another example, there are non- |
I think my
That's a different thing. Even types that are not in general |
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 |
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);
} |
I think that's true, but this issue applies to non-ZSTs as well. |
Sure. Missed that. |
Right, this is not at all about addresses. Even ZSTs may not be duplicated as that may violate uniqueness guarantees, also see rust-lang/rust-memory-model#44. I see sending ZSTs across thread boundaries as similar. |
(unrelatedly, I made a comment somewhere else: #53851 (comment)) @RalfJung You would probably copy Note that it's slightly harder than the check for |
@eddyb Trying that now, let's see if that works. |
@eddyb not sure what is going on, but it doesn't work... see RalfJung@623fa51. The newly added test fails to fail to compile (as in, it compiles). How can that be? |
@RalfJung The codepaths you changed are for unknown values of those types. You also need to handle ADT constructors, which |
Actually promotion is far from the only problem as @eddyb pointed out... struct Bar(i32);
impl !Sync for Bar {}
const C: &'static Bar = &Bar(40);
fn main() {} We can have consts with non- |
Yeah, I'm surprised we didn't bring that up, since it's how the RFC defines rvalue promotion. I believe the only reason That is, we were worried about synchronizing mutation, not address equality (which we treated as generally unguaranteed). |
I think The reason promotion also cares about |
It has nothing to do with merging, and everything to do with not changing runtime semantics. You can only be sure the data isn't written to if you have |
I think you are saying the same thing as me. Runtime semantics get changed when two allocations that start having the same content get merged (deduplicated) and one gets written to. |
I have a proposed fix at #54424 |
WIP: do not borrow non-Sync data in constants We cannot share that data across threads. non-Sync is as bad as non-Freeze in that regard. This is currently WIP because it ignores a test that is broken by #54419. But it is good enough ti get crater going. Fixes #49206. Cc @eddyb @nikomatsakis
Marked p-medium as per our prioritisation discussion |
This is currently marked #[derive(Debug)]
struct Foo {
value: u32,
}
// stable negative impl trick from https://crates.io/crates/negative-impl
// see https://github.com/taiki-e/pin-project/issues/102#issuecomment-540472282 for details.
struct Wrapper<'a, T>(::std::marker::PhantomData<&'a ()>, T);
unsafe impl<T> Sync for Wrapper<'_, T> where T: Sync {}
unsafe impl<'a> std::marker::Sync for Foo where Wrapper<'a, *const ()>: Sync {}
fn _assert_sync<T: Sync>() {}
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() {
// _assert_sync::<Foo>(); // uncomment this causes compile error "`*const ()` cannot be shared between threads safely"
let handle = std::thread::spawn(inspect);
inspect();
handle.join().unwrap();
} @rustbot label: -requires-nightly |
Now that it's been revealed that this is possible (if roundabout) on stable, should the priority on this be increased? What would it take to at least begin emitting warnings here, to prevent new code from taking advantage of this and making it even more painful to fix in the future? |
Is this something that could be tied to editions? If so, a timeline like this might be reasonable:
The latest crater run from Ralf's PR #54424 indicated 18 regressions, but that's 4 year out of date at this point so occurrences could have gone either way. |
It's not even that roundabout, see #67601
```
pub const FOO: &'static *const u32 = &(&BAR as _);
pub const BAR: u32 = 0;
```
This is part of the broad set of questions around observing pointer identity of consts. At least so far the only way I know of to observe this issue is to compare addresses. Basically we should have a lot of instances of this !Sync (but Freeze) type, one for each use, but they all get deduplicated. Maybe we just have to find a way to rationalize this... I don't have a lot of confidence that we can land a fix here.
Making it a hard error only on sone editions does not really help: when I write a !Sync type I still have to consider users on old editions, and have to ensure my type is sound for them.
Is there a known example in the wild of a crate that makes pointer identity arguments for !Sync types, a type that is broken by this issue?
|
…unsound-issues, r=jackh726 Add `known-bug` tests for 11 unsound issues r? `@jackh726` Should tests for other issues be in separate PRs? Thanks. Edit: Partially addresses rust-lang#105107. This PR adds `known-bug` tests for 11 unsound issues: - rust-lang#25860 - rust-lang#49206 - rust-lang#57893 - rust-lang#84366 - rust-lang#84533 - rust-lang#84591 - rust-lang#85099 - rust-lang#98117 - rust-lang#100041 - rust-lang#100051 - rust-lang#104005
…unsound-issues, r=jackh726 Add `known-bug` tests for 11 unsound issues r? ``@jackh726`` Should tests for other issues be in separate PRs? Thanks. Edit: Partially addresses rust-lang#105107. This PR adds `known-bug` tests for 11 unsound issues: - rust-lang#25860 - rust-lang#49206 - rust-lang#57893 - rust-lang#84366 - rust-lang#84533 - rust-lang#84591 - rust-lang#85099 - rust-lang#98117 - rust-lang#100041 - rust-lang#100051 - rust-lang#104005
…unsound-issues, r=jackh726 Add `known-bug` tests for 11 unsound issues r? `@jackh726` Should tests for other issues be in separate PRs? Thanks. Edit: Partially addresses rust-lang#105107. This PR adds `known-bug` tests for 11 unsound issues: - rust-lang#25860 - rust-lang#49206 - rust-lang#57893 - rust-lang#84366 - rust-lang#84533 - rust-lang#84591 - rust-lang#85099 - rust-lang#98117 - rust-lang#100041 - rust-lang#100051 - rust-lang#104005
…unsound-issues, r=jackh726 Add `known-bug` tests for 11 unsound issues r? ``@jackh726`` Should tests for other issues be in separate PRs? Thanks. Edit: Partially addresses rust-lang#105107. This PR adds `known-bug` tests for 11 unsound issues: - rust-lang#25860 - rust-lang#49206 - rust-lang#57893 - rust-lang#84366 - rust-lang#84533 - rust-lang#84591 - rust-lang#85099 - rust-lang#98117 - rust-lang#100041 - rust-lang#100051 - rust-lang#104005
…unsound-issues, r=jackh726 Add `known-bug` tests for 11 unsound issues r? `@jackh726` Should tests for other issues be in separate PRs? Thanks. Edit: Partially addresses rust-lang#105107. This PR adds `known-bug` tests for 11 unsound issues: - rust-lang#25860 - rust-lang#49206 - rust-lang#57893 - rust-lang#84366 - rust-lang#84533 - rust-lang#84591 - rust-lang#85099 - rust-lang#98117 - rust-lang#100041 - rust-lang#100051 - rust-lang#104005
…unsound-issues, r=jackh726 Add `known-bug` tests for 11 unsound issues r? ``@jackh726`` Should tests for other issues be in separate PRs? Thanks. Edit: Partially addresses rust-lang#105107. This PR adds `known-bug` tests for 11 unsound issues: - rust-lang#25860 - rust-lang#49206 - rust-lang#57893 - rust-lang#84366 - rust-lang#84533 - rust-lang#84591 - rust-lang#85099 - rust-lang#98117 - rust-lang#100041 - rust-lang#100051 - rust-lang#104005
Another approach to solving this problem would be trying to rationalize it -- what are the proof obligations for unsafe code authors to ensure that this rustc behavior does not cause unsoundness? If we change my example here to cause actual UB from these shared I feel like there are two ingredients to this:
Basically: I think I am less worried about this issue now and I am not even sure any more if it is a soundness issue. However, I am worried how it might interact with CTFE heap allocations. |
The way I like to think about it is that "all pointers to the same pub struct Wrapper(pub *const i32);
unsafe impl Sync for Wrapper {} would be unsound, since Instead, the actual invariant is, "once a
Conversely, if any of these responsibilities are not upheld, then there's no automatic UB or unsoundness: it just means that the object can no longer be treated as only being accessible from one thread. For instance, if you receive a The big question is, if a span of code is able to safely construct an object using language features, then should it be able to safely directly access it from multiple threads? If so, then an interface that wants to uphold responsibility 1 cannot allow its objects to be publicly constructed. If not, then such an interface can allow its objects to be publicly constructed. The importance of this issue is that this is the only language 'feature' that would allow safe direct access to the same object from multiple threads, without going through references. (The confusion here is that this is more about responsibility 1 than responsibility 2. We aren't really copying around references here, so much as creating multiple references directly, given access to the same underlying object from multiple threads.) In @RalfJung's example, I don't think it's necessarily true that "that can only be a sound API if |
Also, I'd like to note that there's a big class of #![feature(negative_impls)]
use std::{mem, ptr::addr_of};
pub struct CellWrapper<'a, T>(pub &'a mut T);
impl<T> !Sync for CellWrapper<'_, T> {}
impl<T> CellWrapper<'_, T> {
pub fn set(&self, val: T) {
drop({
// SAFETY: Since `Self: !Sync` and we don't unsafely share `self`,
// `self.0` is unique for the duration of this block.
let r: &mut T = unsafe { addr_of!(self.0).read() };
mem::replace(r, val)
});
}
// And so on for the rest of the `Cell<T>` API.
} Clearly, allowing shared references to the same |
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. (Here's an actually compiling version of the example.) So... the safety invariant for shared
The axiom that from Okay then, what happens when the Absent that, we need the concept of whether a value is valid in "no thread", i.e. the safety invariant is predicated not on a Ugh, this is not very satisfying. :/ |
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
I don't see how 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 ( However, |
Sadly 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 |
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 |
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. |
This compiles (playground):
And prints this (addresses vary, but always the same):
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):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.The text was updated successfully, but these errors were encountered: