Using uninitialized memory for fun and profit (2008)(research.swtch.com) |
Using uninitialized memory for fun and profit (2008)(research.swtch.com) |
But there is tension about this: Not allowing access to uninitialized memory, ever, means that you get more guarantees about what foreign (safe) Rust can do, for instance.
Putting that aside, a deliberate "read uninitialized memory with bounded UB" primitive like freeze would only work for types where all possible bit patterns are valid. So no freezing chars[2], references, or sum types. And any transparent wrapper type that has invariants - like, say, slices, vecs, strs, and/or range-restricted integer types - would see them utterly broken when frozen. I suppose you could define some operation to "validate" the underlying bit pattern, but I'm not sure if that would defeat the point of reading uninitialized memory.
[0] LLVM concept that represents uninitialized memory, among other things.
[1] I believe a few other unsafe Rust concepts are actually leaky abstractions around LLVM things
[2] Rust's char must hold valid UTF-8 and will UB if you stick surrogates in there
It's true that LLVM has restrictions on what you can do with undef/poison memory, but LLVM also supports the "freeze" operation that comes up in the Rust discussions (which transforms an undefined value into an arbitrary, well-defined value). It would certainly need to be unsafe to avoid violating invariants like you mentioned, but "LLVM" isn't the blocker to supporting this.
Rather, there are more subtle problems with reading from initialized memory -- for example on Linux, a heap allocator might use MADV_FREE on free memory, which hints to the kernel that a page contains freed memory and the operating system is not required to preserve its contents until the application writes to it again. This means the following sequence of events is possible:
- An application frees some memory, and the heap allocator invokes madvise(MADV_FREE) on the address range.
- The application makes a heap allocation, obtaining a pointer to the free'd memory.
- The application freezes the uninitialized memory and reads from it.
- Due to memory pressure, the kernel decides to reclaim the free'd memory. It unmaps it from the process and uses it somewhere else.
- The application accesses the first allocation again, and sees that its value has now changed to all-zeroes.
Thus, we can see that "freezing" arbitrary memory can't actually be implemented on real-world systems -- the contents of uninitialized memory really can change out from under you until you write to that memory.
It would be possible to implement a "by-reference freeze" that copies a MaybeUninit<T> to a new location, but introducing this functionality still has the downside that you can write a Heartbleed bug without invoking undefined behavior, which is what makes it controversial.
Clearing the hint on read would probably be more sane, but would mean many more potential situations of unnecessarily losing it (GC doing unnecessary scanning, doing a heap dump, debuggers trying to read it, other sorts of memory scanning)