`Arena::alloc_with` is unsound when combined with types only sound behind `&`
CAD97 opened this issue · comments
Example:
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() }
}
// ... elsewhere ...
let mut arena_box = arena.alloc_with(initialize_evil);
arena_box.laughter(); // 💥
This exploits the fact that initializer
is only required to return &T
to create a reference to a type in some state where the &T
-accessible API is sound, but &mut T
access is unsound. This should be resolvable by changing initializer
to require returning &mut T
instead, which will serve as a witness not just that some T
exists at the address, but that all of the API accessible from &mut T
is sound to use, not just the &T
API.
As a somewhat practical example, consider Vec
. Since this is a type I do not control, constructing such an instance of the type is still "library UB" and a new function making construction of a bad Vec
unsound could be added. However, if I create some Vec
where the capacity does not match the size the pointer was allocated with, I believe the current &Vec
API remains sound, even though the &mut Vec
API is terribly unsound when Vec
's invariants are violated in this manner.
This was prompted by someone asking about the shape of the function on urlo. https://users.rust-lang.org/t/asserting-initialization-of-maybeuninit-with-address/88491?u=cad97