Asserting initialization of `MaybeUninit` with address

Is the following function sound? I was unable to break it, but maybe there is something I'm missing?

use std::mem::MaybeUninit;

pub fn validate_uninit<T>(
    place: &mut MaybeUninit<T>,
    initializer: impl FnOnce(&mut MaybeUninit<T>) -> &T,
) -> &mut T {
    assert_eq!(
        initializer(place) as *const T,
        place as *const MaybeUninit<T> as *const T,
        "callback should return the same place",
    );
    // SAFETY: The callback initialized it.
    unsafe { place.assume_init_mut() }
}

Context: this function (not mine), which is basically the same. I'm curious whether it is sound.

Once initializer returns, you have a mutable and a shared reference to the same value, I think. That would be UB.

2 Likes

The shared reference is derived from the mutable reference, so I don't think this is a problem.

It's probably sound. The &T serves as an observation that there is a valid T at that location, and the only way to get two objects at the same address is if T is zero sized, in which case it has no bytes that need to be initialized.

Where it could potentially not be sound is if initializer initializes T into some state where it's sound to use &T but not to use &mut T. However, since doing so necessarily requires unsafe, it's not immediately clear which party is at fault. This can be avoided by just having initializer return &mut T. (In this case, it makes this function essentially useless, but it would still be useful for shared_arena.

pub struct Evil {
    _priv: (),
}

impl Evil {
    pub fn laughter(&mut self) {
        // SAFETY: it is impossible to construct `&mut Evil`.
        unsafe { unreachable_unchecked() };
        // A realistic case would have some safety invariant
        // that only matters for access by &mut, not by &ref.
    }
}

pub fn initialize_evil(place: &mut MaybeUninit<Evil>) -> &Evil {
    // SAFETY: `Evil` is ZST, thus does not need initialization.
    assert!(size_of::<Evil>() == 0);
    // SAFETY: `&Evil` cannot be used to call `Evil::laughter`.
    unsafe { place.assume_init_ref() }
}

This is an instance of the interesting case where two independent APIs are both probably sound in a vacuum, but when combined lead to an unsoundness. Given validate_uninit can be made resilient fairly easily by just requiring initializer to return &mut T, I personally lean towards initialize_evil being okay and validate_uninit being not okay at the current moment, but it's a very weak preference.

A possible type for Evil would be a Vec where the capacity is less than the proper allocation size. I don't believe that it's possible for a shared reference to such a Vec is currently able to cause UB. (But it's still unsound according to the documentation to do so! If you create such a Vec you're giving it license to cause UB all over your program.) With &mut this will cause UB if the Vec reallocates (or you mem::take it and drop it).

You don't ever have two live references at the same time. initializer is no different than having some &mut T and reborrowing it as a shared reference, e.g. let a = &mut 0; let b = &*a;

Because the returned reference is discarded before the place reference is used again, everything's fine here. In fact, the borrowchecker has us covered w.r.t. that particular avenue for problems, since the lifetime of the returned reference is tied to the borrow/loan given to initializer in the first place.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.