Pre-Pooping Your Pants With Rust - Faultlore
This old browser is unsupported and will most likely display funky<br>things.
Pre-Pooping Your Pants With Rust
Wow I sure wrote this article, huh? What the fuck was I thinking, and why did everyone love this so much and latch onto it? I’ve preserved it for the sake of “history” but for real I made a much more useful version of this in the Rustonomicon’s section on leaking. Also I rebranded “Pre-Pooping Your Pants” as just “Leak Amplification”.
§Leakpocalypse
Much existential anguish and ennui was recently triggered by Rust Issue #24292: std::thread::JoinGuard (and scoped) are unsound because of reference cycles. If you feel like you’re sufficiently familiar with Leakpocalypse 2k15, feel free to skip to the next section. If you’ve been thoroughly stalking all my online interactions, then you’ve basically seen everything in this post already. Feel free to close this tab and return to scanning my IRC logs.
The issue in question states:
You can use a reference cycle to leak a JoinGuard and then the scoped thread can access freed memory
This is a very serious claim, since all the relevant APIs are marked as safe, and a use-after-free is something that should be impossible for safe code to perform.
The main focus is on the thread::scoped API which spawns a thread that can safely access the contents of another thread’s stack frame in a statically guaranteed way. The basic idea idea is that thread::scoped returns a JoinGuard type (jg in fn bad in the above example).
JoinGuard’s destructor blocks on the thread joining, and isn’t allowed to outlive any of the things that were passed into thread::scoped. This enables really nice things like:
use std::vec::Vec;<br>use std::thread;<br>fn increment_elements(slice: &mut [u32]) {<br>for a in slice.iter_mut() { *a += 1; }
fn main() {<br>let mut v = Vec::new();<br>for i in (0..100) { v.push(i); }
let mut threads = Vec::new();<br>for slice in v.chunks_mut(10) {<br>threads.push(thread::scoped(move || {<br>increment_elements(slice);<br>}));<br>// when `threads` gets dropped here, `main` will block<br>// on all the threads joining.
which dispatches some workload on every element in an array in 10-element chunks to separate threads (you’d probably want a pool for this; not the point). Magically Rust is able to statically guarantee this is safe without even knowing what a thread is! All it knows is that there’s there’s a Vec threads full of these JoinGuard things which borrow v, and thus can’t outlive it. (Actually Rust also doesn’t really understand Vec either. It really thinks threads is directly borrowing v, even if empty.)
Pretty nifty!
Unfortunately, this is completely wrong . This assumes that destructors are guaranteed to run in safe code. A lot of us in the Rust community (myself included) had grown to assume this was true. After all, we have a function that does nothing but drop an element without calling its destructor called mem::forget, and it’s marked as unsafe!
Turns out this is simply a legacy detail from The Long Long Ago. There are in fact several ways to write mem::forget using only safe code provided by the standard library. A few of them can be regarded as simply implementation bugs, but one is fundamental: rc::Rc.
Rc is our reference counted smart pointer type. It’s actually pretty simple: you put data in a reference counted pointer with Rc::new. The reference count increases when you clone an Rc, and decreases when you drop one. No need to touch the reference count otherwise; the lifetime system will ensure that any internal references to the data are relinquished before the Rc they were obtained through is dropped.
On its own, Rc is totally fine. Because reference-counting is inherently a sharing of data, you can only get shared references to the internal data. This generally precludes mutating the internals of the Rc, so everything is frozen in time once it’s put in there.
Except Rust has internal mutability. While sharing generally implies immutability in Rust, internal mutability is the exception. The Cell types are the primary mechanism for internal mutability: they allow their contents to be mutated while being shared. For this reason they are marked as non-thread-safe. sync::Mutex is their thread-safe counterpart.
Now let’s put those two tools together and write mem::forget:
fn safe_forget(data: T) {<br>use std::rc::Rc;<br>use std::cell::RefCell;
struct Leak {<br>cycle: RefCellOption>>>>,<br>data: T,
let e = Rc::new(Leak {<br>cycle: RefCell::new(None),<br>data: data,<br>});<br>*e.cycle.borrow_mut() = Some(Rc::new(e.clone())); // Create a cycle
This is some janky nonsense, but long-story-short we can create reference-counted cycles using Rc and RefCell. The end result is that the destructor of the value put into our Leak type is never called, even though all of the Rcs have become unreachable! Never calling a destructor is actually fine on its own: you can abort the program or loop forever to similar effect. However in this case Rc has told...