The most deniable memory-corruption bugs don't look like memory bugs at all. The
code allocates the size it computed and copies the length it was given — both
"correct." The error happened earlier, in arithmetic, where a value wrapped,
truncated, or changed sign. By the time it reaches malloc or memcpy, the bug is
already baked into a perfectly innocent-looking call.
- arch
- amd64-64-little
- bug class
- integer overflow -> undersized allocation / OOB
- NXon
three ways the math betrays you#
Multiplication overflow in a size. The classic. count * size wraps past
SIZE_MAX, so the allocation is tiny — then the loop writes count elements into
it:
Signedness / negative length. A signed length passes a <= check while
negative, then becomes enormous when used as an unsigned size:
Truncation. A 64-bit length stored into a 32- or 16-bit field loses its high bits. The bounds check runs on the wide value; the copy uses the narrow one (or vice-versa), and the two disagree.
why it lands as corruption#
There's no out-of-bounds expression anywhere — the OOB is emergent. The
allocation believes one size, the population believes another, and the gap between
them is your overflow. For the multiplication case, picking count so that
count * 16 mod 2^64 is small but count is large gives a heap overflow whose
length you control:
From there it's an ordinary heap overflow — corrupt the next chunk's size, or its
fd, and you're back in tcache poisoning or
unsorted-bin territory.
the allocation was 'right' the whole time
malloc got exactly the (wrapped) number it was handed; the bug was three lines
upstream, in a multiply:
segfault{0x10000001_times_16_is_not_what_you_think}
the defences#
- Checked arithmetic.
__builtin_mul_overflow(count, size, &n)and friends turn the wrap into a detectable error;calloc(count, size)does this check for you — prefer it overmalloc(count*size). size_teverywhere, and check after promotion. Mixingint/size_tinvites the signed-negative-becomes-huge bug; validate the value in the type it'll be used as.-fsanitize=integer,undefinedin testing catches wraps and bad conversions at runtime;-Wconversionflags the truncations at compile time.
The lesson: bounds checks protect the access, but the bug is often in the
arithmetic that feeds the access. Audit every a * b, every signed-to-unsigned
boundary, every assignment into a narrower type — those are where a memory-safety
bug hides inside code that never touches a pointer wrong.