PART I

Chapter 4: From Bug to Vulnerability

The previous chapters established a chain:

integer arithmetic
    ->
allocation size
    ->
pointer arithmetic
    ->
bounds
    ->
memory corruption

They also established a review method:

find the access
    ->
identify the controlling values
    ->
trace those values backward
    ->
reconstruct the intended invariant
    ->
find the first point where the invariant can fail

This chapter takes the next step.

A programming error is not automatically a security vulnerability. The important question is how the error interacts with attacker-controlled data, memory layout, object lifetime, compiler behavior, and the surrounding program.

The goal is therefore to move from:

"This arithmetic can overflow."

to:

"This input can cause this calculation to produce this incorrect object size, which causes this access to leave this object, producing this security consequence."

That is the difference between finding a bug and understanding a vulnerability.


1. Three different questions

When analyzing a suspected memory-safety bug, separate three questions.

Question 1: Is the code incorrect?

For example:

size = count * element_size;

may overflow.

That establishes a correctness problem.

Question 2: Can an attacker influence the bad state?

Suppose:

count

comes from a network packet.

Now the arithmetic problem is externally reachable.

Question 3: What can the attacker cause?

An overflow might produce:

crash

out-of-bounds read

out-of-bounds write

use-after-free

corrupted object state

information disclosure

control-flow corruption

The answers to these questions should not be conflated.

A static analyzer may correctly identify an integer overflow without knowing whether an attacker can reach it.

Conversely, a crash may occur in code where the underlying cause is a subtle arithmetic error several layers earlier.


2. The vulnerability chain

A useful abstraction is:

input
  |
  v
attacker-controlled value
  |
  v
conversion
  |
  v
arithmetic
  |
  v
incorrect size or offset
  |
  v
incorrect object or pointer
  |
  v
invalid access
  |
  v
security consequence

The interesting part is that each stage may transform the value.

For example:

packet length
    ->
uint32_t
    ->
size_t
    ->
count * element_size
    ->
allocation size
    ->
pointer offset
    ->
memcpy length

A reviewer who stops at the first arithmetic operation may miss the actual consequence.

A reviewer who starts only at `memcpy` may miss the root cause.

The vulnerability is the chain.


3. Integer overflow is not one thing

Several different numerical problems are commonly grouped together under the phrase "integer overflow."

They should be distinguished.

3.1 Unsigned wraparound

For an unsigned type, arithmetic is performed modulo one more than its maximum representable value.

Conceptually:

SIZE_MAX + 1

wraps to:

0

and:

0 - 1

wraps to:

SIZE_MAX

This behavior is defined by C.

Defined behavior does not mean safe behavior.

If an unsigned calculation determines an allocation size, defined wraparound can still create a security vulnerability.

3.2 Signed overflow

Signed integer overflow is a different matter.

For example:

int x = INT_MAX;
x++;

The language does not define this as ordinary modular arithmetic.

Signed overflow can therefore create undefined behavior.

This distinction matters because compiler optimizations can rely on the absence of undefined behavior.

A reviewer must not reason about signed overflow as though it were simply unsigned wraparound.

3.3 Truncation

Consider:

uint64_t requested;
uint32_t length;

length = requested;

The value may be perfectly representable in `uint64_t` but not in `uint32_t`.

The resulting value can be smaller than the original.

If `length` later controls an allocation or copy, the truncation can create an undersized object.

The chain becomes:

large value
    ->
narrowing conversion
    ->
smaller value
    ->
allocation
    ->
later use of original logical size
    ->
out-of-bounds access

No arithmetic overflow was necessary.

3.4 Signed/unsigned conversion

Consider:

int length = get_length();

if (length < 0)
    return ERROR;

memcpy(dst, src, length);

This is usually straightforward because the negative case is rejected before the conversion.

Compare:

int length = get_length();

if (length <= MAX_LENGTH)
    memcpy(dst, src, length);

Now negative values pass the check.

The conversion to `size_t` occurs at the call.

The security problem is therefore a type-domain mismatch:

validation domain:
    signed integers

consumption domain:
    unsigned size_t

A useful rule is:

Validate a value in the domain in which it will be consumed.

4. Why size_t does not make code safe

`size_t` is the natural C type for object sizes.

That makes it appropriate.

It does not make arithmetic involving sizes safe.

Consider:

size_t bytes = count * element_size;

The multiplication still has a maximum representable result.

The type communicates:

"This value represents a size."

It does not communicate:

"This calculation cannot overflow."

This distinction is fundamental.

A safe size calculation is therefore about the relationship between operands and the maximum representable size, not merely about choosing `size_t`.


5. The width of the integer type matters

Consider:

uint32_t count;
size_t size = count * element_size;

The result of the multiplication depends on the types involved.

A reviewer should not reason from the apparent destination type alone.

The relevant questions are:

What are the operand types?

What conversions occur before the operation?

What type is used for the operation?

What type receives the result?

Can the result be truncated afterward?

This becomes especially important when code is expected to compile on multiple architectures.


6. 32-bit and 64-bit differences

A bug may be reachable on one architecture and unreachable on another.

For example, suppose:

size_t

is 32 bits on one platform and 64 bits on another.

A multiplication that overflows on the 32-bit platform may be perfectly representable on the 64-bit platform.

That does not necessarily make the source code correct.

It means the vulnerability depends on the implementation environment.

Conversely, a truncation to a fixed-width 32-bit field can remain dangerous even on a 64-bit machine.

Portability is therefore part of vulnerability analysis.

Ask:

What are the widths of the relevant types?

What are the ranges of the input fields?

What assumptions does the code make about those widths?

7. Do not infer type behavior from variable names

Names such as:

length
size
count
offset
index

tell you what the programmer intended.

They do not tell you the actual type.

You may encounter:

int length;
unsigned length;
uint32_t length;
size_t length;
ptrdiff_t length;

These types have different ranges and conversion behavior.

Similarly:

count * sizeof(*items)

may be safer to reason about than:

count * ITEM_SIZE

because the former directly derives the size from the actual C object type.

The compiler knows the type.

The reviewer should use the same source of truth whenever possible.


8. The sizeof trap

Consider:

struct item {
    uint32_t type;
    uint64_t value;
};

A programmer might assume:

sizeof(struct item) == 12

because:

4 + 8 == 12

But C structures can contain padding.

The actual object representation may therefore be larger.

This matters for:

malloc(count * sizeof(struct item))

versus:

malloc(count * 12)

and for:

buffer + i * sizeof(struct item)

versus:

buffer + i * 12

Hard-coded structure sizes are especially dangerous when code is intended to be portable.

The safest approach is usually:

sizeof(struct item)

or:

sizeof(*items)

rather than duplicating the layout manually.


9. Structure padding creates a second class of bug

Padding is not merely an allocation-size issue.

Suppose code serializes a structure by doing:

memcpy(output, &object, sizeof(object));

This may copy padding bytes as well as the actual fields.

If those bytes contain uninitialized data, the operation can disclose information.

This is a different chain:

object layout
    ->
padding
    ->
copied representation
    ->
externally visible data
    ->
information disclosure

The general lesson is that memory-safety analysis should consider both:

how much memory is accessed

and:

what the accessed bytes actually represent.

10. Flexible array members

Consider:

struct packet {
    uint32_t length;
    unsigned char data[];
};

A common allocation pattern is:

packet = malloc(sizeof(*packet) + length);

This is exactly the arithmetic-to-allocation chain.

The intended invariant is:

sizeof(*packet) + length <= SIZE_MAX

But the object also has a logical boundary:

data has length bytes

A correct allocation therefore does not automatically imply that every operation on `data` is safe.

For example:

packet->data[offset]

requires:

offset < length

and:

memcpy(packet->data + offset, src, copy_length);

requires:

offset <= length

and:

copy_length <= length - offset

The structure gives the program a physical representation.

The code still has to preserve the logical bounds.


11. Flexible array members and allocation policy

Consider:

size_t total = sizeof(struct packet) + length;

struct packet *p = malloc(total);

This is often the right general shape.

But review:

length

as an external quantity.

Ask:

Can the addition overflow?

Is length itself trustworthy?

Is the resulting object expected to contain exactly length bytes?

Are later fields or metadata also included?

A more complicated object may look like:

total = sizeof(struct packet)
      + header_length
      + payload_length
      + trailer_length;

Now there are several additions.

Every one is part of the allocation proof.

The more independently supplied size fields exist, the more important it becomes to establish the total size incrementally.


12. calloc does not magically solve multiplication

Consider:

items = calloc(count, sizeof(*items));

This is preferable to:

malloc(count * sizeof(*items))

in some contexts because the allocator interface receives the count and element size separately.

However, the security property still matters.

The implementation needs to ensure that the requested total size is representable.

More importantly, callers must not assume that using `calloc` eliminates all size-calculation problems.

For example:

total = count * element_size;
items = calloc(1, total);

still performs the multiplication before calling `calloc`.

The safe reasoning remains:

Is the requested total representable?

The interface does not remove the underlying mathematical constraint.


13. reallocarray and what it changes

Some environments provide an interface commonly known as:

reallocarray

Its purpose is to express:

resize to count * element_size

while allowing the allocation routine to perform the multiplication with overflow checking.

Conceptually:

reallocarray(ptr, count, element_size)

represents:

realloc(ptr, count * element_size)

with the multiplication treated as part of the allocation operation.

This can eliminate one common class of mistakes:

size = count * element_size;
ptr = realloc(old, size);

But it does not solve every problem.

The caller still needs to reason about:

count

element_size

subsequent indexing

logical object bounds

integer conversions before the call

other arithmetic used to calculate offsets

The useful lesson is:

Move arithmetic into a primitive that checks it when possible,
but do not confuse allocation-size safety with complete memory safety.

14. Pointer arithmetic has its own rules

The chain does not end when the allocation succeeds.

Consider:

p = buffer + offset;

The pointer operation itself has requirements.

In normal C pointer arithmetic, the pointer must be associated with an appropriate array object, and the resulting pointer must stay within that object or one-past-the-end where permitted.

The one-past-the-end pointer is useful:

begin
    |
    v
[ valid elements ]
    |
    v
   end

where:

end = begin + count

The pointer `end` can be used as a boundary.

It must not be dereferenced.

This distinction is important:

valid pointer position

is not always the same as:

valid dereferenceable object

15. One-past-the-end is a boundary, not an element

Suppose:

int values[10];

Then:

values + 10

is a useful one-past-the-end pointer.

But:

*(values + 10)

is not a valid access.

This is why range-based code often looks like:

int *p = values;
int *end = values + 10;

while (p != end) {
    process(*p);
    ++p;
}

The range is:

[values, values + 10)

The right endpoint is excluded.

This half-open representation is extremely useful when reasoning about bounds.


16. Why half-open ranges help

Represent a buffer as:

[begin, end)

Then:

size = end - begin

and a pointer `p` is inside the range when:

begin <= p
&&
p < end

For a copy of `length` bytes beginning at `p`, the conceptual requirement is:

p + length <= end

The same reasoning applies to indexes:

[0, count)

means:

0 <= index < count

This notation removes many off-by-one ambiguities.

It also aligns naturally with C loops:

for (i = 0; i < count; ++i)

17. Pointer subtraction has preconditions too

Consider:

size_t remaining = end - current;

This is only meaningful when `end` and `current` are pointers into the same array object, with the appropriate ordering.

It is not a general-purpose operation for subtracting arbitrary addresses.

This matters when reviewing parser code that uses pointers as cursors.

A common pattern is:

current = buffer;
end = buffer + size;

while (current < end) {
    ...
}

This can be a good design because the bounds are explicit.

But the proof still depends on:

end

being a valid boundary derived from the same object.


18. Pointer provenance and modern C reasoning

Pointer arithmetic is not simply integer arithmetic disguised as syntax.

A pointer carries a relationship to an object.

For example:

char *p = buffer + offset;

is meaningful because `buffer` points into an object and `offset` is interpreted relative to that object.

Converting pointers to integers and manipulating those integers can therefore introduce reasoning problems that do not exist with ordinary array indexing.

Code such as:

uintptr_t x = (uintptr_t)p;
x += offset;
p = (void *)x;

may be used in systems programming, but it requires much more careful reasoning than:

p = p + offset;

The broad rule is:

Keep pointer calculations in pointer form when possible.

This preserves the connection between the pointer and the object it is intended to reference.


19. Subobjects matter

Suppose:

struct object {
    char header[16];
    char data[32];
};

The complete structure is 48 bytes in this simplified example.

A pointer into `header` does not automatically acquire permission to treat the entire structure as one undifferentiated byte region for every operation.

The language's object and subobject model matters.

Security review therefore needs to distinguish:

allocation boundary

from:

object boundary

and:

object boundary

from:

subobject boundary

This becomes particularly important when a buffer contains several logical fields.


20. Intra-object overflows

Not every memory corruption crosses an allocation boundary.

Consider:

struct state {
    char name[16];
    void (*callback)(void);
};

A write beyond `name` may remain inside the same allocated `struct state`.

An address sanitizer may not necessarily describe this simply as a heap-buffer-overflow because the write may still be inside the overall allocation.

Yet the logical object has been corrupted.

This is an important distinction:

physical allocation bounds

are not sufficient to define:

semantic object bounds

Security review must consider both.


21. Custom allocators

Large C systems frequently implement allocation abstractions.

Examples include:

arena_alloc()
pool_alloc()
slab_alloc()
vector_reserve()
buffer_grow()
object_new()

These abstractions can hide the arithmetic.

For example:

vector_reserve(v, count);

may internally perform:

count * sizeof(*v->data)

A reviewer who searches only for `malloc` may miss it.

When encountering a custom allocator, establish its contract.

Ask:

What unit does the size represent?

Does it check multiplication overflow?

What happens on failure?

Does it align the result?

Can the allocation move?

Are existing pointers invalidated?

Does it reserve physical capacity or logical size?

The abstraction should be treated as part of the memory model.


22. Capacity versus length

Consider:

struct buffer {
    unsigned char *data;
    size_t length;
    size_t capacity;
};

The intended invariant is usually:

length <= capacity

and:

data points to at least capacity bytes

An operation such as:

b->data[b->length++] = c;

depends on:

b->length < b->capacity

before the write.

If a resize operation incorrectly updates:

capacity

without actually allocating enough memory, the logical and physical models diverge.

The code may then appear internally consistent while accessing memory incorrectly.

This is one of the most important patterns in real-world C:

metadata says one thing

while:

memory actually contains another.

23. The dangerous metadata problem

Memory corruption frequently involves corrupting metadata that later controls memory access.

For example:

object->length

might determine:

memcpy(..., object->length);

If another vulnerability lets an attacker modify `object->length`, the resulting memory operation may become dangerous.

The chain is now recursive:

memory corruption
    ->
corrupted metadata
    ->
incorrect size
    ->
new memory corruption

This is why the security consequences of one memory bug can be much broader than its immediate write.


24. Allocator metadata is another layer

Memory allocators maintain their own internal state.

The exact representation varies by implementation.

An out-of-bounds write may therefore corrupt data belonging to:

another application object

or:

allocator bookkeeping

The latter can produce consequences such as:

allocator crashes

inconsistent heap state

later invalid allocations

later invalid frees

The details are highly implementation-dependent.

The important point for vulnerability analysis is:

The bytes immediately adjacent to the corrupted object
determine what the corruption can influence.

Do not assume that an overflow is useful simply because it is a write.

Determine what is actually reachable.


25. Exploitability is contextual

Suppose a program contains:

memcpy(buffer, input, length);

and `length` can exceed `buffer`'s capacity.

That establishes a memory-safety vulnerability.

But the severity depends on questions such as:

Can an attacker reach the function?

Can the attacker control length?

Can the attacker control the bytes being written?

How large can the overwrite be?

What object follows buffer?

Can the overwrite reach security-sensitive state?

Does the process have useful privileges?

Are there memory-safety mitigations?

The vulnerability should therefore be described precisely before discussing exploitability.


26. Controlled length versus controlled contents

An out-of-bounds write is especially interesting when an attacker controls both:

how much is written

and:

what is written

Consider:

memcpy(dst, attacker_data, attacker_length);

The attacker potentially controls two dimensions:

range

and:

contents

Compare this with:

memset(dst, 0, attacker_length);

Here the length may still be attacker-controlled, but the bytes are fixed.

Both can be vulnerabilities.

Their exploitability may differ significantly.

This distinction is useful when assessing impact.


27. Out-of-bounds reads are different

Consider:

memcpy(output, buffer + offset, length);

If the source range exceeds the buffer, the result is an out-of-bounds read.

The consequence may be:

crash

or:

information disclosure

The latter can be particularly important if the bytes beyond the intended object contain:

pointers

cryptographic material

credentials

object metadata

addresses

other sensitive state

An out-of-bounds read can also become an information primitive that enables exploitation of a separate vulnerability.

Thus:

read

does not mean:

harmless.

28. Information disclosure can defeat mitigations

Modern systems often use randomized addresses and other defenses.

An information disclosure can reveal values that reduce the effectiveness of those defenses.

For example, if an out-of-bounds read exposes a pointer into a randomized memory region, an attacker may gain information about the process's memory layout.

This illustrates why vulnerabilities should not always be evaluated independently.

A seemingly limited read can increase the exploitability of another memory-corruption bug.

The vulnerability chain can therefore look like:

arithmetic error
    ->
out-of-bounds read
    ->
address disclosure
    ->
weakened memory-layout protection
    ->
easier exploitation of another bug

29. Compiler optimization matters

C's abstract machine rules matter to security analysis.

Consider signed arithmetic that can overflow.

A programmer might reason:

if (x + y < x)
    overflow();

But if the addition is performed using a signed type and can overflow, the language does not guarantee ordinary wrapping behavior.

Compilers may make transformations based on the assumption that undefined behavior does not occur.

This means that source-level reasoning based on "what the CPU probably does" can be wrong.

The correct question is:

What behavior does the language and implementation permit?

Then consider:

What assumptions does the compiler make?

This is especially important when auditing checks intended to detect arithmetic overflow.


30. Do not rely on hardware wraparound for signed integers

A common low-level programming habit is to reason from two's-complement machine behavior:

INT_MAX + 1

"will wrap."

That may describe what a particular processor does at the machine level.

It is not sufficient as a C-language argument.

Security-sensitive arithmetic should be written so that the operation is valid under the language rules rather than relying on accidental machine behavior.

This is particularly important for code compiled with aggressive optimization.


31. Integer promotions can change the calculation

Consider:

uint16_t a;
uint16_t b;

size_t size = a * b;

The multiplication does not necessarily occur as a 16-bit multiplication.

C's integer promotions apply.

The actual arithmetic type depends on the types and implementation.

This can make expressions difficult to reason about by inspection.

The lesson is not to memorize every promotion rule while reviewing every line.

Instead:

When an arithmetic expression is security-critical,
determine the actual types involved.

This is especially important when fixed-width integer types are mixed with:

int

unsigned int

size_t

ptrdiff_t

long

unsigned long

32. Shifts are arithmetic too

Reviewers often search for:

+

-

*

but overlook:

<<

A size calculation such as:

size = count << shift;

is still an arithmetic operation.

It can overflow the destination type or produce an unintended result.

Similarly:

index = value << alignment;

may control a pointer offset.

The chain remains:

external value
    ->
shift
    ->
offset
    ->
pointer
    ->
access

The general rule is:

Any arithmetic operation that influences memory
belongs in the bounds proof.

33. Division is not automatically safe

Consider:

element_size = total_size / count;

If:

count == 0

the operation is invalid.

More subtly, division may truncate.

For example:

bytes / sizeof(*item)

produces an element count that may be smaller than expected.

This can affect:

loop bounds

allocation counts

parser logic

The numerical operation itself may be valid while the semantic interpretation is wrong.

Again:

mathematically valid

does not necessarily mean:

memory-safe.

34. Rounding and alignment

Systems code often rounds sizes upward.

For example:

aligned = (size + alignment - 1) & ~(alignment - 1);

This is a common pattern.

But the addition:

size + alignment - 1

can overflow.

If the result controls an allocation, the chain becomes:

requested size
    ->
rounding arithmetic
    ->
wrapped size
    ->
undersized allocation
    ->
later overflow

A safer design must ensure the rounding calculation itself cannot overflow.

The important lesson is that "helper arithmetic" is still part of the security boundary.


35. Multiplication hidden behind alignment

Consider:

blocks = (size + block_size - 1) / block_size;

total = blocks * block_size;

There are now:

addition

subtraction

division

multiplication

The final `total` may not equal the original `size` if arithmetic wraps or if assumptions about the rounding calculation are incorrect.

Whenever a size is rounded, padded, aligned, or expanded, treat the entire transformation as one size-calculation chain.


36. Parsing is where the chain becomes dangerous

Network and file parsers frequently contain all the ingredients:

attacker-controlled lengths
attacker-controlled counts
nested structures
variable-sized records
arithmetic
allocations
pointer arithmetic
copies

A typical parser may do:

count = read_u32(input);

item_size = read_u32(input);

total = count * item_size;

buffer = malloc(total);

for (i = 0; i < count; ++i)
    parse_item(buffer + i * item_size);

This is almost a textbook example of the complete chain.

The key review technique is to identify every field that contributes to:

size

offset

count

pointer

and trace it from the wire format to the final memory access.


37. Validate the outer boundary before the inner boundary

Suppose a packet contains:

outer_length
    |
    +-- header
    |
    +-- inner_length
    |
    +-- payload

A robust parser should first establish:

outer region fits in input

Then interpret:

inner_length

within that region.

The conceptual invariant becomes:

inner_length <= outer_remaining

rather than:

inner_length <= input_size

The distinction matters because an inner field should normally be constrained by its containing object, not by the entire input buffer.

This is the parser equivalent of respecting subobject bounds.


38. Nested objects create nested invariants

For a hierarchy:

packet
  |
  +-- message
        |
        +-- records
              |
              +-- payload

the bounds should be hierarchical.

For example:

message_end <= packet_end

and:

records_end <= message_end

and:

payload_end <= records_end

A parser that repeatedly validates everything against the top-level input size can accidentally permit inner objects to escape their logical containers.

The physical buffer may still be large enough.

The logical object is nevertheless violated.


39. The first broken invariant may be a trust decision

Not every vulnerability begins with arithmetic.

Consider:

length = header->length;

if (length <= MAX_LENGTH)
    process_packet(data, length);

If the code assumes that `header` itself has already been validated but that assumption is false, the first broken invariant may be:

header is a valid object

rather than:

length is a valid size

This matters because security analysis should not force every bug into the arithmetic category.

The arithmetic-to-memory chain is a particularly useful pattern, but it sits inside a larger model:

input interpretation
    ->
object validity
    ->
arithmetic validity
    ->
memory validity

40. Error handling is part of the chain

Suppose:

size = calculate_size(...);

buffer = malloc(size);

if (buffer == NULL)
    return ERROR;

That handles allocation failure.

But what if:

calculate_size()

returns an error encoded as:

0

or:

SIZE_MAX

or:

(size_t)-1

The caller must understand the contract.

A failed size calculation that is mistaken for a legitimate size can produce another vulnerability.

For example:

size = calculate_size(...);

buffer = malloc(size);

If the function uses `SIZE_MAX` to indicate failure, the caller may accidentally request an enormous allocation.

Security properties depend on error handling just as much as ordinary control flow.


41. Sentinel values are dangerous as sizes

Code sometimes uses:

-1

to indicate failure.

This is particularly dangerous when the result is later converted to `size_t`.

For example:

int get_length(...);

int length = get_length(...);

buffer = malloc((size_t)length);

If:

get_length()

returns:

-1

the conversion produces a very large unsigned value.

The right design is often to make failure explicit rather than encode it in a value that will later cross an integer domain boundary.

For example:

int get_length(..., size_t *result);

or another API that clearly separates:

success/failure

from:

size.

42. API design can prevent entire classes of bugs

Good interfaces make invalid states harder to represent.

Compare:

void copy(void *dst, void *src, size_t length);

with an interface that receives an object carrying its capacity:

void copy_into(struct buffer *dst,
               const void *src,
               size_t length);

The second interface can potentially centralize:

capacity checking

pointer validation

length handling

This does not make it automatically safe.

But it makes the invariant explicit and reduces the number of callers that need to reproduce it.

A recurring security engineering principle is:

Put the invariant as close as possible to the data
it protects.

43. The danger of parallel metadata

Consider:

unsigned char *buffer;
size_t allocated;
size_t used;

These values are related.

The intended invariants might be:

used <= allocated

and:

buffer points to at least allocated bytes.

If a function modifies:

allocated

without resizing the buffer, the model becomes inconsistent.

Likewise, if it resizes the buffer without updating:

allocated

the metadata becomes stale.

This is a common source of vulnerabilities in hand-written dynamic containers.

A reviewer should search for every operation that changes either:

pointer

or:

size metadata

and verify that the relationship remains true.


44. Use-after-free can enter the chain through resizing

Consider:

p = malloc(size);

q = p;

free(p);

use(q);

The obvious issue is lifetime.

But resizing introduces a similar problem:

q = p + offset;

p = realloc(p, new_size);

use(q);

Now the old derived pointer may be invalid even though the allocation itself still exists in some form.

This suggests a broader chain:

object lifetime
    ->
pointer validity
    ->
bounds
    ->
access

The arithmetic-to-memory chain and lifetime-to-memory chain often meet in real code.


45. A bug can change category as it propagates

Consider:

count * element_size

overflows.

At first, the bug is:

integer arithmetic error.

The result is used by:

malloc()

Now it becomes:

allocation-size error.

Later:

items[i]

uses the original `count`.

Now it becomes:

out-of-bounds access.

If the write changes another object's state, it becomes:

memory corruption.

If that state controls a later function call, the security consequence may become:

control-flow corruption.

The same underlying mistake therefore changes character as it propagates.

When documenting a vulnerability, identify both:

root cause

and:

final security effect.

46. Root cause versus trigger

Suppose:

count * sizeof(*items)

overflows only for a very large count.

The trigger may be:

a malicious packet containing a large count.

The root cause is:

unchecked multiplication.

The vulnerable operation is:

allocation based on the wrapped result.

The consequence may be:

out-of-bounds write.

These are different pieces of the analysis.

A good vulnerability report distinguishes them.


47. Root cause versus crash site

Suppose a sanitizer reports:

heap-buffer-overflow

at:

process_item()

The root cause may be:

attacker-controlled count
    ->
multiplication overflow
    ->
undersized allocation

The crash site is simply where the incorrect state finally became observable.

A useful debugging question is:

What value was wrong immediately before the crash?

Then:

Where did that value come from?

Then repeat.

This is backward slicing through the program.


48. Use sanitizers as confirmation, not as proof

AddressSanitizer and related tools are excellent for finding many memory errors.

But a clean run does not prove that the program is safe.

A vulnerability may require:

a particular input

a particular architecture

a particular allocation layout

a particular execution path

a particular optimization configuration

Dynamic testing observes one execution.

The invariant-based analysis reasons about the possible executions.

The two approaches complement each other.


49. Static analysis and symbolic reasoning

Static analyzers are particularly useful for finding paths where:

x + y

x * y

x - y

may violate constraints.

But the most useful question after a warning is:

What does this value control?

For example:

possible integer overflow

is more significant if the result feeds:

malloc

than if it feeds:

logging.

Similarly:

possible out-of-bounds access

should be traced backward to determine whether the bound can be attacker-controlled.

The arithmetic-to-memory chain provides a natural prioritization mechanism.


50. A practical severity heuristic

When several findings are available, consider this rough ordering.

Higher concern:

attacker-controlled arithmetic
    ->
allocation size
    ->
attacker-controlled write

Also high concern:

attacker-controlled arithmetic
    ->
pointer offset
    ->
out-of-bounds write

Potentially high concern:

attacker-controlled length
    ->
out-of-bounds read
    ->
disclosure of sensitive state

Lower concern in some contexts:

arithmetic error
    ->
only an allocation failure
    ->
clean process termination

This is not a formal severity system.

It is a way to prioritize investigation.

The surrounding program determines the actual impact.


51. A complete vulnerability reconstruction

Consider:

struct entry {
    uint32_t length;
    unsigned char data[];
};

int append(struct entry **e,
           const unsigned char *src,
           size_t extra)
{
    size_t new_length;
    size_t total;
    struct entry *tmp;

    new_length = (*e)->length + extra;
    total = sizeof(**e) + new_length;

    tmp = realloc(*e, total);
    if (tmp == NULL)
        return -1;

    *e = tmp;

    memcpy((*e)->data + (*e)->length,
           src,
           extra);

    (*e)->length = new_length;

    return 0;
}

At first glance this looks plausible.

Now reconstruct it.

Step 1: External influence

Suppose:

extra

is derived from an attacker-controlled message.

Step 2: First arithmetic

old_length + extra

can overflow.

The intended invariant is:

extra <= SIZE_MAX - old_length

Step 3: Second arithmetic

sizeof(*e) + new_length

can also overflow.

The intended invariant is:

new_length <= SIZE_MAX - sizeof(**e)

Step 4: Allocation

`realloc` receives `total`.

If either calculation wrapped, the allocation may be smaller than intended.

Step 5: Pointer arithmetic

The copy uses:

(*e)->data + (*e)->length

The pointer is based on the old logical length.

Step 6: Copy

The access requires:

extra <= allocated_capacity - old_length

But the code has not explicitly established that relationship.

Step 7: Metadata update

Only after the copy does the code update:

length = new_length

If the copy overflows, the object can be corrupted before the metadata reflects the new state.

The complete chain is:

attacker-controlled extra
    ->
length addition
    ->
total addition
    ->
allocation
    ->
data pointer
    ->
copy offset
    ->
copy length
    ->
memory corruption

This is exactly the kind of bug that looks reasonable when each statement is inspected independently.

The vulnerability becomes obvious when the statements are treated as one state transition.


52. Improving the design

The safest solution is not always to add more checks to the existing code.

Sometimes the data model should change.

Instead of:

length
capacity
pointer

being manipulated independently, centralize resizing.

For example, conceptually:

int buffer_reserve(struct buffer *b, size_t additional);

Then make that function responsible for:

addition overflow

allocation-size overflow

reallocation

capacity update

pointer update

The caller can then operate under a simpler invariant:

capacity - length >= required_space

This reduces the number of places where the arithmetic-to-memory chain has to be reconstructed.


53. Reserve before writing

A useful dynamic-buffer pattern is:

if (additional > capacity - length)
    grow_buffer(...);

Then, after successful growth:

memcpy(data + length, src, additional);

The conceptual proof is:

additional <= capacity - length

which implies:

length + additional <= capacity

without requiring the unchecked addition to establish the condition.

This pattern is broadly useful for:

parsers

serializers

network buffers

string builders

decompression buffers

dynamic arrays

54. Growth calculations have their own hazards

A common dynamic-array strategy is:

new_capacity = old_capacity * 2;

This can overflow.

So can:

new_capacity = old_capacity + old_capacity / 2;

and:

new_capacity = old_capacity + increment;

A robust growth function needs to establish that the new capacity is representable before performing the arithmetic.

Then it needs to ensure that:

new_capacity * element_size

is also representable.

Capacity growth is therefore another instance of the same chain.


55. A two-stage capacity invariant

For a container of elements, distinguish:

element count

from:

byte capacity.

Suppose:

count = number of elements
capacity = number of allocated elements
S = sizeof(element)

Then:

count <= capacity

and:

capacity * S <= SIZE_MAX

must both hold.

If a function grows the container by:

additional

elements, the new count must satisfy:

additional <= capacity - count

if no reallocation is needed.

If reallocation is needed, the new capacity must satisfy both:

new_capacity >= count + additional

and:

new_capacity * S <= SIZE_MAX

This is a useful example of how a single logical operation can require several linked invariants.


56. Decompression and expansion bugs

The arithmetic-to-memory chain is particularly important when small inputs can produce large outputs.

For example:

compressed_length
    ->
decompressed_length
    ->
allocation
    ->
output pointer
    ->
decompressor write

The decompressed size may be:

count * element_size

or:

rows * columns * channels

or:

blocks * block_size

If the calculated size is wrong, the decompressor may write according to its logical model into an allocation based on a different physical size.

This is a classic separation between:

logical output size

and:

allocated output size.

The same chain applies even when the input itself is small.


57. Serialization and deserialization

Serialization code often performs:

field_count * field_size

or:

header + payload + trailer

Deserialization reverses the process.

The danger is that serialization may assume trusted in-memory values while deserialization receives attacker-controlled representations.

Therefore:

serialization invariant

and:

deserialization invariant

are not symmetric from a security perspective.

A serialized field should be treated as untrusted until its:

representation

range

relationship to the containing object

have been validated.


58. Length fields are claims, not facts

A useful parser mindset is:

A length field is a claim made by the input.

For example:

payload_length = read_u32(...);

does not mean:

the payload has payload_length bytes.

It means:

the input claims that the payload has payload_length bytes.

The parser must establish:

payload_length <= remaining_input

before treating the claim as a boundary.

Similarly, a count field claims:

there are count objects.

The parser must determine whether:

count

is compatible with:

available input

and:

available output capacity.

This mental model prevents a great many parser bugs.


59. Security invariants should survive transformations

Suppose a parser establishes:

length <= remaining

Then it converts:

length

from:

uint32_t

to:

size_t

The property should remain true after conversion.

If it is narrowed instead:

size_t
    ->
uint16_t

the original proof may no longer apply.

Likewise, if the code calculates:

length * element_size

the original bound:

length <= remaining

does not automatically imply:

length * element_size <= remaining

The security property must be re-established after each transformation.

This is one of the most important lessons in data-flow analysis.


60. The invariant ledger

For difficult functions, it can be useful to maintain an explicit ledger.

For example:

count:
    source = packet
    type = uint32_t
    invariant = count <= MAX_COUNT

item_size:
    source = packet
    type = uint32_t
    invariant = item_size <= MAX_ITEM_SIZE

total:
    derived = count * item_size
    invariant = total <= SIZE_MAX

allocation:
    size = total

offset:
    derived = index * item_size
    invariant = offset <= total

copy:
    length = item_size
    invariant = item_size <= total - offset

This makes it much easier to identify where a proof disappears.

It also works well when reviewing code with many helper functions.


61. Finding the first broken invariant

Suppose a function contains:

if (count <= MAX_COUNT)
    ...

Then later:

count *= element_size;

The earlier invariant:

count <= MAX_COUNT

is no longer sufficient to reason about the new value.

The first broken proof may therefore occur not when the multiplication actually overflows, but when the code begins treating the old invariant as though it still described the transformed value.

This distinction is subtle but valuable.

A program can lose a safety proof before it actually performs an invalid operation.

The reviewer should identify the point where the proof ceases to apply.


62. Vulnerability reports should explain the chain

A weak report says:

"Integer overflow in allocation size."

A stronger report says:

"An attacker-controlled element count is multiplied by the
element size without an overflow check. When the product wraps,
the allocation is smaller than the number of elements later
processed. The loop continues to use the original count, causing
writes beyond the allocation."

The second description explains:

source

arithmetic

allocation

pointer/indexing

consequence

That makes the vulnerability understandable and actionable.


63. A useful report structure

For this vulnerability class, a concise technical report can contain:

Summary

Affected code path

Attacker-controlled input

First broken invariant

Arithmetic operation

Resulting incorrect size or offset

Invalid memory operation

Security impact

Preconditions

Suggested remediation

The most important part is the chain.

Avoid reporting only the final sanitizer message.


64. Remediation should repair the invariant

Suppose the problem is:

count * element_size

overflowing.

A poor remediation might simply reduce:

MAX_COUNT

until the current platform happens not to overflow.

That may hide the bug without establishing a general invariant.

A stronger remediation establishes:

count <= SIZE_MAX / element_size

before multiplication.

If the logical count also has a policy limit, retain that separately.

The remediation should therefore encode the property that must remain true.


65. Prefer structural fixes over scattered checks

If ten callers independently perform:

count * sizeof(*items)

ten callers may eventually implement ten slightly different checks.

A central helper can enforce the invariant once.

For example, conceptually:

checked_mul_size(count, sizeof(*items), &bytes)

Then all callers receive either:

success with a representable size

or:

failure.

The exact API is a design decision.

The principle is:

Centralize repeated security invariants when practical.

This reduces duplicated arithmetic reasoning.


66. What good defensive code looks like

A good memory-size calculation tends to have these properties:

The units are obvious.

The types are appropriate.

The arithmetic is checked before it occurs.

The allocation uses the checked result.

The logical count is not confused with byte capacity.

Pointer arithmetic is derived from the same size model.

The complete access range is validated.

Failure paths are explicit.

Related metadata remains synchronized.

Helper functions preserve their contracts.

This is more valuable than memorizing a list of "dangerous functions."


67. A complete audit sequence

For an unfamiliar C component, use the following sequence.

Phase 1: Find externally influenced quantities

Look for:

lengths
counts
offsets
dimensions
indexes
allocation sizes

Phase 2: Find their transformations

Track:

conversions
arithmetic
rounding
alignment
multiplication
addition
subtraction
shifts

Phase 3: Find memory consumers

Track where the values reach:

malloc
calloc
realloc
reallocarray
memcpy
memmove
memset
array indexes
pointer arithmetic
loops

Phase 4: Establish invariants

Write down:

allocation size
logical object size
capacity
offset
access length

Phase 5: Check the proof

Ask whether each invariant remains true after every transformation.

Phase 6: Determine impact

Ask:

read or write?

controllable contents?

controllable length?

which object?

how far can the access extend?

what state can be corrupted?

Phase 7: Confirm dynamically

Use:

sanitizers

fuzzing

targeted tests

debugger inspection

to validate the suspected path.

This is the transition from static code reading to vulnerability analysis.


68. The final mental model

At the beginning of this series, the chain looked like:

integer arithmetic
    ->
allocation size
    ->
pointer arithmetic
    ->
bounds
    ->
memory corruption

After working through the advanced cases, it is useful to expand it:

external representation
    ->
interpretation
    ->
integer type
    ->
conversion
    ->
arithmetic
    ->
size or offset
    ->
allocation or existing object
    ->
pointer arithmetic
    ->
logical object boundary
    ->
physical memory boundary
    ->
access
    ->
corrupted or disclosed state
    ->
security consequence

At every arrow there is an invariant.

At every transformation there is an opportunity for the invariant to be lost.

The central skill is therefore not memorizing which C constructs are dangerous.

It is learning to ask:

What does this value mean?

What type represents that meaning?

What transformations does it undergo?

What object does it eventually control?

What proves that the resulting access is valid?

What happens if that proof is false?

Once you can answer those questions, many apparently unrelated C vulnerabilities become variations of the same underlying problem.

The arithmetic may be different.

The allocator may be different.

The parser may be different.

The memory operation may be different.

But the reasoning remains:

value
    ->
transformation
    ->
invariant
    ->
object
    ->
access
    ->
consequence

That is the point at which a collection of C "traps and pitfalls" becomes a coherent security model.

The objective of auditing is not merely to find code that looks dangerous.

It is to reconstruct the program's assumptions, identify the first assumption that can be violated, and follow that violation all the way to its security consequence.

That is how a bug becomes a vulnerability.


← Previous 6 of 13 Next →