29. Buffer overflows, safe string handling, and CERT C rules
29.1 Anatomy of a stack buffer overflow
#include <stdio.h>
#include <string.h>
void vulnerable(const char *input) {
char buf[16];
strcpy(buf, input); // BUG — no bound; if input is longer than 15 chars,
// this writes past buf, into adjacent stack memory,
// potentially overwriting the saved return address
}
On the stack, buf sits near the saved return address — the location
the CPU jumps to when vulnerable returns. A long enough input
overwrites that address with attacker-chosen bytes, and when the function
returns, execution jumps wherever the attacker wanted. This is the
classic "stack smashing" exploit primitive behind decades of remote-code-
execution CVEs, and exactly why:
void safer(const char *input) {
char buf[16];
snprintf(buf, sizeof buf, "%s", input); // bounded and always null-terminated when size > 0
}
29.2 CERT C's short list that catches most real bugs
| Rule (abbreviated) | In practice |
|---|---|
| STR31-C: Guarantee storage for strings is sufficient | Compute needed size before writing, or use bounded functions |
| ARR30-C/ARR38-C: Don't form/use out-of-bounds pointers | Check indices; never trust external input as an index |
| MEM30-C/MEM31-C: Don't access freed memory; free exactly once | Null out pointers after free (defense in depth), see Section 10.2 |
| INT30-C/INT32-C: Ensure unsigned/signed arithmetic doesn't wrap unexpectedly | Use __builtin_*_overflow or check bounds before arithmetic |
| EXP34-C: Don't dereference null pointers | Check every malloc/fopen/lookup return before using it |
| FIO34-C: Distinguish EOF from a valid data value | Check fgetc's return against EOF before casting to char |
29.3 A worked hardening example: bounded, checked input parsing
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
// Turns a real CVE-shaped bug ("trust the length byte from the network")
// into checked, bounded code.
int parse_length_prefixed(const unsigned char *data, size_t data_len,
unsigned char *out, size_t out_cap) {
if (data_len < 2) return -1; // not enough bytes for a length prefix
unsigned int claimed_len = (unsigned)(data[0] << 8 | data[1]); // 16-bit big-endian length
if (claimed_len > data_len - 2) return -1; // claimed length exceeds what we actually have
if (claimed_len > out_cap) return -1; // claimed length exceeds caller's buffer
memcpy(out, data + 2, claimed_len);
return (int)claimed_len;
}
Every one of those three if checks corresponds to a real historical
CVE class: trusting a length prefix without checking it against both the
actual buffer received and the destination buffer's capacity is the
Heartbleed pattern from Section 1.4, generalized.