C / UNDEFINED BEHAVIOUR AND DEFENSIVE C
Defensive C: invariants, checks and safe integer habits
Write guards that run before the operation: overflow tests built from INT_MAX and SIZE_MAX, safe unsigned comparisons, and assert versus error returns.
What you will learn
- Rewrite overflow tests with the limits: a > INT_MAX - b, n > SIZE_MAX / size
- Compare before subtracting unsigned values: use n > len, never len - n > 0
- Use assert for invariants you guarantee, error returns for untrusted input
- Keep sizes in size_t and never compare a possibly negative int against one
Understanding Defensive C: invariants, checks and safe integer habits
Every operation in C has a domain: the set of operand values for which it gives the answer you meant. a + b on ints is meaningful only while the mathematical sum fits in int, len - off on size_t only while off is at most len, and p[i] only while i indexes real storage. Defensive C means proving the operands are inside that domain before you apply the operator, because afterwards there is nothing trustworthy left to look at: the signed sum has no defined value at all, and the unsigned difference has a perfectly defined value that is nowhere near the right one. So the test has to be phrased in terms of the inputs and the type's limits, using arithmetic that cannot itself fail: a > INT_MAX - b instead of a + b > INT_MAX, and n > SIZE_MAX / size instead of n * size > SIZE_MAX.
Not every condition deserves the same tool. A condition your own code guarantees, such as a ring buffer's head index staying below capacity or a static helper never being called with a null pointer, is an invariant; if it is false you have a bug, and assert is right because it documents the claim, catches the violation in debug builds, and costs nothing once NDEBUG is defined. A condition controlled from outside the program, such as a length field from a file, a number parsed out of argv, or a count from a socket, is not an invariant and must be checked by an ordinary if that returns an error, because assert compiles away and aborting is not a sane response to input that is merely malformed. Conditions fixed at compile time, such as a table having exactly as many rows as the enum that indexes it, belong in static_assert, which cannot be compiled out because it is checked before the program exists.
Most integer accidents in C come from the operand types rather than from the arithmetic. size_t is unsigned, so a subtraction that should go negative wraps to something near SIZE_MAX instead, and comparing a signed value against a size_t converts the signed side first, which turns -1 into SIZE_MAX and makes i < n true when you expected false. Keeping sizes and indices in size_t, keeping values that can legitimately be negative in a signed type, and refusing to let the two meet in a comparison without a deliberate range check removes most of this class of bug. Where the quantity you need is a count times an element size, calloc already performs the multiplication guard, so using it is both shorter and correct on every target.
<stdio.h>
<stdbool.h>
<limits.h>
/* Returns false and leaves *out alone when the mathematical sum
a + b would not fit in an int. Nothing overflows on the way. */
static bool safe_add(int a, int b, int *out)
{
if (b > 0 && a > INT_MAX - b) return false;
if (b < 0 && a < INT_MIN - b) return false;
*out = a + b;
return true;
}
int main(void)
{
int pairs[][2] = {
{ 1500, 27 },
{ 2000000000, 200000000 },
{ -2000000000, -200000000 },
{ INT_MAX, 0 }
};
size_t count = sizeof pairs / sizeof pairs[0];
for (size_t i = 0; i < count; i++) {
int a = pairs[i][0], b = pairs[i][1], sum;
if (safe_add(a, b, &sum))
printf("%d + %d = %d\n", a, b, sum);
else
printf("%d + %d refused: does not fit in int\n", a, b);
}
return 0;
}
A check is only worth writing if it runs before the operation it protects and is built from arithmetic that cannot fail itself.
Worked examples
Compare, then subtract
Shows why a size_t difference cannot be sanity-checked after the fact, and what the guard looks like instead.
<stdio.h>
<string.h>
/* Tail of s starting at byte n, or NULL if s is shorter than n.
On success *left receives the number of bytes after the offset. */
static const char *tail(const char *s, size_t n, size_t *left)
{
size_t len = strlen(s);
if (n > len) return NULL; /* compare first, never subtract first */
*left = len - n; /* now the subtraction cannot wrap */
return s + n;
}
int main(void)
{
const char *s = "abc";
size_t len = strlen(s), left = 0;
const char *p;
printf("unguarded len - 5: %zu\n", len - 5);
p = tail(s, 5, &left);
printf("tail(s, 5): %s\n", p ? "pointer" : "NULL");
p = tail(s, 1, &left);
printf("tail(s, 1): \"%s\", left = %zu\n", p, left);
return 0;
}
Example explained
Line 1len - 5 is computed in size_t, so 3 - 5 wraps modulo 2^64 rather than going negative; the value is well defined and useless.
Line 2A later test such as (len - 5) > 0 would pass, which is why post-subtraction sanity checks cannot catch this at all.
Line 3n > len compares two values that already exist, so the guard itself cannot wrap, and after it len - n is the real difference.
Line 4tail(s, 5) hands the caller a NULL it can test instead of a pointer five bytes past a three-byte string.
Guarding a count times a size
Demonstrates the division-based check that rejects an allocation request before the multiplication wraps.
<stdio.h>
<stdlib.h>
<stdint.h>
/* Refuses the request instead of handing malloc a wrapped size. */
static void *alloc_array(size_t n, size_t size)
{
if (size != 0 && n > SIZE_MAX / size) return NULL;
return malloc(n * size);
}
int main(void)
{
size_t n = SIZE_MAX / 4 + 1; /* 2^62 on a 64-bit target */
int *v;
printf("n * 4 as size_t: %zu\n", n * 4);
printf("alloc_array(n, 4): %s\n",
alloc_array(n, 4) ? "allocated" : "refused");
v = alloc_array(8, sizeof *v);
printf("alloc_array(8, %zu): %s\n", sizeof *v, v ? "allocated" : "refused");
free(v);
return 0;
}
Example explained
Line 1SIZE_MAX / size is exact and cannot overflow, so it yields the largest usable n before any multiplication happens.
Line 2n set to SIZE_MAX / 4 + 1 makes n * 4 exactly 2^64, which wraps to 0, so an unguarded malloc could return a tiny block while the caller believes it owns 2^62 elements.
Line 3The size != 0 test exists only to keep the division defined for a zero element size.
Line 4calloc(n, size) performs the same check internally, so reach for it whenever zeroed memory is acceptable.
Invariant versus expected failure
Separates a condition that can only be a bug from a condition that happens in correct programs.
<assert.h>
<stdbool.h>
<stdio.h>
CAP
struct ring {
int buf[CAP];
size_t head; /* invariant: head < CAP */
size_t count; /* invariant: count <= CAP */
};
static bool ring_push(struct ring *r, int v)
{
assert(r != NULL); /* caller bug if it fires */
assert(r->head < CAP && r->count <= CAP); /* our own invariant */
if (r->count == CAP) return false; /* normal and recoverable */
r->buf[(r->head + r->count) % CAP] = v;
r->count++;
return true;
}
int main(void)
{
struct ring r = { .head = 0, .count = 0 };
for (int v = 1; v <= 6; v++)
printf("push %d: %s\n", v, ring_push(&r, v) ? "stored" : "full");
printf("count = %zu\n", r.count);
return 0;
}
Example explained
Line 1assert(r != NULL) records a precondition the caller must meet; a null pointer here is a programming error, not data to validate.
Line 2A full buffer is not an invariant violation, so it returns false rather than aborting the process.
Line 3Both asserts disappear under -DNDEBUG, so nothing with an effect lives inside them: the store and count++ stay outside.
Line 4count = 4 confirms the fifth and sixth pushes were refused without any write past buf[3].
Important notes
assert calls abort, so it is a debugging tool for states that should be impossible; malformed input deserves an error return instead, and library code should never kill its caller's process over a bad argument value it was designed to reject.
Write guards against INT_MAX, INT_MIN and SIZE_MAX rather than literals such as 2147483647; the numbers printed in this lesson assume 32-bit int, 64-bit size_t and 4-byte int, and they change on other targets.
Common mistakes
Testing after the addition, as in int s = a + b; if (s < a) return -1;. The overflow has already happened, so the program has no defined behaviour, and the optimiser may delete the branch because it is entitled to assume a + b did not overflow.
Writing if (len - off > 0) on size_t values. When off exceeds len the subtraction wraps to a value near SIZE_MAX, the test passes, and that wrapped number goes on to memcpy as a byte count.
Putting real work inside assert, such as assert((p = malloc(n)) != NULL) or assert(fclose(f) == 0). A release build with -DNDEBUG never allocates or never closes, so debug and release stop behaving the same way.
Try it yourself
Change, predict, then run
Write bool safe_mul(int a, int b, int *out) that assumes a and b are non-negative, asserts that precondition, and returns false instead of multiplying when the product would pass INT_MAX, using a single division. Call it with (46340, 46340) and (46341, 46341) and confirm only the second is refused.
Open the C workspaceCheck your understanding
A parser holds size_t len (bytes actually available) and size_t off (an offset read from an untrusted file header), and is about to copy len - off bytes. Which guard actually prevents a huge copy size?
- if (len - off < 0) return -1;
- if (off > len) return -1;
- assert(off <= len);
- if ((int)(len - off) < 0) return -1;
Show answer
Only off > len compares values that already exist, so the guard cannot wrap, and once it passes len - off is the true difference (zero when off equals len). The first option is dead code because a size_t is never negative. The assert is the tempting one: it states exactly the right condition, but off comes from a file, so this is input validation rather than an invariant, and under -DNDEBUG the check vanishes and the wrapped length reaches the copy. The cast version relies on the wrap having already happened plus an implementation-defined conversion.