Skip to content
← Prism
Prism
Overview
Features
defer orelse zeroinit raw bounds-check auto-unreachable auto-static check
C in practice
Overview goto skips init tag namespace struct padding strict aliasing char signedness signed overflow VLAs segfaults
Spec Draft Releases Blog
GitHub ↗

bounds-checkruntime array bounds checking for C

Prism wraps local array subscripts with a runtime bounds check. Out-of-bounds accesses call __builtin_trap(): no source changes required.

opt-out: prism -fno-bounds-check

#Overview

Buffer overflows from unchecked array subscripts are still a common exploit class in C (CWE-787, CWE-125). The C standard does not require subscript bounds to be validated. ASan catches these at runtime with heavy shadow-memory overhead. Static analyzers find some at compile time. Neither is on by default.

Prism adds bounds checking by default with no source changes. The failure path traps; the happy path is a checked index. Local, static and complete file-scope array subscripts are wrapped with a helper that traps on out-of-bounds access: for both fixed-size arrays and VLAs.

#Behavior

A subscript arr[idx] on a tracked array is rewritten to:

arr[__prism_bchk((__prism_bchk_size_t)(idx), sizeof(arr)/sizeof(arr[0]))]

__prism_bchk_size_t is a typedef emitted once per translation unit: unsigned long long on GCC/Clang, unsigned __int64 on MSVC. It is not size_t, because flatten mode feeds the backend already-preprocessed text where __SIZE_TYPE__ would not expand, and unsigned long would truncate on LLP64.

If idx >= len, __prism_bchk calls __builtin_trap() (or __debugbreak() + abort() on MSVC). Otherwise it returns idx unchanged. The unsigned cast maps negative indices to large positive values, which fail the >= check and trap.

Before: unchecked
int buf[64];
int x = buf[idx];  // silent overflow if idx >= 64
After: bounds checked
int buf[64];
int x = buf[__prism_bchk((__prism_bchk_size_t)(idx),
                         sizeof(buf)/sizeof(buf[0]))];
// traps if idx >= 64

The helper is emitted once per translation unit as a static inline function. __builtin_expect marks the failure branch cold: the happy path has near-zero impact on branch prediction.

For VLAs, sizeof(arr)/sizeof(arr[0]) is evaluated at runtime (C99 §6.5.3.4), so the check always uses the correct length regardless of how the VLA was sized.

#What gets checked

PatternCheckedReason
arr[i]: local fixed array✓ YesPrimary case
vla[i]: local VLA✓ Yessizeof(vla) evaluates at runtime
arr[m[i]]: nested subscript in index✓ BothInner subscripts wrapped recursively
int arr[100]: declarator bracket✗ NoTagged as declarator, never wrapped
sizeof(arr[i]), typeof(arr[i])✗ NoUnevaluated operand: would spuriously trap on VLAs
s.arr[i], p->arr[i]✗ NoStruct member: local array size unrelated
&arr[i]: unary address-of✗ NoOne-past-end address is legal C
p[i]: pointer (not array)✗ NoPointer bounds unknown at compile time
arr[i]: array parameter✗ NoParameters decay to pointers; size unknown
gArr[i]: file-scope array✓ YesTracked like a local; length from sizeof

#Examples

void process(int n) {
    int buf[64];
    int vla[n];

    buf[0]   = 1;    // checked: 0 < 64, ok
    buf[63]  = 1;    // checked: 63 < 64, ok
    buf[64]  = 1;    // checked: 64 >= 64, TRAP
    buf[-1]  = 1;    // checked: wraps to huge uint, TRAP
    vla[n-1] = 1;    // checked at runtime: n-1 < n, ok
    vla[n]   = 1;    // checked at runtime: n >= n, TRAP
}

Nested and recursive subscripts are both wrapped:

int matrix[8][8];
int idx[4] = {0, 1, 2, 3};

// Outer subscript checked:
matrix[i][j] = 0;       // i checked against 8

// Both subscripts in arr[m[i]] checked:
int val = data[idx[i]]; // i checked against 4, result checked against data's length

#Limits (v1)

#Opt-out

Disable globally:

prism -fno-bounds-check file.c

There is no per-subscript opt-out. If a specific call site uses a commutative subscript pattern that Prism rejects, rewrite it to arr[idx] form or disable bounds checking for that translation unit.

#Spec Reference

bounds-check: Prism Spec §6.10