-
-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Doc: MaybeUninit::assume_init_read Safety: warn on thread safety
#148398
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
base: main
Are you sure you want to change the base?
Doc: MaybeUninit::assume_init_read Safety: warn on thread safety
#148398
Conversation
This comment has been minimized.
This comment has been minimized.
Due to `impl<T: Sync> Sync for MaybeUninit<T>`, `MaybeUninit::assume_init_read` is prone to the illegal use transferring ownership of `!Send + Sync` value across threads. This commit adds a warning on this thread safety problem to the method's Safety section. This commit also adds two code examples related to this problem.
bf0ca0c to
1033c71
Compare
MaybeUninit::assume_init_read Safety: warn on thread safetyMaybeUninit::assume_init_read Safety: warn on thread safety (fix of #148083)
MaybeUninit::assume_init_read Safety: warn on thread safety (fix of #148083)MaybeUninit::assume_init_read Safety: warn on thread safety
|
@rustbot label A-docs I-unsound |
|
@rustbot label -I-unsound -I-prioritize |
|
Is it worth adding the following, or something similar?
|
|
@Amanieu Can I re-set you as the reviewer? I know you all have loads of work to do, but it's been three weeks and I'd like to know if re-assigning helps to make review process easier. |
| /// the thread safety of the transfer. Note that `MaybeUninit<T>` is [`Sync`] if `T` | ||
| /// is merely [`Sync`]; therefore, safely obtaining `&MaybeUninit<T>` does *not* ensure | ||
| /// the said thread safety. If `T` is [`Send`], the thread safety is | ||
| /// guaranteed; otherwise, you will need to provide type-specific reasoning to | ||
| /// prove that the transfer and your usage of the value `T` are actually thread-safe. | ||
| /// If your type's safe API relies on this function, you may need to manually | ||
| /// implement `!Send`, `!Sync`, [`Send`], and/or [`Sync`] to *tighten* the conditions | ||
| /// for the type to be [`Send`] and/or [`Sync`]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The text after "Note that MaybeUninit<T> is Sync" gets a bit confusing because it tries to explore all the combinations of [!]Send and [!]Sync. Instead this should only focus on the problematic combination which is T: !Send + Sync, where you need to manually ensure that you are maintaining the Send invariants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took time, and I finally came up with something worth consideration:
the thread safety of the transfer. The main problematic case is
T: Sync + !Send. In this situation,T: SyncgivesMaybeUninit<T>: Sync, so you can freely share&MaybeUninit<T>across threads through safe operations. However, actually reading outTwithassume_init_readis not guaranteed to be thread-safe becauseTis notSend. In this case, the type system no longer enforces the usual thread-safety guarantees, so you must rely on your own reasoning that this transfer and the subsequent uses ofTare thread-safe.If your type's safe API can call this method on
Sync + !Sendtypes, note that keeping its auto-implementations ofSend/Syncmay be unsound. You might need to manually implementSyncand/orSendto ensure that any cross-thread ownership transfer ofTthrough your type requiresT: Send. Generally, safe cross-thread ownership transfer ofTrequiresT: Send, regardless of whetherT: Syncor whetherMaybeUninit<T>: Sync.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the wording I came up with. Let me know your thoughts.
Since this function does not require
T: Send, you also need to carefully consider thread safety. If you call this function from a different thread than the one that holds theMaybeUninit<T>, it logically constitutes a cross-thread ownership transfer of the contained valueT. This can be problematic whenT: Sync + !SendbecauseSyncallows&MaybeUninit<T>to be safely shared across threads. In this case, the type system no longer enforces the usual thread-safety guarantees, so you must rely on your own reasoning that this transfer and the subsequent uses ofTare thread-safe.If a safe API can result in this function being called on
Sync + !Sendtypes, then keeping its auto-implementations ofSend/Syncmay be unsound. To avoid this, you can manually addT: Sendbounds to the callers of this function. Generally, safe cross-thread ownership transfer ofTrequiresT: Send, regardless of whetherT: Syncor whetherMaybeUninit<T>: Sync.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, the type system no...
The first paragraph, as a whole, seems like a wonderful simplification to me. Since I love verboseness, I might add:
In this case, for (as/since)
T: !Send, the type system no longer enforces...
However I guess it depends on preference. Brevity might win. What do you think?
On the second paragraph... hmm... to me it is puzzling. It required back-tracking when I read it. I was thinking of, like,
pub mod take_once {
use core::mem::{self, MaybeUninit};
use core::sync::atomic::{self, AtomicBool};
/// lock-free Mutex<Option<T>>, but you can only take ownership of the inner value;
/// you can't write, insert or borrow anything that's inside.
pub struct TakeOnce<T> {
alive: AtomicBool,
inner: MaybeUninit<T>,
}
impl<T> TakeOnce<T> {
pub fn new(t: T) -> Self {
Self {
alive: AtomicBool::new(true),
inner: MaybeUninit::new(t),
}
}
pub fn take(&self) -> Option<T> {
if self.alive.swap(false, atomic::Ordering::Relaxed) {
unsafe { Some(self.inner.assume_init_read()) }
} else {
None
}
}
}
impl<T> Drop for TakeOnce<T> {
fn drop(&mut self) {
if mem::replace(self.alive.get_mut(), false) {
unsafe {
self.inner.assume_init_drop();
}
}
}
}
// The following impl is MANDATORY: without this, `self.take()` is unsound
unsafe impl<T: Send> Sync for TakeOnce<T> {}
}I meant:
If your type's (
TakeOnce's) safe API calls this method onSync+!Sendtypes (T), then keeping its (TakeOnce's) auto-implementations ofSend/Syncmay be unsound.
On the other hand, I failed to see what you meant at glance.
If a safe API can result in this function being called on
Sync + !Sendtypes, then keeping its auto-implementations ofSend/Syncmay be unsound.
What do you mean by "its auto implementations"? I assume "its" points to the singular "a safe API", but "a safe API" can be a method of a type or a function in the wild... what did you intend that would have some implementations of Send/Sync?
|
r? Amanieu |
* Add a introductory sentence to the thread safety paragraph in Safety section * Clarify that a bad usage example is actually library UB
Fixes #148083.
Due to
impl<T: Sync> Sync for MaybeUninit<T>,MaybeUninit::assume_init_readis prone to the illegal use transferring ownership of!Send + Syncvalue across threads. This PR adds a warning on this thread safety problem to the method's Safety section. This PR also adds two code examples related to this problem.On the bad case code example: I wish I could use something better than
MutexGuard<'_, u32>, because I wasn't able to produce Miri-detectable UB.core::sync::Exclusive<&Cell<u32>>would be nice, but it's unstable.