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.