Skip to content

Lesson 2 of 9 · 25m

Pointer Arithmetic, Array Decay, and Bounds Discipline

p + n moves elements, not bytes; sizeof lies at function boundaries; walk untrusted buffers without walking off them.

Half the protocol parsers in production C walk a uint8_t * across a buffer pulled off a socket. Get the arithmetic wrong by one element and you've shipped an out-of-bounds read; misunderstand sizeof and you've shipped an overflow. Pointer arithmetic is simple — one rule, really — and still the source of a remarkable share of CVEs.

The rule: p + n advances by n elements, i.e. n * sizeof(*p) bytes. For int *p, p + 3 moves 12 bytes on a 32-bit-int platform. Indexing is sugar on top: p[i] is defined as *(p + i), which is why the perverse i[p] also compiles — commutativity of +. Subtraction of two pointers into the same array gives a ptrdiff_t element count, not a byte count.

The second rule trips more people: an array decays to a pointer to its first element in almost every expression — but not for sizeof. Inside a function that takes char buf[64] as a parameter, sizeof buf is sizeof(char *) (8 on x86-64), because the parameter is really a pointer. The 64 in the signature is decorative.

#include <stdint.h>
#include <string.h>

/* Walk length-prefixed records: [u16 len][payload]... */
const uint8_t *next_rec(const uint8_t *cur, const uint8_t *end,
                        size_t *payload_len) {
    if (end - cur < 2) return NULL;           /* header fits? */
    uint16_t len;
    memcpy(&len, cur, sizeof len);            /* unaligned-safe read */
    cur += 2;
    if ((size_t)(end - cur) < len) return NULL; /* payload fits? */
    *payload_len = len;
    return cur + len;                          /* one past this record */
}

Three details carry the security weight. First, bounds are checked with end - cur before advancing — a pointer may legally point one element past the end of an array, but anything further (or dereferencing that one-past pointer) is undefined behavior. Second, the memcpy for len is not timid style: casting cur to uint16_t * can misalign the read (UB on strict-alignment targets) and violates effective-type rules. GCC and Clang compile the memcpy to a single load anyway. Third, uint8_t/unsigned char is the right walk type because character types may alias any object — casting a byte buffer to a struct pointer and dereferencing it is the aliasing bug that keeps shipping.

Try this

Write a main that feeds next_rec a hand-built byte string with a truncated final record (len says 10, buffer holds 6). Verify it returns NULL instead of walking off the end — then remove the second bounds check, rebuild with -fsanitize=address, and watch the report land on cur + len/memcpy. That failure you just produced is the exact shape of real over-read CVEs.

Field notes

  • sizeof(array) vs sizeof(pointer) is the single most common C interview question for a reason: it causes real bugs at function boundaries.
  • Never compare p < end after p has already run past end — forming the out-of-range pointer is itself UB. Keep end and subtract.
  • ptrdiff_t can be negative; size_t can't. Know which type your difference lands in before comparing it to anything.