Safety-Critical AI

The Real Cost of Dynamic Memory in Safety-Critical Systems

Why malloc is problematic for certification and how static allocation can simplify verification

Published
January 15, 2026 19:20
Reading Time
10 min
Comparison of heap fragmentation over time versus static allocation with predictable memory layout

Dynamic memory allocation is fundamental to modern software. Languages and frameworks assume heap access. Data structures grow and shrink as needed. Memory management happens automatically or through simple malloc/free pairs.

In safety-critical systems, this flexibility becomes a liability.

Certification standards like DO-178C (aerospace), IEC 62304 (medical devices), and ISO 26262 (automotive) impose requirements that make dynamic allocation difficult to verify and, in the most stringent classifications, effectively prohibited. Understanding why requires examining what dynamic allocation actually does at runtime and why those behaviours conflict with certification objectives.

This article explores the technical problems with dynamic memory in safety-critical contexts, examines how certification standards address these concerns, and demonstrates static allocation patterns that can satisfy verification requirements for neural network inference.

What Dynamic Allocation Does

When code calls malloc(size), the runtime memory allocator must:

  1. Search for a free block of sufficient size
  2. Split the block if it’s larger than needed (in most implementations)
  3. Update internal bookkeeping structures
  4. Return a pointer to the allocated region

When code calls free(ptr), the allocator must:

  1. Validate the pointer (in robust implementations)
  2. Mark the block as available
  3. Coalesce adjacent free blocks (in most implementations)
  4. Update bookkeeping structures

These operations have properties that create challenges for safety-critical systems.

Variable Execution Time

The time to complete malloc depends on the current state of the heap. A fresh heap with large contiguous free space satisfies requests quickly. A fragmented heap may require searching through many small blocks before finding one that fits.

This variability makes worst-case execution time (WCET) analysis difficult. Safety-critical systems often require bounded timing: the system must respond within a guaranteed deadline. If malloc can take 10μs in the best case and 10ms in the worst case, the system must be designed for the 10ms case, wasting capacity in normal operation.

Fragmentation

Repeated allocation and deallocation of varying sizes fragments the heap. Free memory exists, but in pieces too small to satisfy requests. A system with 1MB free might fail to allocate 64KB because no single contiguous region remains.

Fragmentation is non-deterministic. It depends on the exact sequence of allocations and frees, which may depend on input data, timing, or external events. Two runs of the same program with different inputs can produce different fragmentation patterns, causing one to succeed and the other to fail.

Failure Modes

When malloc cannot satisfy a request, it returns NULL. Correct handling of allocation failure is notoriously difficult:

// Common pattern - often wrong
char* buffer = malloc(size);
if (buffer == NULL) {
    // Now what? 
    // - Return error? (caller must handle)
    // - Log and continue? (with what buffer?)
    // - Abort? (acceptable in safety-critical?)
}

Every allocation site requires failure handling. Every failure handler must be tested. In a system with hundreds of allocation points, this creates a verification burden that grows with code complexity.

Hidden State

The heap is global mutable state shared across the entire program. An allocation in one module affects available memory for all other modules. A memory leak in a rarely-executed path may not manifest until hours or days into operation.

This hidden coupling makes reasoning about system behaviour difficult. Two modules that appear independent may interact through heap exhaustion in ways that are hard to predict and harder to test.

What Certification Standards Require

Certification standards address these concerns with varying degrees of strictness.

DO-178C (Aerospace)

DO-178C does not explicitly prohibit dynamic allocation, but its objectives create significant barriers at higher Design Assurance Levels (DAL).

Objective A-5 requires demonstration that the software “does not have unintended functions.” Dynamic allocation’s dependence on runtime state makes this difficult to show exhaustively.

Objective A-7 requires verification that the software performs its intended functions. If allocation can fail, the failure paths must be verified, including demonstration that the system remains safe when memory is exhausted.

For DAL A (catastrophic failure conditions), the verification burden is so high that most projects prohibit dynamic allocation after initialisation as a practical matter. The CAST-21 position paper from certification authorities explicitly addresses dynamic memory, noting that its use “ichever requires specific means to show compliance.”

IEC 62304 (Medical Devices)

IEC 62304 Class C (highest safety classification) requires:

  • Risk analysis of each software item
  • Verification that software items meet requirements
  • Traceability from requirements through design to tests

Dynamic allocation introduces risks (fragmentation, exhaustion, timing variance) that must be analysed, mitigated, and verified. For life-sustaining systems, the analysis overhead often exceeds the cost of redesigning with static allocation.

ISO 26262 (Automotive)

ISO 26262 Part 6 Table 1 lists “No dynamic objects or variables” as a recommendation for ASIL C and ASIL D systems. While not an absolute prohibition, deviating from recommendations requires documented rationale and alternative measures.

The standard’s emphasis on deterministic timing and freedom from interference aligns poorly with heap allocation’s variable timing and global state.

Design Property: Analysability

Static allocation enables complete analysis of memory usage at compile time. Maximum memory consumption is known before the software runs, eliminating runtime exhaustion as a failure mode.

The Static Allocation Alternative

Static allocation means all memory is reserved at compile time or during initialisation, with no runtime allocation during normal operation. This approach trades flexibility for analysability.

Caller-Provided Buffers

Instead of functions allocating their own memory, callers provide buffers:

// Dynamic allocation pattern
result_t* process_data(const input_t* input) {
    result_t* result = malloc(sizeof(result_t));
    if (result == NULL) return NULL;
    // ... process ...
    return result;  // Caller must free
}

// Static allocation pattern
int process_data(const input_t* input, result_t* result) {
    // result buffer provided by caller
    // ... process ...
    return 0;  // Success
}

The caller controls memory lifetime. The function cannot fail due to allocation. Memory ownership is explicit rather than implicit.

Fixed-Size Arrays

Where dynamic arrays might grow, static allocation uses fixed maximum sizes:

// Dynamic pattern
typedef struct {
    float* weights;  // malloc'd, size varies
    int size;
} layer_t;

// Static pattern
#define MAX_WEIGHTS 1024

typedef struct {
    float weights[MAX_WEIGHTS];  // Fixed at compile time
    int used;                     // Actual count in use
} layer_t;

This wastes memory when actual usage is below the maximum. For safety-critical systems, the trade-off is often acceptable: memory is cheap compared to verification effort.

Object Pools

For systems that genuinely need runtime object creation, pools pre-allocate a fixed number of objects:

#define POOL_SIZE 32

typedef struct {
    message_t messages[POOL_SIZE];
    uint8_t available[POOL_SIZE];  // 1 = free, 0 = in use
    int next_free;
} message_pool_t;

message_t* pool_acquire(message_pool_t* pool) {
    for (int i = 0; i < POOL_SIZE; i++) {
        int idx = (pool->next_free + i) % POOL_SIZE;
        if (pool->available[idx]) {
            pool->available[idx] = 0;
            pool->next_free = (idx + 1) % POOL_SIZE;
            return &pool->messages[idx];
        }
    }
    return NULL;  // Pool exhausted
}

void pool_release(message_pool_t* pool, message_t* msg) {
    int idx = msg - pool->messages;
    pool->available[idx] = 1;
}

Pool exhaustion is still possible, but the maximum is known statically. Testing can verify behaviour when all objects are in use. There is no fragmentation because all objects are the same size.

Application to Neural Network Inference

Neural network inference is particularly amenable to static allocation because the memory requirements are known at model load time.

Model Structure

A neural network has fixed structure:

  • Layer count is fixed
  • Weight dimensions per layer are fixed
  • Maximum activation sizes between layers are fixed

All of this is known when the model is deployed. There is no need for runtime allocation.

Inference Buffers

Inference requires temporary buffers for intermediate activations. Using fixed-point arithmetic for determinism, the maximum size is determined by the largest layer:

// Determined at model compile time
#define MAX_ACTIVATION_SIZE 4096

typedef struct {
    fixed_t weights[MAX_WEIGHTS];
    fixed_t biases[MAX_BIASES];
    fixed_t activation_a[MAX_ACTIVATION_SIZE];
    fixed_t activation_b[MAX_ACTIVATION_SIZE];
} inference_ctx_t;

void infer(inference_ctx_t* ctx, const fixed_t* input, fixed_t* output) {
    // Ping-pong between activation buffers
    const fixed_t* current = input;
    fixed_t* next = ctx->activation_a;
    
    for (int layer = 0; layer < NUM_LAYERS; layer++) {
        apply_layer(ctx, layer, current, next);
        current = next;
        next = (next == ctx->activation_a) ? ctx->activation_b : ctx->activation_a;
    }
    
    memcpy(output, current, OUTPUT_SIZE * sizeof(fixed_t));
}

Two activation buffers suffice for any depth network by alternating between them. The maximum size is the largest layer, not the sum of all layers.

Convolution Buffers

2D convolution can be implemented with static buffers:

void fx_conv2d(const fixed_t* input, int in_h, int in_w,
               const fixed_t* kernel, int k_h, int k_w,
               fixed_t* output) {
    int out_h = in_h - k_h + 1;
    int out_w = in_w - k_w + 1;
    
    // No allocation - all pointers provided by caller
    for (int oh = 0; oh < out_h; oh++) {
        for (int ow = 0; ow < out_w; ow++) {
            int64_t acc = 0;
            for (int kh = 0; kh < k_h; kh++) {
                for (int kw = 0; kw < k_w; kw++) {
                    int ih = oh + kh;
                    int iw = ow + kw;
                    acc += (int64_t)input[ih * in_w + iw] * 
                           (int64_t)kernel[kh * k_w + kw];
                }
            }
            output[oh * out_w + ow] = (fixed_t)(acc >> 16);
        }
    }
}

The function uses only stack variables (loop counters, accumulator) and caller-provided buffers. Stack usage is O(1), predictable and analysable.

Verification Benefits

Static allocation simplifies verification in several ways.

Memory Usage Analysis

With static allocation, maximum memory usage is computable at compile time:

Total RAM = sizeof(inference_ctx_t) 
          + sizeof(input_buffer) 
          + sizeof(output_buffer)
          + stack_high_water_mark

This total can be compared against available RAM with certainty. There is no “usually enough” or “depends on input.” Either the memory fits or it doesn’t.

Stack Analysis

Static allocation tools can compute precise stack usage for each function and call path. Combined with static buffers, the total memory footprint is fully characterised without running the code.

No Exhaustion Testing

If memory cannot be exhausted at runtime, exhaustion handling does not require testing. This eliminates a class of difficult-to-reach test cases and associated verification evidence.

Timing Predictability

Without malloc’s variable search time, timing analysis becomes tractable. Loop bounds are known. Memory access patterns are predictable. WCET can be computed or measured with confidence.

Trade-offs

Static allocation is not without costs.

Static Allocation Benefits
  • Compile-time memory analysis
  • No fragmentation
  • No exhaustion during operation
  • Predictable timing
  • Simplified verification
Static Allocation Costs
  • Memory sized for worst case
  • Maximum sizes must be known in advance
  • Less flexible data structures
  • May require architectural changes
  • Not suitable for all applications

For applications where input sizes vary enormously (text processing, general-purpose computing), static allocation may be impractical. For embedded inference with known model dimensions, the constraints are typically acceptable.

Implementation Patterns

Initialisation-Time Allocation

Some systems permit allocation during initialisation but not during operation:

typedef struct {
    fixed_t* weights;
    fixed_t* activations;
    int initialised;
} runtime_ctx_t;

int runtime_init(runtime_ctx_t* ctx, const model_config_t* cfg) {
    // Allocation permitted here
    ctx->weights = malloc(cfg->weight_bytes);
    ctx->activations = malloc(cfg->activation_bytes);
    
    if (!ctx->weights || !ctx->activations) {
        free(ctx->weights);
        free(ctx->activations);
        return -1;  // Initialisation failure is recoverable
    }
    
    ctx->initialised = 1;
    return 0;
}

int runtime_infer(runtime_ctx_t* ctx, const fixed_t* in, fixed_t* out) {
    // No allocation here - operation phase
    assert(ctx->initialised);
    // ... inference using pre-allocated buffers ...
    return 0;
}

This pattern separates startup (where failure is recoverable) from operation (where allocation failure could be catastrophic). Certification focuses on demonstrating that no allocation occurs after initialisation.

Compile-Time Configuration

For maximum analysability, buffer sizes can be compile-time constants:

// config.h - generated from model analysis
#define INPUT_SIZE   784
#define HIDDEN_SIZE  256
#define OUTPUT_SIZE  10
#define MAX_ACTIVATION MAX(INPUT_SIZE, MAX(HIDDEN_SIZE, OUTPUT_SIZE))

// inference.c
static fixed_t g_activation_a[MAX_ACTIVATION];
static fixed_t g_activation_b[MAX_ACTIVATION];

The sizes are visible in the source. Static analysis tools can compute total memory without execution. Changes to the model require recompilation, but the memory contract is explicit.

Practical Considerations

Memory Overhead

Static allocation typically uses more memory than dynamic allocation would for the same workload. The overhead is the difference between worst-case and average-case usage.

For neural network inference, the overhead is often modest. Layer sizes don’t vary at runtime; they’re fixed by the model architecture. The “worst case” is every case.

Code Portability

Static allocation patterns require larger buffers to be passed through call chains. This can make APIs more verbose:

// Dynamic style - cleaner API
image_t* resize(const image_t* src, int new_w, int new_h);

// Static style - explicit buffers
int resize(const image_t* src, image_t* dst, 
           int new_w, int new_h,
           uint8_t* scratch, size_t scratch_size);

The verbosity is the cost of explicitness. For safety-critical code, explicit memory management is typically preferred despite the syntactic overhead.

Legacy Integration

Existing codebases may assume dynamic allocation. Migration to static allocation can require significant refactoring. For new safety-critical projects, designing for static allocation from the start is substantially easier than retrofitting.

Implementation Reference

The certifiable-inference project demonstrates static allocation throughout:

  • All buffers provided by callers
  • No malloc after initialisation
  • Fixed-size structures with compile-time dimensions
  • O(1) stack usage in all operations

The implementation is suitable for environments where dynamic allocation is prohibited and provides a reference for teams designing their own safety-critical inference engines.

Conclusion

Dynamic memory allocation introduces variability that conflicts with safety-critical certification objectives. Timing varies with heap state. Fragmentation can cause late-life failures. Exhaustion handling creates verification burden.

Static allocation eliminates these concerns by moving memory decisions to compile time. The cost is reduced flexibility and potential memory overhead. For applications with known memory requirements, like neural network inference with fixed model dimensions, the trade-off typically favours static allocation.

Certification standards increasingly recognise this trade-off. While few explicitly prohibit dynamic allocation, the verification burden it creates pushes safety-critical projects toward static patterns. Understanding why helps engineers make informed architectural decisions early in development, when the cost of change is lowest.

As with any architectural approach, suitability depends on system requirements, memory constraints, and regulatory context. Static allocation is not universally appropriate, but for safety-critical AI where certification is required, it offers a verification-friendly foundation.


For a working implementation of static allocation patterns for neural network inference, see certifiable-inference or try the live simulator.

About the Author

William Murray is a Regenerative Systems Architect with 30 years of UNIX infrastructure experience, specializing in deterministic computing for safety-critical systems. Based in the Scottish Highlands, he operates SpeyTech and maintains several open-source projects including C-Sentinel and c-from-scratch.

Discuss This Perspective

For technical discussions or acquisition inquiries, contact SpeyTech directly.

Get in touch
← Back to Insights