Skip to main content

Defensive C

intermediate16 min readLesson 139 of 148

The reflexes that keep code on the defined side: pre-checks, bounded APIs, initialization, and the sanitize-the-input posture.

Pre-check instead of post-detect

Every UB trap has a defined-side pre-check:

/* overflow */   if (a > 0 && b > INT_MAX - a) ...
/* shift */      if (k < 32) x = (unsigned)x << k;
/* bounds */     if (i < n) use(a[i]);
/* null */       if (p && p->next) ...

The check and the use must be made of different operations โ€” checking a + b < 0 before computing a + b is checking with the UB itself.

Bounded by default

Every buffer-carrying call names its capacity. snprintf over sprintf; memcpy(dst, src, n) where n is computed from the destination's capacity, not the source's promise. The bounded call is never slower by an amount that matters and never the bug's origin.

Initialize at the declaration

int x = 0;                     /* not "int x;" */
TNode *n = calloc(1, sizeof *n);   /* zeroed: fields have defined values */
char buf[64] = {0};

calloc for structs whose fields must start defined; explicit initializers for stack arrays. The cost is unmeasurable; the UB class it deletes is real.

The honest test posture

Because UB cannot be relied on to fail (it may work on your machine today), tests assert the defined replacement's behavior: sat_add returns INT_MAX at the boundary, parse rejects the overflow input, the shift helper clamps. The W-solution pattern in this course is exactly that: Ws are defined but wrong โ€” they misparse, drop, or corrupt in ways tests deterministically catch. Graded code never depends on UB failing; it depends on correct behavior being verifiable.

Now practice

Defensive GymBounded APIs that refuse to trust their callers.1 challenge ยท ยท ~26 min