This post is about wether mem::{replace, swap, take}
are unsound because they allow getting an bitwise copy of an otherwise !Copy
type.
The following code is sound since without calling unsafe code causing undefined behavior is not possible as Token::new_pending
is marked as unsound.
pub mod private {
pub struct Token(bool);
impl Default for Token {
fn default() -> Self {
Self::new_ready()
}
}
impl Token {
/// # Safety
///
/// The caller of this function is responsible for making sure that
/// `Self::make_ready` is called **before** calling `Self::cleanup`.
///
/// Othewise calling `Self::cleanup` will result in undefined behavior.
///
pub unsafe fn new_pending() -> Self {
Self(true)
}
pub fn new_ready() -> Self {
Self(false)
}
pub fn make_ready(&mut self) {
self.0 = false;
}
pub fn cleanup(self) {
if self.0 {
unsafe {
// SAFETY: The caller of `Self::new_pending` is responsible
// for making sure that this never happens
std::hint::unreachable_unchecked()
}
}
}
}
}
We can then create a safe wrapper around Token
to allow creating a pending token without risking undefined behavior:
mod pirvate {
pub struct Token(bool);
impl Default for Token { .. }
impl Token { .. }
pub struct Wrapper(Token);
impl Wrapper {
pub fn new_pending() -> Self {
Self(unsafe {
// SAFETY: this is safe because we don't give public access to `Token`
Token::new_pending()
})
}
pub fn new_ready() -> Self {
Self(Token::new_ready())
}
pub fn cleanup(mut self) {
self.0.make_ready();
self.0.cleanup();
}
pub fn get_mut(&mut self) -> &mut Token {
// `&mut Token` cannot be converted to `Token` since `Token != Copy`
&mut self.0
}
}
}
This relies both on Wrapper
s field 0
being private and Token
not implementing Copy
let mut wrapper = Wrapper::new_pending();
// compile error: "field `0` of struct `private::Wrapper` is private"
let token: Token = wrapper.0.cleanup();
// compile error: `cannot move out of a mutable reference`
wrapper.get_mut().cleanup();
^^^^^^^^^^^^^^^^^^---------
| |
| value moved due to this method call
move occurs because value has type `private::Token`, which does not implement the `Copy` traitimplement the `Copy` trait
The problem occurs if we mix our api with functions from mem
which use unsafe internaly, mainly by calling ptr::read
on a mutable reference to Token
which on could leak a pending Token
thus allowing safe code to cause undefined behavior.
let mut wrapper = Wrapper::new_pending();
// this is ub
std::mem::replace(wrapper.get_mut(), Token::new_ready()).cleanup();
I came up with this example as I was thinking about the soundness of take_mut::take
which can cause ub with an even simpler code:
pub mod private2 {
pub struct Token { _lock: () };
impl Token {
pub fn new() -> &'static mut Self {
unsafe {
// SAFETY: `Token` is a ZST
std::ptr::NonNull::dangling().as_mut()
}
}
pub fn cleanup(self) {
unsafe {
// SAFETY: There is no way to construct an instance of
// `Self` so this code is never run
std::hint::unreachable_unchecked()
}
}
}
}
use private2::*;
pub fn ub() {
take_mut::take(Token::new(), |token| {
token.cleanup();
loop {}
});
}
These examples are ment as purely theoretical and don't reflect real world code that should be, only what could be writen.
So the question is whether private
is unsound or mem::replace
is unsound.