← Prism
Prism
Overview Spec Draft Releases
Features
defer orelse zeroinit raw bounds-check auto-unreachable auto-static
GitHub ↗

bounds-check

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 (CWE-787, CWE-125) remain one of the most exploited vulnerability classes in C. 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 with zero overhead on the happy path and zero source changes. Every local array subscript is 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 local array is rewritten to:

arr[__prism_bchk((unsigned long long)(idx), sizeof(arr)/sizeof(arr[0]))]

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((unsigned long long)(idx), 64ULL)];
// 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✗ Nov1 limitation — only local arrays tracked

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