rust-lang / rust-lang/rust

Add `MaybeUninit::assume_init_read` Safety constraint: it can easily break `!Send` invariant

Open
#148,083 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-docs I-unsound P-medium T-libs
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Location (URL)

https://doc.rust-lang.org/std/mem/union.MaybeUninit.html#method.assume_init_read

Summary

MaybeUninit::<T>::assume_init_read, together with impl<T: Sync> Sync for MaybeUninit<T>, can be (mis)used to break the Send invariant: when T: Sync, one can share &MaybeUninit<T> across threads (since &MaybeUninit<T>: Send) and then assume_init_read a fresh owned T on another thread. If T: Sync + !Send, this constitutes a transfer of ownership of a !Send value between threads, which violates Send's safety contract and can lead to undefined behavior (such as data races). The current Safety section does not mention this class of misuse.

Concretely, consider the following code (Playground):

pub mod mover {
    use core::mem::MaybeUninit;
    pub struct Mover<'a, T: 'a>(&'a MaybeUninit<T>);

    impl<'a, T: 'a> Mover<'a, T> {
        /// Put a value in `Mover`.
        pub fn new(t: T) -> Self {
            Mover(Box::leak(Box::new(MaybeUninit::new(t))))
        }
        /// Take an inner value out.
        pub fn into_inner(self) -> T {
            unsafe {
                // SAFETY: self.0 is initialized via new().
                // self is destructed, so no duplication will occur.
                // HOWEVER: this allows us to `!Send` move across threads.
                self.0.assume_init_read()
            }
        }
    }
}

use mover::Mover;
use std::thread;

// Compiles because `&MaybeUninit<T>` is `Send` when `T: Sync`.
// `Mover<'_, T>` is thus `Send` under the same condition.
fn horror<T, U, F: FnOnce(T) -> U>(t: T, f: F) -> thread::JoinHandle<U>
where
    T: Sync + 'static,
    U: Send + 'static,
    F: Send + 'static,
{
    let m: Mover<'static, T> = Mover::new(t);
    thread::spawn(|| f(m.into_inner()))
}

This does not violate the two explicit constraints in the current documentation (the value is initialized, and not duplicated), yet it breaks the Send invariant for T, sending T:Sync + !Send across threads. Violating Send/Sync invariants may cause undefined behavior such as data races.

Request: Extend the Safety section with wording like:

In addition, the caller must uphold the thread-safety auto-trait invariants of T. In particular, do not use this function to move an instance of a !Send type to another thread (for example, by sharing &MaybeUninit<T> across threads and calling assume_init_read there). Breaking Send or Sync guarantees may lead to undefined behavior such as data races.

As an additional note, API authors whose safe abstraction relies on assume_init_read should consider re-implementing Send/Sync with a stricter bound, e.g. requiring T: Send, because the default auto-trait derivation from MaybeUninit<T> may be too permissive.

ptr::read can cause this kind of issue and doesn't document this danger, but raw pointers are neither Sync nor Send. MaybeUninit::assume_init_read can exploit this and we have impl<T: Sync> Sync for MaybeUninit<T>, so I believe we need a good documentation.

I can send a PR as soon as we agree that this is actually a problem and that an additional Safety invariant documentation is a good way to resolve this! I don't think anyone wants to resolve this by introducing unsafe impl<T: Send + Sync> Sync for MaybeUninit<T>, but I'd like to be sure.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the documented MaybeUninit::assume_init_read method and read its current Safety section alongside the supplied Playground example. Verify how sharing &MaybeUninit permits the !Send transfer, then check that the documentation explains the Send/Sync invariants and the stricter bounds API authors may need. Done means the Safety section clearly covers this misuse without changing the implementation.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
documentation
Issue type
Documentation
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.