I just wanna map over a mut in-place

It's just 3 lines of unsafe code, no way that's unsound!


What we wanna do

#[derive(Debug)]
struct NotCopy(u32);

fn main() {
    let f = |x: NotCopy| NotCopy(x.0 + 1);

    let mut c = NotCopy(5);
    let v = &mut v;

    *v = f(v);
}

This code won’t compile because v is &mut NotCopy and f expects a value of NotCopy. Simple enough, but we are assigning back to *v so why can’t the compiler infer its ok to move out of the value, we’re moving back in right away… right? lets dig into it

Digging in

First off lets look at the canonical safe way these semantics are achieved:

let mut c = Some(NotCopy(5));
let v = &mut c;

let val = v.take().unwrap();
v.replace(f(val));

This is alright, but this option is kind of a pain. We have an unwrap that we know can never fail (or can it oooohhh forshadowing), and if that is actually a field or something then we have to pollute the field type. Additionally if that field is used elsewhere you have to unwrap it each time, which can get verbose and annoying quick.

Lets try some unsafe

let mut c = NotCopy(5);
let mut v = &mut c;

unsafe {
    let val = std::ptr::read(v);
    let val = f(val);
    std::ptr::write(v, val);
}

Now I know everyone says not to use it, but its just a little bit of unsafe what could go wrong. No more pollution of the type, and its just three lines of unsafe code so it should be easy to see the bugs…

This code is unsound. Allow me to demonstrate

#[derive(Debug)]
struct PrintDrop;

impl Drop for PrintDrop {
    fn drop(&mut self) {
        println!("drop");
    }
}

fn main() {
    let f = |x| {
        panic!("kaboom");
    };

    let mut c = PrintDrop;
    let mut v = &mut c;

    unsafe {
        let val = std::ptr::read(v);
        let val = f(val);
        std::ptr::write(v, val);
    }
}

This prints “drop” twice. Now lets image that PrintDrop is say… a Box, this just caused a double-free. godbolt

The explanation

The basic issue is that if f panics then val will be dropped, the memory at v is now undefined but we are still unwinding the stack, we have a &mut to undefined memory. For starters this is just plain old UB, you’re not allowed to construct a reference to undefined memory (pointers are ok as long as you don’t deref), but even in the concrete we’ve demonstrated a double-free with a trivial example.

Remember that unwrap right at the start? Well now we can see that there is a case when it can fail, this case. The Option API has let us specify exactly the state of the memory at the address in this panic case, it will be None and the value will only be dropped once.

As someone who has fought with these kinds of exception safety guarantees in C++ (std::make_unique am I right 💀), when I realised the Option pattern just makes this statically memory safe seemingly almost by accident just as a consequence of Rust’s strict constraints on safe code, well it’s just so cool I wanted to share.