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.
int buf[64];
int x = buf[idx]; // silent overflow if idx >= 64int buf[64];
int x = buf[__prism_bchk((unsigned long long)(idx), 64ULL)];
// traps if idx >= 64The 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
| Pattern | Checked | Reason |
|---|---|---|
arr[i] — local fixed array | ✓ Yes | Primary case |
vla[i] — local VLA | ✓ Yes | sizeof(vla) evaluates at runtime |
arr[m[i]] — nested subscript in index | ✓ Both | Inner subscripts wrapped recursively |
int arr[100] — declarator bracket | ✗ No | Tagged as declarator, never wrapped |
sizeof(arr[i]), typeof(arr[i]) | ✗ No | Unevaluated operand — would spuriously trap on VLAs |
s.arr[i], p->arr[i] | ✗ No | Struct member — local array size unrelated |
&arr[i] — unary address-of | ✗ No | One-past-end address is legal C |
p[i] — pointer (not array) | ✗ No | Pointer bounds unknown at compile time |
arr[i] — array parameter | ✗ No | Parameters decay to pointers; size unknown |
gArr[i] — file-scope array | ✗ No | v1 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 lengthLimits (v1)
- Pointers not tracked — only local array variables.
int *p = arr; p[i]is not checked. - File-scope arrays not tracked — only block-scope declarations.
- Array parameters not tracked —
int a[10]as a parameter decays to a pointer. - 2D arrays: outer index only —
m[i][j]checksibut notj(inner[has]as the preceding token, not an identifier). - Commutative subscripts are a hard error —
idx[arr](wherearris the tracked array in the index position) cannot be safely checked with the v1 model and is rejected. Rewrite asarr[idx]or use-fno-bounds-check. orelsein subscripts —arr[x orelse 0]bypasses the bounds check (orelse is processed first). v1 limitation.
Opt-out
Disable globally:
prism -fno-bounds-check file.cThere 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.