Confession: I have a small Rust CLI that calls Box::leak on its config at startup, and for about a year I also had a cleanup path that turned that leaked reference back into a Box so the memory got freed on exit. It worked every time I ran it. I assumed that meant it was fine. Then Rust 1.99 landed on October 1 and the Box::leak docs now tell me, politely, that I've been standing on a trapdoor.
This post is about the two small corners of 1.99 that touch everyday memory handling: Vec::into_parts and Vec::from_parts, and the new guidance on leaked boxes. The headline feature of the release is C-variadic function definitions, which I'll mention once and then ignore, because I have never needed to write printf in Rust and I suspect you haven't either.
Short version for the impatient: if you decompose a Vec into a pointer, a length and a capacity, 1.99 gives you a NonNull-based pair of functions that replace the ManuallyDrop dance. If you leak a Box and later free it again, stop, and use Box::into_non_null instead.
What 1.99.0 actually stabilized
The official 1.99.0 announcement lists the headline items first: defining variadic functions with the "C" and "C-unwind" ABIs, and three functions for getting size and alignment out of raw pointers (Layout::for_value_raw, mem::size_of_val_raw and mem::align_of_val_raw). Further down, the stabilized API list holds the ones I care about. Vec::into_parts and Vec::from_parts are there, next to Box::into_non_null and Box::from_non_null.
A few smaller things also made it in. VecDeque::retain_back, String::from_utf8_lossy_owned, std::fs::set_times, and IntoIterator for Box<[T; N]> all went stable. I haven't used any of them yet. The one I'm most curious about is from_utf8_lossy_owned, since I suspect it saves an allocation in code that decodes untrusted bytes, but I'll only know once I profile something.
None of this is dramatic. Nobody will write a thread about it. But the Vec and Box pieces fix a papercut that I have seen in real unsafe code, including mine, and that is worth a post.
Vec decomposition before and after
If you have ever handed a buffer to C code, or built your own arena-ish thing, you have written this. Before 1.99, the stable recipe went through ManuallyDrop so that the original Vec wouldn't free the memory when it went out of scope.
use std::mem::ManuallyDrop;
let mut v = ManuallyDrop::new(vec![1u8, 2, 3]);
let (ptr, len, cap) = (v.as_mut_ptr(), v.len(), v.capacity());
// ... hand ptr/len/cap to something else, get them back later ...
let v = unsafe { Vec::from_raw_parts(ptr, len, cap) };
It works, but it asks you to remember three things: wrap in ManuallyDrop, read all three values before anything else touches the vector, and use a raw *mut T that can technically be null in the type system even though a Vec never gives you one. Here is the same thing on 1.99.
let (ptr, len, cap) = vec![1u8, 2, 3].into_parts();
// ... hand ptr/len/cap to something else, get them back later ...
let v = unsafe { Vec::from_parts(ptr, len, cap) };
The standard library docs for Vec describe into_parts as returning (NonNull<T>, usize, usize): the pointer, the length in elements, and the allocated capacity in elements. They are the same arguments, in the same order, that from_parts takes. After the call, the caller owns the memory. The only way to hand responsibility back is to rebuild a Vec with from_parts, which lets the destructor do the cleanup.
The type change matters more than the line count. NonNull<T> can't be null, so one class of "did I check for null" mistake disappears at the type level. The ManuallyDrop wrapper disappears too, and with it the possibility of reading capacity() after something already mutated the vector.
The invariants did not go away
I want to be careful here, because "safer signature" can sound like "safe". It isn't. from_parts is still unsafe, and the docs spell out the invariants that the compiler can't check. The pointer must have come from the global allocator. T must have exactly the same alignment the pointer was allocated with, and the docs are explicit that a less strict alignment doesn't count, because deallocation requires the same layout it was allocated with. The size of T times the capacity has to match the allocated size in bytes.
The allocator rule is where C interop goes wrong. If the buffer was allocated by malloc on the C side, you can't rebuild a Vec from it, because Rust will try to free it with its own allocator. The usual symptom is a crash or heap corruption somewhere far from the actual mistake, which makes it miserable to debug. So the rule I use now: a buffer crosses the boundary in the direction it was born. Rust-allocated memory goes out with into_parts and comes back with from_parts. C-allocated memory gets copied into a Vec, or stays wrapped in a slice with a custom drop.
My honest take on the new API is that it makes the correct code shorter, and that is the best a systems language can do. It does not stop anyone from writing the wrong code.
The Box::leak guidance I had been ignoring
Now the trapdoor. The 1.99.0 notes say there are no changes to language semantics, but the documentation for Box::leak now recommends against patterns that later deallocate the leaked memory. The stated reason is that such code interacts badly with current and future compiler optimizations, and especially with the upcoming stabilization of custom allocators. The notes say to prefer Box::into_non_null or Box::into_raw instead, and that the same advice applies to the other leak functions in the standard library.
The Box docs go further than the release notes. They say leak is mainly useful for data that lives for the rest of the program. If the memory should eventually be freed, use into_raw or into_non_null. And reconstructing a Box from the leaked reference, for example with Box::from_raw, is only possible when the allocator is Global, "even then it is a grey area", and many seemingly harmless ways of doing it are undefined behavior.
That is my exact bug. Here is the shape of what I had.
let cfg: &'static mut Config = Box::leak(Box::new(load_config()));
// much later, on shutdown
unsafe { drop(Box::from_raw(cfg as *mut Config)); }
And here is what I changed it to.
let cfg_ptr = Box::into_non_null(Box::new(load_config()));
// use it
let cfg: &Config = unsafe { cfg_ptr.as_ref() };
// on shutdown
unsafe { drop(Box::from_non_null(cfg_ptr)); }
The new version is slightly uglier, because I now write unsafe at the point of use instead of hiding it behind a 'static reference. I think that is the right trade. The &'static mut told the compiler, and everyone reading, that this memory lives forever. Then I contradicted it at shutdown. The NonNull version tells the truth: this memory has an owner, and the owner frees it.
Do I even need to free it
Honestly, for a CLI that exits right after, probably not. The operating system reclaims everything when the process ends, and the simplest fix for my bug would have been to delete the shutdown code and keep the plain Box::leak. I kept the free anyway, because a deliberate leak shows up as noise whenever I run the tool under a leak checker. A cleaner answer might be std::sync::OnceLock for global config, which needs no unsafe code at all. I haven't switched yet because the config is passed through three layers, and that refactor is bigger than this post.
One other thing from the Rust blog is relevant if you ship Windows builds. A separate post says that with Rust 1.100.0, the 32-bit i686-pc-windows-msvc target loses host tools tier status, and the gnu variant is similarly demoted. The announcement on demoting i686 Windows targets says builds of the standard library will still be distributed, but the compiler won't run on those hosts. If you compile on a 32-bit Windows machine, you'll want to know that before 1.100.
I cover this kind of low-level cleanup in more depth in my work on systems tooling, and I wrote about a different Rust habit that bit me in the debugger survey post.
What to do this week
Run rustup update stable and then search your own code for Box::leak. For each hit, ask whether the memory is ever freed afterwards. If it is, switch to Box::into_non_null and Box::from_non_null. If it isn't, leave it alone and add a one-line comment saying the leak is deliberate. Then grep for from_raw_parts and see whether any of them take a buffer that wasn't allocated by Rust. Those are the ones that will crash you at 2 a.m.
Originally published at abrarqasim.com. I write there about React, PHP, Rust, Go and the AI tooling around them.
Top comments (0)