>Zig gives buffer overflow safety and null pointer safety but not use after free, double free
It can provide the latter two through the use of the `GeneralPurposeAllocator`, which tracks UAF and double free.
Stack pointer escape safety is being actively researched (there are a few tracking issues, see [1]). I'm personally interested in this, I've written a decent-ish amount of Zig for toy/personal projects, and anecdotally stack pointer escape is the only type of memory error that has bitten me more than once (though to be fair, one of these cases was calling into a C API, so even Rust wouldn't have helped).
More broadly, the ability to catch all forms of UB in Debug/safety builds is an accepted proposal [2], though whether or not it will be possible in practice is a different (and interesting!) question.
The way `GeneralPurposeAllocator` works is kinda scary though, since it may result in whole memory pages used only for very small allocations, effectively multiplying the memory usage of the program. Also kinda goes against the memory exhaustion safety, since now you're more likely to exhaust memory.
Yeah, I don't think it's right to say Zig doesn't have use-after-free and double-free safety. If you want that, you can just use a general purpose allocator. Note that you can't forget to choose an allocator since they are explicit.
Is this somehow harder than, say, choosing not to use "unsafe" in Rust?
Maybe all that is missing is a linter to help enforce whatever memory-management policy you've decided on. That's not really needed for small, coherent teams, but would be important for using Zig in larger projects with multiple teams and/or disparate individual contributors. (Perhaps such a thing exists and I just don't know about it.)
You might also be able to use an arena allocator where free is a no-op. That has different tradeoffs, but is also safe for use-after-free and double-free.
As you say, stack escape is the main thing where Zig doesn't have a good memory-safety story yet (at least not that I've heard). I guess there are a few others that concern me when I see them on a list, though I haven't hit them in real life.
Static analysis at the IR level would be awesome. It could catch use-undefined, stack escape, and probably uaf/df as well so you don't have to lean on gpa's (potentially expensive) tricks. Plus there are times when you maybe don't want to use page allocator.
As an aside. I'm not certain I understand how double free is memory unsafe (in the sense of "causing vulnerabilities")
A double free breaks invariants for the memory allocator. For example, the freed memory may have been handed out to someone else and if you free it again, it will be marked as unused even though that code relies on their stuff being available. One very classic way of exploiting a double-free is a sequence like this happens:
1. Some code allocates memory.
2. The code frees the memory, but keeps a stale reference to it around. It is marked as unused by the allocator.
3. Some other code allocates memory. Maybe it's reading the password file off of disk. The allocator has some unused memory lying around so it hands it out–but it turns out that this is actually just a reuse of the buffer from earlier. It is now marked as "in use" again by the allocator.
4. The code from earlier has a bug and frees the allocation again. This means that the allocation is now marked as "unused".
5. Another allocation request hands out this memory again. Maybe it's a buffer for user input? Well, it's been scribbled all over with other data now.
6. Someone asks to authenticate and the password checking code gets called. It has the password right here to check against…oh, wait, that memory got freed out from under it and overwritten with some attacker-controlled content!
> I'm not certain I understand how double free is memory unsafe (in the sense of "causing vulnerabilities")
Perhaps there are some allocators where doing that hits UB. UB in memory allocation is probably always a memory safety issue. I would say if your code accepts any allocators where double-free could be UB then you've got a safety issue.
What do you think the difference is between “patching out free” and allocators as a first-class feature? I’ll bet you can think of a few rather significant ones if you try.
Rust-the-language doesn't do any dynamic allocation, so it doesn't exhaust memory at all. The Rust stdlib currently guarantees an abort on OOM, but in the future this will be changed to a panic (which you can always configure to be an abort if you want). Both of these behaviors are memory-safe (it's unclear what "memory exhaustion safety" is referring to).
Also to clarify: there are platforms (like linux) which de facto have nonfailable allocation, if you run out of memory the system will oom kill your process, and neither zig nor rust will save you from that.
I believe In practice the most common place where failable OOM is a big deal is in embedded systems programming
No language can save you from OOMs, but because Zig pervasively uses explicit memory allocation, it makes it easy to greatly mitigate OOM risks by front-loading all the fallibility:
1. Calculate and allocate all your required memory immediately upon process startup (including memory from OS resources like sockets)
2. Call mlockall to prevent the pages from being swapped out
3. Write "-1000" to /proc/self/oom_score_adj to avoid the OOM killer
4. Use your preallocated memory as a pool for all further allocations
With the above approach, the application has full control over how to handle application-level OOMs (e.g. applying backpressure to connecting clients, or shrinking non-critical caches) once it is past the start-up stage.
Zig also gives you memory exhaustion safety which rust does not.