logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·31.10.26
← Back to news

Static Allocation, Constant Work

Original article
matklad.github.io|38 points|0 comments|by surprisetalk|Sep 2, 2026

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 TTT, 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

StrategyLogical UAF?Physical Type Confusion?Risk Level
malloc / freeYesYesHigh (RCE)\text{High (RCE)}High (RCE)
Homogeneous PoolYesNo*Medium (Logic Error)\text{Medium (Logic Error)}Medium (Logic Error)
Type-Segregated PoolYesNo*Medium (Logic Error)\text{Medium (Logic Error)}Medium (Logic Error)

*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 UUU cannot be reused for type TTT.
  • Locality: It may actually improve memory locality.
  • Safety: It eliminates most forms of type confusion.

Memory Overhead≈∑(Rare Types)≪Total Memory\text{Memory Overhead} \approx \sum (\text{Rare Types}) \ll \text{Total Memory}Memory Overhead≈∑(Rare Types)≪Total Memory

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:

  1. The Tracking Approach:
    • Initialize memory via @memset(orders, undefined).
    • Use a DynamicBitSet or a free list to track available slots.
const OrderPool = struct {
    orders: []Order,
    free: DynamicBitSet,
    fn acquire(pool: *OrderPool) ?*Order { 
        /* logic to find free bit */ 
    }
};
  1. 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_capacity at startup.
  • Pre-allocate all necessary structures.
  • Implement explicit state transition functions.
  • Use assert to verify state invariants.