Leaking

    Everything is terrible and we have new and exotic problems to try to solve.

    Many people like to believe that Rust eliminates resource leaks. In practice,this is basically true. You would be surprised to see a Safe Rust programleak resources in an uncontrolled way.

    However from a theoretical perspective this is absolutely not the case, nomatter how you look at it. In the strictest sense, “leaking” is so abstract asto be unpreventable. It’s quite trivial to initialize a collection at the startof a program, fill it with tons of objects with destructors, and then enter aninfinite event loop that never refers to it. The collection will sit arounduselessly, holding on to its precious resources until the program terminates (atwhich point all those resources would have been reclaimed by the OS anyway).

    We may consider a more restricted form of leak: failing to drop a value that isunreachable. Rust also doesn’t prevent this. In fact Rust has a function fordoing this: . This function consumes the value it is passed andthen doesn’t run its destructor.

    In the past mem::forget was marked as unsafe as a sort of lint against usingit, since failing to call a destructor is generally not a well-behaved thing todo (though useful for some special unsafe code). However this was generallydetermined to be an untenable stance to take: there are many ways to fail tocall a destructor in safe code. The most famous example is creating a cycle ofreference-counted pointers using interior mutability.

    It is reasonable for safe code to assume that destructor leaks do not happen, asany program that leaks destructors is probably wrong. However unsafe codecannot rely on destructors to be run in order to be safe. For most types thisdoesn’t matter: if you leak the destructor then the type is by definitioninaccessible, so it doesn’t matter, right? For instance, if you leak a Box<u8>then you waste some memory but that’s hardly going to violate memory-safety.

    However where we must be careful with destructor leaks are proxy types. Theseare types which manage access to a distinct object, but don’t actually own it.Proxy objects are quite rare. Proxy objects you’ll need to care about are evenrarer. However we’ll focus on three interesting examples in the standardlibrary:

    • Rc

    Now, consider Drain in the middle of iteration: some values have been moved out,and others haven’t. This means that part of the Vec is now full of logicallyuninitialized data! We could backshift all the elements in the Vec every time weremove a value, but this would have pretty catastrophic performanceconsequences.

    Instead, we would like Drain to fix the Vec’s backing storage when it isdropped. It should run itself to completion, backshift any elements that weren’tremoved (drain supports subranges), and then fix Vec’s len. It’s evenunwinding-safe! Easy!

    Now consider the following:

    This is pretty clearly Not Good. Unfortunately, we’re kind of stuck between arock and a hard place: maintaining consistent state at every step has anenormous cost (and would negate any benefits of the API). Failing to maintainconsistent state gives us Undefined Behavior in safe code (making the APIunsound).

    So what can we do? Well, we can pick a trivially consistent state: set the Vec’slen to be 0 when we start the iteration, and fix it up if necessary in thedestructor. That way, if everything executes like normal we get the desiredbehavior with minimal overhead. But if someone has the audacity tomem::forget us in the middle of the iteration, all that does is leak even more(and possibly leave the Vec in an unexpected but otherwise consistent state).Since we’ve accepted that mem::forget is safe, this is definitely safe. We callleaks causing more leaks a leak amplification.

    Rc is an interesting case because at first glance it doesn’t appear to be aproxy value at all. After all, it manages the data it points to, and droppingall the Rcs for a value will drop that value. Leaking an Rc doesn’t seem like itwould be particularly dangerous. It will leave the refcount permanentlyincremented and prevent the data from being freed or dropped, but that seemsjust like Box, right?

    Nope.

    This code contains an implicit and subtle assumption: ref_count can fit in ausize, because there can’t be more than usize::MAX Rcs in memory. Howeverthis itself assumes that the ref_count accurately reflects the number of Rcsin memory, which we know is false with mem::forget. Using we canoverflow the ref_count, and then get it down to 0 with outstanding Rcs. Thenwe can happily use-after-free the inner data. Bad Bad Not Good.

    This can be solved by just checking the ref_count and doing something. Thestandard library’s stance is to just abort, because your program has becomehorribly degenerate. Also oh my gosh it’s such a ridiculous corner case.

    The thread::scoped API intended to allow threads to be spawned that referencedata on their parent’s stack without any synchronization over that data byensuring the parent joins the thread before any of the shared data goes outof scope.

    Here f is some closure for the other thread to execute. Saying thatF: Send +'a is saying that it closes over data that lives for 'a, and iteither owns that data or the data was Sync (implying &data is Send).

    Because JoinGuard has a lifetime, it keeps all the data it closes overborrowed in the parent thread. This means the JoinGuard can’t outlivethe data that the other thread is working on. When the JoinGuard does getdropped it blocks the parent thread, ensuring the child terminates before anyof the closed-over data goes out of scope in the parent.

    Usage looked like:

    In principle, this totally works! Rust’s ownership system perfectly ensures it!…except it relies on a destructor being called to be safe.