It doesn't.
For me that's not enough to move the needle.
I'm a Rust person by both day job and hobby, but I can see the niche Zig is in as being quite useful in particular for embedded work.
One thing it definitely has over Rust is way better support for explicit per-structure allocator/allocation management. The allocator_api stuff in Rust has just been sitting unstable for years at this point, while Zig shipped with support for this immediately. Which is key for work in all sorts of systems level contexts.
Probably the language I want is somewhere between the two.
In C/C++ land you need to consider not only the code being changed but also the entire code base and how that code is used. It's not enough to look at the diff. You need to understand the context.
Not to say that isn't also true with Rust to some degree, but the degree is usually in terms of logic and less "is some pointer/reference going to cause a race/segfault/buffer xflow?"
The langauge itself doesn't define allocation, Box is in the stdlib and this allows for nostd libraries to deal with things as I guess most of us might expect I'd think. It would be cool to allow for a global allocator though to enable std on more platforms with better semantics, no disagreement there.
Nullability (and error handling) is compile time checked though.
Developers can generally be taught to handle memory ownership issues fairly well, esp when helped out with RAII smart pointers, etc.
But in my experience when you throw concurrency in, even very high quality engineers tend to stumble and accidentally introduce race conditions.
In this respect Rust has something over the garbage collected languages, too. Because Java, Go, JS, C#, etc will effectively shield you from "use after free" but they will absolutely not stop you from passing an unlocked, etc. reference to another thread and screwing yourself up royally; and Rust will.
Zig also gives you memory exhaustion safety which rust does not.
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.
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.
As an aside. I'm not certain I understand how double free is memory unsafe (in the sense of "causing vulnerabilities")
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!
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.
I believe In practice the most common place where failable OOM is a big deal is in embedded systems programming
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.
Sure this more like DDOS mitigation, so it memory related safety, not memory safety.