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.
int buf[64];
int x = buf[idx]; // silent overflow if idx >= 64int buf[64];
int x = buf[__prism_bchk((__prism_bchk_size_t)(idx),
sizeof(buf)/sizeof(buf[0]))];
// 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 | ✓ Yes | Tracked 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)
- Pointers not tracked: only local array variables.
int *p = arr; p[i]is not checked. - File-scope and
staticarrays are tracked: block scope, file scope, and block-scopestaticall get the check. The array must be complete:extern int a[];has no length to divide by, so it is left bare, whileint a[][4] = {{1,2,3,4}};is complete once the initializer fixes the outer dimension. - Array parameters not tracked:
int a[10]as a parameter decays to a pointer. - Every dimension is checked:
m[i][j]wraps both indices,m[i][j][k]wraps all three. Each uses the length of its own rank, so the inner check dividessizeof(m[0])bysizeof(m[0][0]). - 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 is checked:arr[x orelse 0]lowers to a ternary first, then the whole ternary is wrapped, so the substituted fallback is bounds-checked too.
#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.