Static Allocation, Constant Work
Fixed Allocation, Constant Workloads
September 2, 2026
This piece is a reflection on a previous discussion regarding the most grueling challenges of memory safety. Specifically, it addresses the scenario where a pointer to one variant of a union persists even after a different variant has been written to that memory. Consequently, a "live" typed pointer ends up interpreting bytes that no longer belong to the original type.
The Anatomy of a Use-After-Free (UAF)
The author shares a personal experience from developing a limit-order matching engine. A bug was shipped where a cancelled order was returned to the memory pool while a reference to it still existed within its price level. When the pool re-allocated that specific memory slot to a new order, the stale link continued to resolve, leading to a use-after-free error.
Initially, this was dismissed as mere carelessness regarding lifetimes. However, upon further reflection, the author posits that a recycling pool is essentially a tagged union, where the "tag" represents the current generation of the object occupying that slot—yet this tag is invisible to the type system.
Logical vs. Physical Memory Safety
There is a critical distinction between the logical error of a use-after-free and its physical manifestation.
1. The malloc/free Approach
When using standard dynamic allocation, a logical UAF manifests as physical type confusion. This is highly dangerous. If two different types share the same memory address, a value intended as a simple integer in one object could be interpreted as a function pointer in another, creating an exploitable goto primitive.
2. The Object Pool Approach
If a pool is used to store "dead" objects of a specific type , a logical UAF still occurs, but the physical result is different. We have memory aliasing, but no type confusion.
Note: The only exception is if the object contains an inline enum, which could still lead to type-related issues.
Assuming no inline enums are present, the behavior becomes deterministic. While the result is logically incorrect, it is not an arbitrary memory corruption vulnerability.
Comparison Table: Allocation Strategies
| Strategy | Logical UAF? | Physical Type Confusion? | Risk Level |
|---|---|---|---|
malloc / free | Yes | Yes | |
| Homogeneous Pool | Yes | No* | |
| Type-Segregated Pool | Yes | No* |
*Unless inline enums are used.
Hardening via Type Segregation
A technique for hardening code involves using type-segregated pools. If the allocation function is typed (utilizing a comptime parameter or a runtime type witness), the allocator can maintain separate pools for each type.
Trade-offs of Type Segregation:
Memory Efficiency:It is slightly less efficient because memory freed from type cannot be reused for type .- Locality: It may actually improve memory locality.
- Safety: It eliminates most forms of type confusion.
Interestingly, if enum variants are always heap-allocated rather than inlined, this safety is preserved.
The Philosophy of Static Allocation
While generational indices are a common fix, another approach—inspired by TigerStyle—is the total elimination of dynamic allocation after the initialization phase.
The "Hard Limit" Strategy
Instead of allowing the heap to grow, the system defines a maximum capacity at startup:
$ order-engine --orders-max=1_000_000
// Example initialization
const orders: []Order = try gpa.alloc(Order, cli_args.orders_max);
If the system receives more requests than orders_max, the surplus is simply rejected. This prevents catastrophic failure. If a system operates at capacity without strict limits, it risks triggering the kernel's OOM (Out of Memory) killer, which could terminate the engine or its supervisor, causing total downtime.
Implementation Patterns
There are two primary ways to manage this static slice:
- The Tracking Approach:
- Initialize memory via
@memset(orders, undefined). - Use a
DynamicBitSetor a free list to track available slots.
- Initialize memory via
const OrderPool = struct {
orders: []Order,
free: DynamicBitSet,
fn acquire(pool: *OrderPool) ?*Order {
/* logic to find free bit */
}
};
- The Neutral State Approach: Introduce a "reserved" or "no-op" state to the order type.
const Order = struct {
id: u128,
price: u32,
count: u32,
tag: enum { bid, ask, reserved },
};
pub const reserved_order: Order = .{
.id = 0, .price = 0, .count = 0, .tag = .reserved
};
By initializing the entire array with .reserved, the developer shifts their mental model. Orders are no longer "created" or "destroyed"; they simply circulate.
Summary of Benefits
Final Checklist for Implementation:
- Define
max_capacityat startup. - Pre-allocate all necessary structures.
- Implement explicit state transition functions.
- Use
assertto verify state invariants.