CWE-787: Out-of-bounds Write¶
CWE Top 25 Rank: 2 (2024). Consistent top-3 since 2019. CVSS pattern: typically 7.5–10.0. Heap OOB write = code execution primitive in modern exploits.
Functional Semantics¶
A write operation targets memory address base + offset where offset >= allocated_size. The write lands in adjacent memory: another heap chunk's metadata, a stack frame's return address, or a global variable. The semantics are: the program believes it is writing to its own data; the CPU executes the write without fault; the process state is silently corrupted.
This is distinct from a segfault (which occurs only when the target address is unmapped). OOB writes within the same allocation arena succeed silently.
Consequence chain:
OOB write → corrupt adjacent data
→ if metadata (heap chunk header): heap corruption → malloc/free crash or controlled allocation
→ if return address (stack): control flow hijack
→ if vtable pointer: type confusion → arbitrary virtual dispatch
→ if adjacent buffer: data corruption, logic errors, secondary vulnerabilities
Root Causes¶
1. Off-by-one in loop bounds¶
// VULNERABLE: writes index n into buffer of size n
void copy_path(char *dst, const char *src, size_t n) {
size_t i;
for (i = 0; i <= n; i++) { // <= should be <
dst[i] = src[i];
}
}
dst[n] writes one byte past the allocation. On the stack this overwrites the next local variable or saved frame pointer.
// FIXED
void copy_path(char *dst, const char *src, size_t n) {
size_t i;
for (i = 0; i < n; i++) {
dst[i] = src[i];
}
dst[n - 1] = '\0'; // explicit null termination within bounds
}
2. Integer overflow in size calculation¶
// VULNERABLE: attacker controls nmemb and size
void *make_grid(size_t nmemb, size_t size) {
void *buf = malloc(nmemb * size); // overflow if nmemb=0x80000001, size=2 → allocates 2 bytes
if (!buf) return NULL;
memset(buf, 0, nmemb * size); // writes nmemb*size bytes into 2-byte buffer
return buf;
}
// FIXED: use calloc (handles overflow internally) or check explicitly
#include <stdint.h>
void *make_grid(size_t nmemb, size_t size) {
if (nmemb && size > SIZE_MAX / nmemb) return NULL; // overflow check
return calloc(nmemb, size); // calloc zeroes + checks overflow
}
3. Unbounded string copy¶
// VULNERABLE: strcpy/sprintf without length limit
char hostname[64];
void set_hostname(const char *user_input) {
strcpy(hostname, user_input); // OOB if input > 63 chars
}
// FIXED
void set_hostname(const char *user_input) {
snprintf(hostname, sizeof(hostname), "%s", user_input);
// OR: strlcpy(hostname, user_input, sizeof(hostname));
}
4. Rust unsafe slice indexing¶
// VULNERABLE
unsafe fn write_header(buf: &mut [u8], offset: usize, value: u32) {
let ptr = buf.as_mut_ptr().add(offset); // no bounds check
*(ptr as *mut u32) = value; // UB if offset + 4 > buf.len()
}
// FIXED: bounds check before unsafe, or use safe API
fn write_header(buf: &mut [u8], offset: usize, value: u32) -> Option<()> {
buf.get_mut(offset..offset + 4)?
.copy_from_slice(&value.to_le_bytes());
Some(())
}
Trigger Conditions¶
| Condition | Mechanism |
|---|---|
| External input controls array index | Direct index injection |
| External input controls allocation size | Integer overflow in n * sizeof(T) |
| External input controls loop bound | Off-by-one via crafted length field |
| Network/file-supplied length field < actual data | Write past truncated allocation |
| Signed/unsigned mismatch in index | Negative index wraps to large positive |
Signed/unsigned mismatch example:
// VULNERABLE: signed comparison with unsigned loop variable
int8_t index = get_user_index(); // attacker sends -1
if (index < MAX_ITEMS) { // -1 < 10 → true
table[index] = value; // table[-1] = write before array
}
Affected Ecosystems¶
| Language | Risk | Notes |
|---|---|---|
| C | Critical | No bounds checking anywhere in stdlib by default |
| C++ | Critical | STL operator[] unchecked; .at() throws but rarely used |
| Rust (safe) | None | Panics on OOB at runtime |
| Rust (unsafe) | High | ptr::write, slice::from_raw_parts_mut have no checks |
| Go | Low | Runtime panics on OOB slice write; unsafe.Pointer arithmetic bypasses this |
| Java/Python/JS | Very Low | Managed runtimes; array OOB = exception, not corruption |
| Assembly | Critical | No runtime protection |
Detection Heuristics¶
High-signal patterns (review immediately):
memcpy(dst, src, len)wherelenderives from external input without alen <= sizeof(dst)guard immediately before.strcpy,strcat,gets,sprintf(withoutnvariants) - treat as automatic findings.- Array subscript
arr[i]whereiisintorsize_tderived from a network/file/IPC source. malloc(a * b)ormalloc(a + b)where either operand is external input - overflow possible.- Pointer arithmetic:
ptr + offsetwhereoffsetis external and there is noptr + offset < ptr + alloc_sizecheck. for (i = 0; i <= len; i++)- off-by-one;<=should almost always be<.
Static analysis anchors: - Taint: source = read(), recv(), fread(), getenv(), argv[] - Sink: memcpy dst, strcpy dst, array subscript write, pointer dereference write - Check: is taint sanitized (bounds checked) on all paths between source and sink?
Exploitability Factors¶
| Factor | Impact on exploitability |
|---|---|
| Heap OOB vs stack OOB | Heap: ASLR/PIE bypass needed; Stack: often direct RIP/EIP control |
| Write size | 1 byte (off-by-one): hard but possible (House of Einherjar); arbitrary: straightforward |
| Adjacent data | Heap metadata > vtable ptr > return addr in terms of control |
| ASLR/PIE enabled | Increases difficulty; info leak (CWE-125) typically used to defeat |
| Stack canaries | Detects stack buffer overflow before return; heap OOB unaffected |
Fixing Patterns¶
| Pattern | Application |
|---|---|
Use n-bounded functions | strncpy, snprintf, strncat, fgets over unbounded equivalents |
calloc for array allocation | Handles nmemb * size overflow check per C11 |
| Explicit pre-condition check | assert(offset + len <= buf_size) or if guard returning error |
| Address Sanitizer (ASan) in tests | -fsanitize=address catches OOB at runtime in test suites |
| Fortify Source | -D_FORTIFY_SOURCE=2 adds compile-time and runtime checks for string functions |
| Static analysis | Coverity, CodeQL cpp/overflow-buffer, PVS-Studio V531, Semgrep c.lang.security.buffer-not-null-terminated |
| Safe wrappers | strlcpy/strlcat (OpenBSD), memcpy_s (C11 Annex K) |
Gotchas - False Positive Indicators¶
- Ring buffer modulo:
buf[i % SIZE]- always in-bounds ifSIZEmatchessizeof(buf)/sizeof(buf[0]). Verify the modulus matches the allocation size exactly. - Sentinel-terminated copy:
while (*src) dst[i++] = *src++;- looks dangerous but caller may guaranteestrlen(src) < sizeof(dst). Verify contract at call site. - Known-size stack local + sizeof guard:
memcpy(local, src, sizeof(local))withlocalbeing a fixed stack array - this is safe; sizeof gives compile-time size. - Two-phase alloc+fill:
malloc(n)thenmemset(p, 0, n)with the samen- safe; the common dangerous pattern is where the fill uses a differentnfrom the alloc. - Compiler-inserted array size: GCC/Clang
-Warray-boundsmay catch some but not dynamic cases; absence of warning does not mean absence of bug.
See Also¶
- CWE-125: Out-of-Bounds Read - read-side counterpart; often paired in Heartbleed-style bugs
- CWE-416: Use After Free - another memory corruption class; often chained with OOB write
- CWE-190: Integer Overflow - root cause for size-calculation OOB writes
- CWE-119: Buffer Errors - parent CWE
- CWE-121: Stack Overflow - stack-specific subtype
- CWE-122: Heap Overflow - heap-specific subtype