PART I
A large class of C vulnerabilities follows a surprisingly predictable chain:
integer arithmetic
->
allocation size
->
pointer arithmetic
->
bounds
->
memory corruption
The individual operations are usually ordinary. There is nothing inherently suspicious about multiplying a count by a structure size, allocating the resulting number of bytes, advancing a pointer through the allocation, and writing objects into it.
The danger appears when an assumption made at one stage is carried into the next stage without being checked.
A value that is numerically wrong can therefore become a size that is too small, which becomes a pointer into the wrong location, which becomes an invalid bounds assumption, which finally becomes a memory corruption primitive.
The important skill is not memorizing five classes of bugs. It is learning to follow the value through the entire chain.
Consider a function that receives a number of records:
void process(size_t count)
{
struct record *p;
p = malloc(count * sizeof(struct record));
...
}
At first glance this is straightforward:
number of records (count) * size of one record (sizeof(struct record)) = number of bytes
But the multiplication is an arithmetic operation performed in a finite integer type.
In C, size_t is an unsigned integer type. If count is very large (e.g., on a 32-bit system where size_t maxes out at 4,294,967,295), multiplying it by sizeof(struct record) can exceed the maximum possible value of size_t.
In the case of unsigned arithmetic, malloc receives the wrapped-around small value (e.g., 32 bytes) and successfully allocates a small chunk of memory. Writing 4,294,967,328 bytes worth of data into a 32-byte buffer causes a severe heap buffer overflow, leading to a very common and dangerous vulnerability known as an integer overflow leading to a heap buffer overflow.
This type of memory corruption can cause the program to crash, or perhaps even result in a remote code execution vulnerability.
The important point is that the allocation routine does not know what the programmer intended.
That gives us the first transition:
arithmetic error
->
incorrect allocation size
This is why arithmetic involving memory sizes deserves more scrutiny than ordinary arithmetic.
The most important expressions to recognize are things like:
count * sizeof(T)
count * element_size
header_size + payload_size
offset + length
width * height * bytes_per_pixel
entries * sizeof(pointer)
base + index * stride
These are not merely calculations. They determine the amount of memory that subsequent code believes exists.
Suppose the intended allocation is:
size = count * sizeof(struct record);
but `size` has wrapped to a much smaller value.
The allocation can succeed perfectly.
That is an important property of this bug class.
There does not have to be an allocation failure.
In fact, an allocation failure would often make the problem easier to detect.
Instead, the program may receive a valid pointer to a valid allocation:
p = malloc(size);
The pointer is legitimate.
The allocation is legitimate.
The size is legitimate according to the allocator.
Only the programmer's assumption about the size is wrong.
That distinction is crucial when investigating memory corruption. A corrupted pointer is not required. A valid pointer to an undersized allocation is sufficient.
Consider:
size_t bytes;
if (count > SOME_LIMIT)
return ERROR;
bytes = count * sizeof(struct record);
p = malloc(bytes);
The upper-bound check may look reassuring, but it only helps if the limit was chosen with the multiplication in mind.
For example, checking:
count <= 1000000
does not prove that:
count * sizeof(struct record)
fits in the type used for `bytes`.
The useful question is not:
"Did we validate count?"
It is:
"Did we prove that every arithmetic operation used to derive the allocation size is safe?"
That is a much stronger condition.
When calculating:
count * element_size
a common pattern is to check the multiplication before performing it.
Conceptually:
if (count > SIZE_MAX / element_size)
return ERROR;
bytes = count * element_size;
The division is safe because it is performed before the multiplication that might overflow.
Likewise, for:
header + payload
the corresponding reasoning is:
if (payload > SIZE_MAX - header)
return ERROR;
total = header + payload;
The general pattern is:
Before addition:
prove that the result fits.
Before multiplication:
prove that the result fits.
This is more reliable than calculating first and trying to determine afterward whether the result was reasonable.
Now suppose the allocation succeeds:
p = malloc(bytes);
The program may then treat the allocation as an array:
for (i = 0; i < count; ++i)
p[i] = records[i];
This is where the arithmetic mistake becomes a pointer-arithmetic problem.
The expression:
p[i]
is conceptually based on:
address of p + i * sizeof(*p)
The programmer is therefore making a second assumption:
the allocation contains at least count objects of type *p
But that assumption came from the earlier multiplication.
If the multiplication used to calculate `bytes` was wrong, the pointer arithmetic can now walk beyond the allocation.
The chain is becoming:
bad arithmetic
->
undersized allocation
->
pointer arithmetic assumes a larger allocation
->
pointer leaves the allocated object
This is the point at which the vulnerability becomes much more concrete.
A common mental mistake is to think of pointer arithmetic as simply producing addresses.
In C, pointer arithmetic has a relationship to an actual object or array.
If:
struct record *p;
points to the first element of an array of `n` records, then expressions such as:
p + i
are meaningful within the appropriate range associated with that array.
But if only two records were allocated, this does not become safe merely because `p + 100` can be represented as an address.
The machine may be capable of calculating that address.
That does not mean the program owns the object located there.
This distinction is fundamental:
address calculation != ownership
and:
address exists != object exists
Security vulnerabilities frequently arise when code silently changes from reasoning about allocated objects to reasoning about addresses.
Suppose we have:
p = malloc(count * sizeof(*p));
and later:
for (i = 0; i < count; ++i)
p[i] = value;
The loop appears correctly bounded.
The condition:
i < count
may be completely correct.
Yet the program can still write out of bounds.
Why?
Because the loop bound and the allocation bound were derived through different assumptions.
The real safety condition is:
i < number_of_objects_actually_allocated
not merely:
i < number_of_objects_intended
If the allocation size was calculated incorrectly, `count` may no longer describe the memory that exists.
This is one of the most useful ways to think about C memory safety:
A bounds check is only as good as the quantity being used as the bound.
A variable named `count`, `length`, `size`, `capacity`, or `width` carries no authority merely because of its name.
You need to know where its value came from and whether it still describes the object being accessed.
Consider a packet containing a number of entries:
// Define a structure to hold an entry containing a type and a value
struct entry
{
uint32_t type;
uint32_t value;
};
// Allocate and read a specified number of entries, vulnerable to integer overflow
struct entry *read_entries(size_t count)
{
struct entry *entries;
size_t bytes;
// Calculate total bytes needed; vulnerable to integer overflow if count is too large
bytes = count * sizeof(*entries);
// Allocate memory for the entries
entries = malloc(bytes);
// Check if the allocation failed and return NULL if so
if (entries == NULL)
return NULL;
// Read data into each entry in the allocated array
for (size_t i = 0; i < count; ++i)
read_entry(&entries[i]);
// Return the populated array of entries
return entries;
}
The code has a clear data flow:
count
|
v
count * sizeof(*entries)
|
v
malloc(bytes)
|
v
entries
|
v
entries[i]
|
v
memory write
That diagram is more useful for security analysis than looking at each statement independently.
The vulnerability question is:
Can an attacker influence count?
If yes, next ask:
Can count * sizeof(*entries) overflow?
If yes, next ask:
Can malloc return a smaller allocation than the loop assumes?
If yes, next ask:
Can entries[i] reach outside that allocation?
If yes, next ask:
What does read_entry write?
If it writes through the supplied pointer, the final consequence can be an out-of-bounds write.
The vulnerability is not simply "integer overflow."
It is:
attacker-controlled count
->
integer overflow
->
undersized allocation
->
apparently valid pointer
->
incorrect pointer range
->
out-of-bounds write
->
memory corruption
That is the chain you should learn to recognize.
The initial arithmetic error does not have to be a multiplication.
Consider:
// Calculate buffer size by adding header and payload sizes (vulnerable to integer overflow)
total = header_size + payload_size;
// Allocate memory for the combined header and payload buffer
buffer = malloc(total);
// Copy the input payload into the buffer immediately following the header area
memcpy(buffer + header_size, input, payload_size);
There are now several arithmetic relationships:
total = header_size + payload_size
and:
destination = buffer + header_size
and:
destination range = [header_size, header_size + payload_size)
If `total` wraps because the addition overflowed, the allocation may be smaller than the region described by the later `memcpy`.
The code might therefore have:
correct-looking allocation
->
correct-looking pointer arithmetic
->
correct-looking copy length
while the combination is invalid.
This is why security analysis should follow related values rather than isolated statements.
The arithmetic does not necessarily overflow in the original type.
Conversions can introduce the problem.
For example:
// Store the user-supplied length in a size_t variable
size_t len = user_supplied_length;
// Cast/convert the size_t length into a signed integer
// (vulnerable to integer truncation/sign extension bugs)
int n = len;
// Check if the signed integer is less than the maximum allowed size
if (n < MAX_SIZE)
...
// Allocate memory using the signed integer n
// (if a large user-supplied value wrapped around to negative,
// malloc receives a massive size_t value due to implicit conversion)
buffer = malloc(n);
If `len` is larger than the range of `int`, the conversion can change its value.
Now the validation is operating on `n`, not on the original quantity.
A similar problem can occur when signed and unsigned values interact:
// Declare a signed integer to hold a length (vulnerable to negative values)
int length;
// Declare an unsigned size_t variable to hold the final allocation size
size_t allocation;
// Check if length is less than MAX (if length is negative, this check is bypassed)
if (length < MAX)
// Assign the signed length to the unsigned size_t variable
// (if length is negative, implicit conversion turns it into a massive positive value)
allocation = length;
The comparison and conversion rules can produce results that differ from what a programmer expects, particularly when negative values are possible.
The broader lesson is:
Track the value and its type through every conversion.
Do not stop the analysis at the first apparently reasonable bounds check.
Even if the allocation size was calculated correctly, an offset can overflow or otherwise become invalid.
Consider:
// Allocate a buffer of the specified size in bytes
char *p = malloc(size);
// Calculate a new pointer by adding an offset
// (vulnerable if offset is out of bounds or mishandled)
char *q = p + offset;
// Copy input data into the destination buffer starting at q
// (vulnerable to a buffer overflow if offset + length exceeds the allocated size)
memcpy(q, input, length);
The relevant condition is not simply:
offset < size
It is the combined relationship:
offset + length <= size
Otherwise the starting pointer can be inside the allocation while the copied region extends beyond it.
This produces another common chain:
offset
+
length
->
required range
->
bounds check
->
memory access
The arithmetic and the bounds check are inseparable.
A check of only the starting position is not sufficient.
Many real vulnerabilities reduce to one quantity that is trusted too far.
For example:
count
length
size
offset
capacity
stride
width
height
index
The quantity may be introduced at an input boundary:
network packet
file
image
archive
IPC message
configuration
command line
database
It then passes through several transformations:
external value
->
parsed integer
->
converted integer
->
arithmetic
->
allocation size
->
pointer offset
->
loop bound
->
memory access
At each stage, the program may assume that the value still means what it originally meant.
That assumption is often where the vulnerability lives.
A particularly subtle version occurs when the program maintains both a physical capacity and a logical length:
capacity = 1024;
length = 900;
The allocation represents:
capacity
while the data currently in use represents:
length
Both are legitimate numbers, but they answer different questions.
A common mistake is to use one where the other is required.
For example:
// Check if the index is within the valid bounds of the buffer
// (vulnerable to sign extension/comparison issues if types differ)
if (index < length)
// Assign the value to the buffer at the specified index
buffer[index] = value;
might be correct for reading existing elements but wrong if the operation is intended to append up to `capacity`.
Conversely:
// Check if the index is within the valid capacity
// of the buffer to prevent out-of-bounds access
if (index < capacity)
// Process the element located at the validated index in the buffer
process(buffer[index]);
may read uninitialized or logically invalid data.
The security lesson is broader than "check bounds":
Know what each bound means.
A bound should have a precise relationship to the object being accessed.
In a memory-safe language, an integer overflow might result in an exception or another controlled failure.
In C, arithmetic frequently sits directly on the boundary between data and memory.
A calculated integer can become:
an allocation size
an array index
a pointer offset
a copy length
a loop bound
a structure count
a buffer capacity
The integer therefore controls the shape of the program's memory access.
That makes arithmetic errors disproportionately important.
The security significance is not:
"integer overflow is bad"
but:
"integer overflow can change the program's model of how much memory exists."
Once that model is wrong, subsequent code may remain internally consistent while operating outside the actual allocation.
It is useful to distinguish the first mistake from the eventual security consequence.
For example:
count * sizeof(*p)
might overflow.
That is the arithmetic defect.
The allocation might then be too small.
That is the allocation defect.
The loop might then calculate:
p + i
outside the allocation.
That is the pointer/bounds defect.
The program might then execute:
p[i] = value;
That is the invalid memory access.
The resulting overwrite might corrupt:
another heap object
allocator metadata
a function pointer
an object containing security state
a return address
a vtable
application data
The CVE may ultimately be described as:
heap buffer overflow
out-of-bounds write
memory corruption
rather than "integer arithmetic error."
This is why looking only for the final CWE category can miss the beginning of the chain.
When reviewing C code for this class of vulnerability, pick a quantity that controls memory and follow it forward.
For example:
user_length
Ask:
Where did it come from?
Then:
What type is it?
Then:
Has it been converted?
Then:
Is it added to anything?
Then:
Is it multiplied?
Then:
Is the result used as an allocation size?
Then:
What object does the resulting pointer refer to?
Then:
How are offsets calculated?
Then:
What establishes the access bounds?
Then:
Does the final operation stay within those bounds?
This is often more effective than searching for suspicious functions such as `malloc` or `memcpy` in isolation.
One recurring misconception is:
"malloc succeeded, therefore the pointer is safe."
No.
`malloc` answers a narrow question:
Can the allocator provide this many bytes?
It does not answer:
Is this the number of bytes the program actually needs?
Nor:
Will later pointer arithmetic remain within those bytes?
Nor:
Is the caller using the allocation consistently?
For example:
// Assign an attacker-controlled value directly to the size variable
size = attacker_controlled_value;
// Allocate memory using the untrusted size
// (vulnerable to integer overflows, out-of-memory denial of service,
// or zero-size allocations)
p = malloc(size);
may be perfectly valid code.
The security problem can occur later:
// Loop through each element up to the expected count
// (vulnerable to a buffer overflow if expected_count
// exceeds the actual allocated capacity of pointer p)
for (i = 0; i < expected_count; ++i)
// Assign a value to the buffer element at index i
p[i] = ...;
The allocator has done exactly what it was asked to do.
The caller asked for the wrong amount.
The best defense is not one magic check. It is preventing an invalid assumption from propagating.
At the arithmetic stage:
prove that calculations cannot overflow.
At the allocation stage:
ensure the allocated size represents the actual required object.
At the pointer stage:
keep offsets associated with the object they are allowed to address.
At the bounds stage:
validate the complete accessed range, not merely its starting point.
At the memory-access stage:
ensure the operation's size agrees with the validated range.
In other words:
arithmetic
->
size
->
object
->
range
->
access
Each arrow is a place where an invariant should be preserved.
A useful invariant for an allocated array is:
allocated_bytes >= element_count * sizeof(element)
and, for an access:
offset + access_size <= allocated_bytes
These equations capture much of the problem.
The first says:
the allocation is large enough for the logical object.
The second says:
the particular operation stays within that object.
Notice that both equations contain arithmetic.
That is why bounds checking cannot be separated from integer-safety checking.
A bounds check built on an overflowed quantity may simply prove the wrong thing.
For an array allocation, prefer a checked relationship between the count and element size:
// Check if multiplying count by the size of the element
// would overflow SIZE_MAX to prevent integer overflow
if (count > SIZE_MAX / sizeof(*p))
// Return an error if an overflow would occur
return ERROR;
// Safely allocate memory for the array since the
// multiplication is now guaranteed not to overflow
p = malloc(count * sizeof(*p));
For a buffer consisting of a header and payload:
// Check if adding payload_size and header_size would
// exceed SIZE_MAX to prevent integer overflow
if (payload_size > SIZE_MAX - header_size)
// Return an error if the addition would overflow
return ERROR;
// Safely calculate the total size since the addition
// is now guaranteed not to overflow
total_size = header_size + payload_size;
// Allocate memory for the buffer using the validated total size
buffer = malloc(total_size);
The exact error handling will depend on the program, but the important property is that the potentially overflowing operation occurs only after its safety has been established.
For more complicated calculations, it can be useful to make the calculation itself a separate operation with an explicit success/failure result. This makes it harder for an unchecked intermediate value to escape into an allocator or pointer calculation.
Consider:
// Parse the input size into an unsigned size_t variable
size_t input_size = parse_size(input);
// Convert or truncate the size_t value into an unsigned int
// (vulnerable to truncation on 64-bit systems where size_t
// is larger than unsigned int)
unsigned int n = input_size;
// Check if the unsigned int is less than or equal to the
// maximum allowed entries
if (n <= MAX_ENTRIES)
// Process the validated count (vulnerable if truncation
// bypassed bounds or caused a mismatch)
process(n);
The validation is now about `n`, while the original input was represented by `input_size`.
A more robust approach is to establish the range in the type that will carry the value through the relevant computation.
Likewise, if a length is eventually used as a `size_t`, make sure the conversion to `size_t` has the intended meaning before relying on it for a memory operation.
The general rule is:
Validate the value you are actually going to use.
The same model is useful when starting from a crash.
Suppose a crash occurs at:
memcpy(dst, src, len);
Do not stop at:
"memcpy crashed."
Work backward:
Why is dst invalid?
Then:
How was dst calculated?
Then:
Where did its offset come from?
Then:
How was the allocation size calculated?
Then:
Which values determined that size?
Then:
Where did those values originate?
You may eventually arrive at:
attacker-controlled packet field
->
integer conversion
->
overflowing multiplication
->
undersized allocation
->
incorrect offset
->
out-of-bounds memcpy
The crash site is the end of the chain, not necessarily the beginning of the bug.
The most valuable security bugs are often not obvious calls to dangerous functions.
A program may contain:
// Read an unsigned 32-bit integer from the network packet
// to determine the item count
n = read_u32(packet);
// Calculate the total allocation size by multiplying
// count by the item struct size
// (vulnerable to an integer overflow if n is large,
// leading to a wrapped, overly small byte count)
bytes = n * sizeof(struct item);
// Allocate memory for the items using the potentially
// wrapped 'bytes' value
items = malloc(bytes);
// Loop n times to parse each item
// (vulnerable to a severe heap buffer overflow because the loop
// runs for the full 'n' iterations while malloc only allocated
// a fraction of the required memory due to the integer overflow)
for (i = 0; i < n; ++i)
parse_item(&items[i], packet);
all of which look individually reasonable (not withstanding the comments).
The vulnerability emerges from their relationship.
For example:
n = read_u32(packet);
bytes = n * sizeof(struct item);
items = malloc(bytes);
for (i = 0; i < n; ++i)
parse_item(&items[i], packet);
There is no obviously malicious operation.
Yet the entire security question can be summarized as:
Can n cause bytes to be smaller than the number of items
subsequently addressed by the loop?
If the answer is yes, the program has crossed the boundary from an arithmetic bug into a memory-safety vulnerability.
When you see C code manipulating memory, mentally draw this chain:
VALUE
|
v
ARITHMETIC
|
v
SIZE
|
v
ALLOCATION
|
v
POINTER
|
v
OFFSET
|
v
BOUND
|
v
MEMORY ACCESS
Then ask one question at every transition:
"What proves that this value is still valid for the next operation?"
For example:
count
|
| What proves the multiplication cannot overflow?
v
allocation_size
|
| What proves this represents the required object?
v
allocation
|
| What proves this offset belongs to that object?
v
pointer
|
| What proves the entire access fits?
v
memory access
This is the core lesson.
A memory corruption vulnerability often does not begin with a bad pointer.
It begins much earlier, with a number.
That number is used to calculate a size.
The size determines what memory exists.
The program then performs pointer arithmetic based on that assumption.
A bounds check may validate the wrong quantity.
And only at the final memory access does the original mistake become visible as memory corruption.
So the security-relevant chain is:
integer arithmetic
->
allocation size
->
pointer arithmetic
->
bounds
->
memory corruption
Once you learn to trace that chain forward and backward, a large class of apparently unrelated C vulnerabilities becomes much easier to recognize.