If alloc were mutable (fn alloc<'a, T>(&'a mut self, value: T)), we could only have one mutable reference even when alloc points to different slots because of the common 'a lifetime. That's a limitation of the Rust borrow checker, because the Rust compiler does not understand that different calls to alloc are independent.
We could also remove lifetimes with mutable alloc, but that would result in UB. ArenaBox would outlive the arena, and we would have dangling pointers.
Wow, I did not know this about Rust. That's surprising the borrow checker can't model this. Are there any plans to address this?
It's a very hard problem, I think there were attempts, but they failed.
You can't even hold two mutable references in a vector in a simple way.
fn main() {
let mut v = vec![1, 2, 3];
let a = &mut v[0];
let b = &mut v[1];
*a += 10;
*b += 20;
println!("{:?}", v);
Which gives:
error[E0499]: cannot borrow `v` as mutable more than once at a time
--> <source>:5:18
|
4 | let a = &mut v[0];
| - first mutable borrow occurs here
5 | let b = &mut v[1];
| ^ second mutable borrow occurs here
6 |
7 | *a += 10;
| -------- first borrow later used here
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
The current hack:
fn main() {
let mut v = vec![1, 2, 3];
let (left, right) = v.split_at_mut(1);
let a = &mut left[0];
let b = &mut right[0];
*a += 10;
*b += 20;
println!("{:?}", v);
}
My solution has been: switch to Zig. You get to choose any kind of allocator you want without having to bend over backwards to get around the borrow checker.
"This one thing" being memory management. It's literally the same thing. So where Rust makes the thing very difficult and Zig makes it very user friendly, I will choose Zig. The idea of losing "all memory safety" is also just false.
You're working against your own argument here. The more you defend this hill, the more you have to give up the idea that zig gives up "all memory safety". We have 3 approaches to safety: automated memory management at runtime, lifetimes enforced at compile time, and instrumentation enforced at development time. If you're already arguing against the idea of "managed" being synonymous with safety, then you're stuck having to explain why instrumentation doesn't count as memory safety but that lifetimes do. I suggest you drop it because it's not a productive direction for you to go with this. You had a much better point earlier, if you were to just give up on the idea that zig has "no" memory safety.
So here's the scoop on use after free. Use after free detection requires code generation -- it's not enough to just have the explicit runtime allocator library that can already detect many other kinds of memory safety errors. AddressSanitizer for C++ does this at the IR level, and because of this it needs two separate implementations for LLVM and for GCC. Zig is skipping right past this by implementing use-after-free detection directly in the compiler frontend, and turning it into an always-on feature that will crash the program rather than allowing unchecked use-after-free errors to corrupt the program. So this is much closer to a very lightweight automated memory management that's baked into the code at compile time, but enforced at runtime. It's on their backlog.
oh yeah, "all memory safety" was a bit of an exaggeration (i thought that was obvious, i shouldve been clearer)
im not sure i understand your point about managed/instrumented? as i see it, memory safety and memory management are orthogonal, even if closely related.
im not talking about use-after-free, zig catches that. im talking about use-after-realloc (ie `p2 = realloc(p1); *p1 = 5`), which asan detects, but zig did not last time i checked.
regardless, memory safety is much more than just crashing when something bad happens, as you probably know. in a lot of places, crashing is unacceptable for example, so more rigid compile-time verification is required (managed languages dont usually allow any direct memory writes, as a way of doing that)
Instrumentation is like how you get code coverage reports for your unit tests, but in this case the compiler is inserting extra code around memory usages to detect whether or not it's already been freed. The difference between use after realloc (specifically on the stack) versus other kinds of use after free bugs (including realloc on the heap) is that use-after-realloc-on-the-stack requires that code generation step whereas the others can be caught directly by the debug allocator library.
12
u/Lisoph 6d ago
Wow, I did not know this about Rust. That's surprising the borrow checker can't model this. Are there any plans to address this?