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)vssizeof(pointer)is the single most common C interview question for a reason: it causes real bugs at function boundaries.- Never compare
p < endafterphas already run pastend— forming the out-of-range pointer is itself UB. Keependand subtract. ptrdiff_tcan be negative;size_tcan't. Know which type your difference lands in before comparing it to anything.