Static allocation doesn’t eliminate use-after-free bugs, it just reduces them.
If you have mutable data structures, it’s still always possible to refer to something that used to have one meaning and now means something else. That’s still a use-after-free bug.
I was going to say that it’s similar to how GC can still have memory leaks, but in fact it’s the exact same problem: reachability of data that is no longer relevant/correct.
That jumped out to me too. The point of use after free is that your program has a logic error. The logic error doesn't go away just because it doesn't crash.
There's a lot of wisdom in this post, but that made me wince. It's a bit like saying "Our system doesn't crash when we dereference a null pointer." Well, cool. What could possibly go wrong.
Still, I think it's best to just ignore that line and read the rest of the post. It's an unfortunate distraction to the core idea (which does have real value). For example:
> here is how we calculate the amount of memory needed to store messages for TigerBeetle’s consensus protocol: <snip>
> And if we get the calculation wrong? Well, since we enforce static allocation, we can check these calculations with assertions (i.e. that a free message is always available). If we then get the static allocation calculation wrong, this can be surfaced sooner through fuzzing, rather than having no limits and eventual resource exhaustion (and cascading failure!) in production. When combined with assertions, static allocation is a force multiplier for fuzzing!
Being able to put a hard upper bound on the amount of memory your system can use is generally a sign of good design. The alternatives are somewhat messy but still work. (The HN clone I maintain https://www.laarc.io/ runs via a while-loop bash script, because it dies via OOM every couple weeks. But it works great, since every time it dies it just restarts.)
To be fair, we're certainly concerned about logic errors and buffer bleeds. The philosophy in TigerBeetle is always to downgrade a worse bug to a lesser. For example, if it's a choice between correctness and liveness, we'll downgrade the potential correctness bug to a crash.
In the specific case of message buffer reuse here, our last line of defense then is also TigerBeetle's assertions, hash chains and checksums. These exhaustively check all function pre/post-conditions, arguments, processing steps and return values. The assertion-function ratio is then also tracked for coverage, especially in critical sections like our consensus or storage engine.
So—apologies for the wince! I feel it too, this would certainly be a nasty bug if it were to happen.
So you only reuse memory for objects with a checksum? Buffer bleed is scary if exploitable (see heartbleed) and I’m curious how you are protecting against it in practice.
We use what we have available, according to the context: checksums, assertions, hash chains. You can't always use every technique. But anything that can possibly be verified online, we do.
Buffer bleeds also terrify me. In fact, I worked on static analysis tooling to detect zero day buffer bleed exploits in the Zip file format [1].
However, to be clear, the heart of a bleed is a logic error, and therefore even memory safe languages such as JavaScript can be vulnerable.
In zig, you can run your a subset of tests with a normal allocator (or better yet, a page allocator) that will really help you find these and then swap out for the statically allocated system later.
Even resource leaks are still possible. Marking an object in a static array as used is still allocation. Forget to mark it as unused later and it still remains unavailable. Do that enough times and you'll still run out. What this approach does is eliminate unconstrained memory use. Leak-related failures will tend to surface earlier and more predictably, instead of at some distant unpredictable time when the system can't satisfy your program's infinite appetite. And there are resources besides memory.
This approach is still useful, but it's no panacea. It can even leave various kinds of capacity unused, or it can become a tuning nightmare if you try to avoid that. It has long been common in embedded (as others have pointed out) due to the requirements and tradeoffs characteristic of that space, but for general-purpose computation it might not be the tradeoff you want to make when other techniques can solve the same problem without some of the drawbacks.
Where this hair gets particularly thin is when you start with an app that uses fixed memory and then you end up in a situation where it works out that running many copies of the code is a solution to a (scalability) problem. Then pre-allocation becomes a problem because you've just locked up a hefty chunk of memory.
And that can be a self fulfilling prophecy. One of the problems with memory pools is concurrent access, both by your programming language and the processor, and so it starts looking attractive to spool up a process per task.
Based on my limited experience with using arenas and preallocated memory I find debugging memory issues significantly harder since it’s super easy to accidentally reuse allocations and only notice much later.
The first pointer arithmetic bug I fixed, I only narrowed it down once I realized that the function was getting garbage data. Several others since have been the same way. This data structure has a value that can’t possibly be arrived at, therefore this data is fake.
When you point to real, dead objects, the bug can be anywhere or everywhere. If you have a pattern of using impure functions everywhere this can be a nightmare to identify and fix at scale.
Yep, I was thinking along the same lines. What if the database kept a reference to a connection for some timer or something and the connection memory later gets used for a different connection? GC languages don't have this problem because you would just allocate a fresh connection each time and the language itself guarantees that said physical memory is not reused unless it is unreachable.
use after free is really about the consequences of use-after-reuse. The bug is always there, but you don't notice the behavior until someone changes the data out from under you. Until someone reuses the memory allocation, your app appears to be working properly.
I understand your point. My comment was a weak attempt at being clever. Free (the concept) versus free(void* ptr) (the function call) - the latter would not be present without dynamic allocation.
At the risk of shilling for Big Rust (or at least any language with ownership semantics, really) -- well-designed Rust code can truly eliminate use-after-free. I was the lead engineer on a project that wrote an entire filesystem from scratch in Rust, and we did exactly this. 100K lines of code without any use-after-free bugs in the whole development process.
One of the first bits of code we wrote in the project was a statically allocated pool of buffers such that the only way to free buffers back to the pool is for all references to that buffer being dropped.
Static allocation doesn’t eliminate use-after-free bugs, it just reduces them.
If you have mutable data structures, it’s still always possible to refer to something that used to have one meaning and now means something else. That’s still a use-after-free bug.
I was going to say that it’s similar to how GC can still have memory leaks, but in fact it’s the exact same problem: reachability of data that is no longer relevant/correct.