Frontend Labs
⋯

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.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36