PART I

Chapter 3 - Pointer Arithmetic and Bounds

Worked Exercises: Finding the First Broken Invariant

The previous chapters established a model:

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

This chapter puts that model into practice.

Each exercise presents a small piece of C code. The examples are intentionally realistic rather than artificially minimal. In several cases, the final memory error is not caused directly by the first mistake.

For each example, try to answer four questions before reading the solution:

1. What value controls the memory access?
2. Where did that value come from?
3. What invariant is supposed to hold?
4. Where is that invariant first broken?

The fourth question is the most important.

The first broken invariant is often several operations before the eventual invalid memory access.


Exercise 1: The obvious multiplication

Consider:

struct item {
    uint32_t type;
    uint32_t value;
};

struct item *make_items(size_t count)
{
    struct item *items;

    items = malloc(count * sizeof(*items));
    if (items == NULL)
        return NULL;

    for (size_t i = 0; i < count; ++i)
        initialize_item(&items[i]);

    return items;
}

### Questions

1. What arithmetic operation deserves attention?
2. What invariant must hold before `malloc`?
3. Can the loop itself be correctly written while the function is still unsafe?
4. Where should the defensive check occur?

### Solution

The multiplication is:

count * sizeof(*items)

The required invariant is:

count * sizeof(*items) <= SIZE_MAX

If this multiplication overflows, `malloc` receives an allocation smaller than the number of objects subsequently addressed by the loop.

The loop can therefore be perfectly correct:

i < count

while the allocation is incorrect.

A suitable check is:

if (count > SIZE_MAX / sizeof(*items))
    return NULL;

items = malloc(count * sizeof(*items));

The important lesson is that the loop bound does not prove the allocation is large enough.

The chain is:

count
    ->
overflowing multiplication
    ->
undersized allocation
    ->
valid-looking items pointer
    ->
correct-looking loop
    ->
out-of-bounds access

The first broken invariant is the allocation-size calculation.


Exercise 2: A check that proves the wrong thing

Consider:

#define MAX_ITEMS 1000000

struct item *make_items(size_t count)
{
    struct item *items;

    if (count > MAX_ITEMS)
        return NULL;

    items = malloc(count * sizeof(*items));
    if (items == NULL)
        return NULL;

    return items;
}

### Questions

1. What does the check actually prove?
2. Does it prove that the multiplication is safe?
3. How would you review this code without knowing the size of `struct item`?
4. What is the stronger form of the check?

### Solution

The check proves only:

count <= MAX_ITEMS

It does not directly prove:

count * sizeof(*items) <= SIZE_MAX

Whether the multiplication is safe depends on the relationship between `MAX_ITEMS` and `sizeof(*items)`.

A fixed item-count limit can be perfectly reasonable as a policy constraint, but it should not be confused with an arithmetic overflow check.

The stronger check is:

if (count > SIZE_MAX / sizeof(*items))
    return NULL;

If both a policy limit and an arithmetic limit are required, both can be checked:

if (count > MAX_ITEMS)
    return NULL;

if (count > SIZE_MAX / sizeof(*items))
    return NULL;

The first broken assumption in the original code is that limiting the number of elements necessarily limits the resulting byte count sufficiently.


Exercise 3: The endpoint check

Consider:

int append_data(unsigned char *buffer,
                size_t capacity,
                size_t offset,
                const unsigned char *data,
                size_t length)
{
    if (offset > capacity)
        return -1;

    memcpy(buffer + offset, data, length);
    return 0;
}

### Questions

1. What does the check prove?
2. What property does `memcpy` actually require?
3. Can `buffer + offset` be valid while the copy is invalid?
4. Rewrite the validation.

### Solution

The check establishes:

offset <= capacity

It does not establish:

offset + length <= capacity

The starting address may be inside the buffer while the copied range extends beyond it.

A safer formulation is:

if (offset > capacity)
    return -1;

if (length > capacity - offset)
    return -1;

memcpy(buffer + offset, data, length);

The subtraction is performed only after proving that `offset <= capacity`.

The intended invariant is:

offset + length <= capacity

but the code should avoid evaluating the addition when it could overflow.

The first broken invariant is not the pointer calculation. It is the missing relationship between the starting offset and the length of the operation.


Exercise 4: Two arithmetic operations

Consider:

void *allocate_buffer(size_t count,
                      size_t element_size,
                      size_t header_size)
{
    size_t total;

    total = header_size + count * element_size;

    return malloc(total);
}

### Questions

1. How many independent arithmetic operations can overflow?
2. Is checking only the multiplication sufficient?
3. Is checking only the addition sufficient?
4. What order should the checks take?

### Solution

There are two operations:

count * element_size

and:

header_size + result

Both must be safe.

First establish:

count <= SIZE_MAX / element_size

assuming `element_size` is nonzero.

Then calculate the product.

Next establish:

product <= SIZE_MAX - header_size

Only then calculate:

total = header_size + product;

Conceptually:

if (element_size != 0 &&
    count > SIZE_MAX / element_size)
    return NULL;

product = count * element_size;

if (product > SIZE_MAX - header_size)
    return NULL;

total = header_size + product;

The important lesson is that a size expression must be analyzed operation by operation.

This:

header + count * element_size

is not one arithmetic operation. It contains a multiplication followed by an addition.


Exercise 5: The negative length

Consider:

int process(char *buffer, int length)
{
    if (length > 1024)
        return -1;

    memcpy(buffer, input, length);
    return 0;
}

### Questions

1. What values pass the check?
2. What happens to a negative `length` when passed to `memcpy`?
3. What is wrong with treating `length <= 1024` as sufficient validation?
4. What should the function establish?

### Solution

The check rejects values greater than 1024.

It does not reject negative values.

The effective condition is therefore:

length <= 1024

rather than:

0 <= length <= 1024

`memcpy` takes a `size_t` length. A negative `int` is converted to `size_t`.

Thus a value such as:

-1

can become a very large unsigned value.

The function should establish that the length is both nonnegative and within the destination's capacity.

For example:

if (length < 0 || length > 1024)
    return -1;

memcpy(buffer, input, (size_t)length);

The deeper lesson is to consider the value at the point where it is consumed, not merely at the point where it was received.


Exercise 6: The parser with a trusted-looking count

Consider:

struct file_header {
    uint32_t count;
};

int load(const unsigned char *data, size_t data_size)
{
    const struct file_header *header;
    struct item *items;
    size_t count;

    if (data_size < sizeof(*header))
        return -1;

    header = (const struct file_header *)data;
    count = header->count;

    items = malloc(count * sizeof(*items));
    if (items == NULL)
        return -1;

    for (size_t i = 0; i < count; ++i)
        parse_item(data, data_size, &items[i]);

    free(items);
    return 0;
}

### Questions

There are at least two independent bounds problems here.

Identify them.

### Solution

The first is the allocation calculation:

count * sizeof(*items)

The value came from external data and may overflow `size_t`.

The second is the input-side bound.

Even if the allocation is safe, nothing shown establishes that the input contains enough data for:

count

items.

The program has two separate objects whose bounds must be established:

output allocation
    ->
enough space for count items

and:

input buffer
    ->
enough encoded data for count items

A common mistake is to notice the output allocation and forget that parsing also consumes input.

The chain therefore splits:

external count
    |
    +--> allocation size --> output bounds
    |
    +--> required input --> input bounds

Both branches must be safe.


Exercise 7: The nested size calculation

Consider:

int read_image(const unsigned char *data,
               size_t data_size,
               uint32_t width,
               uint32_t height,
               uint32_t channels)
{
    size_t pixels;
    size_t bytes;
    unsigned char *image;

    pixels = width * height;
    bytes = pixels * channels;

    image = malloc(bytes);
    if (image == NULL)
        return -1;

    memcpy(image, data, bytes);

    free(image);
    return 0;
}

### Questions

1. How many arithmetic stages are present?
2. Which checks are required?
3. Does the fact that `width`, `height`, and `channels` are 32-bit make the calculation safe?
4. What additional input-side condition is missing?

### Solution

There are two multiplications:

width * height

and:

pixels * channels

Each must be checked.

The fact that the inputs are 32-bit does not make the result fit in `size_t` on every possible implementation, and even on implementations where it does, the intermediate and final values still need to be considered explicitly.

The intended allocation invariant is:

width * height * channels <= SIZE_MAX

but the calculation should be performed in checked stages.

There is also an input-side requirement:

data_size >= bytes

The allocation being large enough says nothing about whether `data` contains `bytes` readable bytes.

The complete reasoning is:

dimensions
    ->
pixel count
    ->
byte count
    ->
allocation
    ->
destination bounds

and independently:

dimensions
    ->
byte count
    ->
source bounds

Exercise 8: The apparently safe `realloc`

Consider:

int grow(struct item **items,
         size_t count,
         size_t additional)
{
    count += additional;

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

    if (*items == NULL)
        return -1;

    return 0;
}

### Questions

Identify every distinct issue you would investigate.

### Solution

There are at least three.

First:

count += additional;

can overflow.

The required condition is:

count <= SIZE_MAX - additional

before performing the addition.

Second:

count * sizeof(**items)

can overflow.

That multiplication needs its own check.

Third, the original pointer is overwritten before checking whether `realloc` succeeded.

If `realloc` fails, the original allocation remains allocated, but:

*items

has been assigned `NULL`.

The caller has lost the pointer to the original allocation.

A safer structure is conceptually:

if (additional > SIZE_MAX - count)
    return -1;

new_count = count + additional;

if (new_count > SIZE_MAX / sizeof(**items))
    return -1;

tmp = realloc(*items, new_count * sizeof(**items));

if (tmp == NULL)
    return -1;

*items = tmp;

The important lesson is that a single line can contain multiple independent safety properties.

The review should not stop after finding one.


Exercise 9: Derived pointers and `realloc`

Consider:

struct buffer {
    unsigned char *data;
    size_t size;
};

int extend(struct buffer *b, size_t extra)
{
    unsigned char *cursor;
    unsigned char *tmp;

    cursor = b->data + b->size;

    tmp = realloc(b->data, b->size + extra);
    if (tmp == NULL)
        return -1;

    b->data = tmp;
    b->size += extra;

    *cursor = 0;
    return 0;
}

### Questions

1. Is `cursor` necessarily valid after `realloc`?
2. What arithmetic must be checked?
3. What additional issue exists even if `realloc` happens to return the same address?
4. What invariant is the code trying to maintain?

### Solution

`cursor` is derived from the old allocation.

A successful `realloc` may move the allocation.

If it moves, `cursor` becomes invalid.

Therefore:

cursor = b->data + b->size;

must not be used after a successful `realloc`.

There is also an arithmetic issue:

b->size + extra

must not overflow.

Even if `realloc` returns the same address, the code still needs to ensure that writing:

*cursor = 0;

is within the newly allocated object.

The intended model appears to be:

b->data
    |
    +---- b->size bytes ----+
                             |
                           cursor

The program wants `cursor` to point at the first byte after the existing data, presumably to write a terminator.

A safer implementation would calculate the offset before reallocation, perform the checked resize, update the base pointer, and then reconstruct the derived pointer if needed.

The important lesson is:

A pointer derived from an allocation is part of the allocation's state.

Changing the allocation can invalidate the derived pointer even when the pointer variable itself has not changed.


Exercise 10: The parser with subtraction

Consider:

int parse_payload(const unsigned char *packet,
                  size_t packet_size,
                  size_t header_size)
{
    size_t payload_size;
    unsigned char *payload;

    payload_size = packet_size - header_size;

    payload = malloc(payload_size);
    if (payload == NULL)
        return -1;

    memcpy(payload,
           packet + header_size,
           payload_size);

    free(payload);
    return 0;
}

### Questions

1. What must be established before `packet_size - header_size`?
2. What happens if `header_size > packet_size`?
3. Does the later `memcpy` validation need anything else?
4. Identify the complete chain.

### Solution

Before subtracting, establish:

header_size <= packet_size

Otherwise the unsigned subtraction can wrap to a very large value.

The resulting chain could be:

invalid header size
    ->
unsigned subtraction
    ->
huge payload_size
    ->
huge allocation request
    ->
invalid input range

There is also an important conceptual issue.

The program appears to assume:

packet[header_size ... packet_size)

is the payload.

That range exists only if:

header_size <= packet_size

The subtraction is therefore not merely an arithmetic detail. It expresses a range relationship.

This is a useful general rule:

Whenever a size is calculated by subtraction,
ask what ordering relationship makes that subtraction valid.

Exercise 11: The stride calculation

Consider:

void process(unsigned char *buffer,
             size_t count,
             size_t stride)
{
    for (size_t i = 0; i < count; ++i) {
        unsigned char *p = buffer + i * stride;
        process_record(p);
    }
}

Assume the caller has allocated:

count * stride

bytes.

### Questions

1. Is the caller's allocation necessarily safe?
2. Is the pointer calculation necessarily safe?
3. What property must hold for every iteration?
4. What should a reviewer ask about `process_record`?

### Solution

The caller must first establish:

count * stride <= SIZE_MAX

Then, for each iteration, the pointer calculation:

i * stride

must represent a valid offset within the allocation.

The intended range is approximately:

0 <= i < count

and each record should occupy:

stride

bytes.

The important invariant is:

i * stride + record_access_size <= count * stride

for every iteration.

This immediately raises another question:

Does process_record access at most stride bytes?

The caller may have correctly calculated the record boundaries while the callee independently violates them.

This illustrates the importance of compositional bounds:

outer object bound
    ->
subobject bound
    ->
callee access bound

Each layer must preserve the invariant.


Exercise 12: The duplicated size calculation

Consider:

struct item {
    uint32_t type;
    uint64_t value;
};

size_t allocate_size(size_t count)
{
    return count * sizeof(struct item);
}

void initialize(void *buffer, size_t count)
{
    for (size_t i = 0; i < count; ++i) {
        unsigned char *p =
            (unsigned char *)buffer + i * 12;

        initialize_item(p);
    }
}

### Questions

1. What is wrong even if the multiplication in `allocate_size` is safe?
2. What happens if the structure contains padding?
3. What invariant has been duplicated incorrectly?
4. What should replace the literal `12`?

### Solution

The allocation uses:

sizeof(struct item)

while the access uses:

12

Those values are not necessarily equal.

On many systems, the structure will contain padding because of alignment requirements.

Even if they happen to be equal on the current platform, the code has created two independent representations of the object's size.

The allocation assumes:

item_size = sizeof(struct item)

while the initialization code assumes:

item_size = 12

Those assumptions can diverge.

The access should derive the stride from the actual type:

i * sizeof(struct item)

or, preferably, use properly typed pointer arithmetic:

struct item *items = buffer;

initialize_item(&items[i]);

The broader lesson is:

A memory layout should have one authoritative representation.

Hard-coded sizes are particularly dangerous when they represent C object layouts.


Exercise 13: The validation happens too early

Consider:

int process(unsigned char *buffer,
            size_t capacity,
            size_t length)
{
    if (length > capacity)
        return -1;

    length += 16;

    memset(buffer, 0, length);

    return 0;
}

### Questions

1. Is the original validation sufficient?
2. What invariant is broken?
3. What should be checked after modifying `length`?
4. Can the addition itself overflow?

### Solution

The original check establishes:

length <= capacity

But the actual memory operation uses:

length + 16

The relevant invariant is therefore:

length + 16 <= capacity

The code changed the value after validating it.

A correct approach is to validate the final quantity:

if (length > capacity)
    return -1;

if (16 > capacity - length)
    return -1;

length += 16;

memset(buffer, 0, length);

The addition itself can also overflow if performed without checking, although the capacity relationship may provide enough information to prevent that in this particular case.

The general lesson is:

A bounds proof belongs as close as practical to
the operation it justifies.

If the quantity changes, the proof may no longer apply.


Exercise 14: The loop has a hidden unit conversion

Consider:

struct record {
    uint64_t timestamp;
    uint32_t value;
};

void process_records(unsigned char *buffer,
                     size_t bytes,
                     size_t count)
{
    for (size_t i = 0; i < count; ++i) {
        struct record *r =
            (struct record *)(buffer + i);

        process_record(r);
    }
}

### Questions

1. What unit is `i` expressed in?
2. What unit does the pointer addition use?
3. What was probably intended?
4. Why can this bug be especially subtle?

### Solution

`buffer` is an `unsigned char *`.

Therefore:

buffer + i

advances by:

i bytes

not:

i records

The intended stride is presumably:

sizeof(struct record)

The code should instead use something equivalent to:

struct record *records = (struct record *)buffer;

for (size_t i = 0; i < count; ++i)
    process_record(&records[i]);

or explicitly:

buffer + i * sizeof(struct record)

The subtlety is that the code is syntactically correct.

The pointer arithmetic is valid as byte-pointer arithmetic.

The problem is semantic: the unit of the offset does not match the unit of the logical object.

This is another way the chain can break:

logical count
    ->
wrong unit conversion
    ->
wrong pointer offset
    ->
wrong object
    ->
memory error

Exercise 15: The complete chain

Consider the following simplified parser:

struct header {
    uint32_t count;
    uint32_t item_size;
};

int load(const unsigned char *data,
         size_t data_size)
{
    const struct header *h;
    unsigned char *items;
    size_t count;
    size_t item_size;
    size_t total;

    if (data_size < sizeof(*h))
        return -1;

    h = (const struct header *)data;

    count = h->count;
    item_size = h->item_size;

    total = count * item_size;

    items = malloc(total);
    if (items == NULL)
        return -1;

    for (size_t i = 0; i < count; ++i) {
        unsigned char *item =
            items + i * item_size;

        parse_item(data + sizeof(*h),
                   data_size - sizeof(*h),
                   item,
                   item_size);
    }

    free(items);
    return 0;
}

### Questions

Find as many independent problems as you can.

Do not stop at the first one.

### Solution

This example contains several layers.

### Problem 1: Allocation multiplication

The calculation:

count * item_size

can overflow.

The required property is:

count * item_size <= SIZE_MAX

before the multiplication.

### Problem 2: Pointer multiplication

The calculation:

i * item_size

can overflow independently.

It must be valid for every iteration.

### Problem 3: Output range

The item pointer:

items + i * item_size

must identify a region inside the allocation.

The intended invariant is:

i * item_size + item_size <= total

for every valid `i`.

### Problem 4: Input subtraction

The expression:

data_size - sizeof(*h)

requires:

data_size >= sizeof(*h)

That condition has actually been established by the earlier check, so this particular subtraction is safe.

This is an important example of a safe arithmetic operation.

Not every arithmetic expression needs to be treated as a vulnerability.

The question is whether the necessary invariant has been established.

### Problem 5: Input availability

Even though:

data_size - sizeof(*h)

is valid, that does not prove there is enough input data for all `count` items.

`parse_item` receives:

data + sizeof(*h)

and:

data_size - sizeof(*h)

but the code has not shown that the aggregate encoded representation fits inside that remaining input.

### Problem 6: Per-item input size

If every item consumes:

item_size

bytes, the input must contain at least:

count * item_size

bytes.

That is another size calculation which needs to be checked.

### Problem 7: Callee contract

Even if the caller proves that the input contains `count * item_size` bytes and the output contains the same number of bytes, `parse_item` must itself respect:

item_size

for each item.

The outer function cannot assume the callee is bounded merely because the pointer is valid.

### The complete chain

The intended relationship is:

count
    +
item_size
    |
    v
count * item_size
    |
    +----------------+
    |                |
    v                v
output size      input size
    |                |
    v                v
allocation       input bounds
    |
    v
i * item_size
    |
    v
item pointer
    |
    v
parse_item
    |
    v
memory access

This is exactly the kind of code where examining only the final `parse_item` call is insufficient.

The vulnerability may originate in the first multiplication.


A Worked Review Method

The exercises illustrate a repeatable process.

Suppose you encounter:

p = malloc(size);

...

q = p + offset;

...

memcpy(q, src, length);

Do not immediately inspect `memcpy`.

Instead construct the chain:

size
  |
  v
allocation
  |
  v
p
  |
  +---- offset ----> q
  |
  +---- size
           |
           v
      available range

Then establish the required invariant:

offset + length <= size

Now work backward.

Where did `offset` come from?

Where did `length` come from?

Where did `size` come from?

Suppose you discover:

size = count * element_size;

Now the invariant becomes:

offset + length <= count * element_size

Then ask whether the right-hand side was calculated safely.

The analysis has moved from a memory operation to arithmetic.

That is exactly what we want.


Exercise Ranking: Which Bugs Should You Investigate First?

Not every suspicious expression deserves equal attention.

When reviewing a large codebase, prioritize chains that have these properties:

1. The controlling value is externally influenced.
2. The value is used in arithmetic.
3. The arithmetic determines an allocation or access size.
4. The resulting pointer is used for writing.
5. The access occurs in a parser, decoder, or other untrusted-data path.
6. The code performs multiple conversions or arithmetic operations.
7. The same quantity controls both allocation and indexing.
8. There are separate input and output buffers.
9. A size is represented by multiple variables or units.
10. A custom allocator hides the actual size calculation.

The most interesting pattern is often:

external value
    ->
arithmetic
    ->
allocation
    ->
loop
    ->
pointer arithmetic
    ->
write

This combines attacker control, arithmetic, allocation, and memory corruption in one path.


Distinguishing Real Problems from False Positives

A good reviewer should also know when not to report a problem.

Consider:

if (count > SIZE_MAX / sizeof(*items))
    return ERROR;

items = malloc(count * sizeof(*items));

The multiplication is present, but the relevant invariant has already been established.

That is not an integer-overflow vulnerability.

Likewise:

if (offset > capacity)
    return ERROR;

if (length > capacity - offset)
    return ERROR;

memcpy(buffer + offset, data, length);

The subtraction is not itself a problem because:

offset <= capacity

has already been established.

The important skill is therefore not:

"Find arithmetic."

It is:

"Determine whether the arithmetic is justified by an invariant."

This distinction greatly improves both manual review and static-analysis triage.


A More Formal View

For an allocated byte array, let:

B = allocation size
O = offset
L = access length

The fundamental safety property is:

O + L <= B

But because `O + L` may overflow, the operational proof should be expressed as:

O <= B

and:

L <= B - O

The second condition is meaningful only after the first has been established.

For an array of `N` elements of size `S`, the allocation requires:

N * S <= SIZE_MAX

and an access to element `I` requires:

I < N

If an access spans `K` elements starting at `I`, the required relationship is:

I <= N

and:

K <= N - I

Again, the subtraction form avoids requiring an unchecked addition.

These patterns generalize.

When possible, express bounds in terms of:

remaining capacity

rather than:

endpoint calculated by addition

This often makes overflow reasoning substantially easier.


A Useful Three-Layer Model

For complicated code, it can help to divide the analysis into three layers.

Layer 1: Numerical validity

Ask:

Are the numbers themselves valid?

This includes:

overflow
underflow
truncation
signedness
conversion
units

Layer 2: Object validity

Ask:

Does the number correctly describe the object?

This includes:

allocation size
logical length
capacity
element count
structure size
object lifetime

Layer 3: Access validity

Ask:

Does the actual operation stay within the object?

This includes:

pointer arithmetic
indexes
offsets
copy lengths
loops
subobject boundaries

A vulnerability can arise at any layer, but the layers are connected:

numerical validity
    ->
object validity
    ->
access validity

The original five-part chain is therefore a more detailed version of this three-layer model.


The Most Important Review Question

When you find a suspicious memory operation, ask:

What fact makes this access safe?

Do not accept answers such as:

"The value came from the header."

"The loop checks the count."

"malloc succeeded."

"The pointer is non-NULL."

"The offset was validated."

Instead, turn the claim into an equation.

For example:

Why is this write safe?

Because:

index < count

Why is that sufficient?

Because:

allocation contains count elements

Why does it contain count elements?

Because:

allocation_size = count * sizeof(*items)

Why is that calculation valid?

Because:

count <= SIZE_MAX / sizeof(*items)

Now the proof is explicit.

If one of those statements cannot be established, that is where the review should focus.


Final Exercise: Find the First Broken Link

Consider:

int decode(const unsigned char *input,
           size_t input_size,
           uint32_t count,
           uint32_t width)
{
    size_t bytes;
    unsigned char *out;

    if (count > 100000)
        return -1;

    bytes = count * width;

    if (bytes > input_size)
        return -1;

    out = malloc(bytes);
    if (out == NULL)
        return -1;

    for (uint32_t i = 0; i < count; ++i) {
        unsigned char *p = out + i * width;

        memcpy(p,
               input + i * width,
               width);
    }

    return 0;
}

Before reading further, identify the first broken invariant.

### Answer

The first suspicious operation is:

bytes = count * width;

The inputs are attacker-influenced values, and the multiplication can overflow.

The subsequent check:

if (bytes > input_size)

does not repair the problem.

An overflow can produce a small `bytes` value that passes the check.

The program can then allocate too little memory.

The loop still uses the original `count` and `width`:

i * width

and eventually calculates a pointer beyond the allocation.

The resulting chain is:

count, width
    |
    v
overflowing multiplication
    |
    v
incorrect bytes
    |
    v
incorrect allocation
    |
    v
i * width
    |
    v
pointer outside allocation
    |
    v
memcpy
    |
    v
out-of-bounds access

There is also a second side to the copy:

input + i * width

so the input-side range must be considered as well.

The first broken invariant is therefore numerical:

count * width <= SIZE_MAX

The memory corruption comes later.

That distinction is the central skill this chapter is designed to teach.


Closing Perspective

The strongest C vulnerability reviews do not merely identify dangerous statements.

They reconstruct the assumptions connecting those statements.

A typical chain looks like:

attacker-controlled number
    ->
integer conversion
    ->
arithmetic
    ->
allocation size
    ->
allocated object
    ->
pointer arithmetic
    ->
logical bounds
    ->
memory operation

At every transition, ask:

What invariant is supposed to hold?

Then ask:

Where is it established?

Then ask:

Can the value change before it is used?

Finally:

What happens if the invariant is false?

The first broken invariant is usually more valuable than the final crash site.

An integer overflow may look harmless until it controls an allocation.

An undersized allocation may look harmless until a loop trusts the original count.

A pointer calculation may look harmless until its offset is outside the object.

A bounds check may look correct until you discover that it checks the starting point but not the complete range.

And an invalid write may look like an isolated memory bug until you trace it backward to the attacker-controlled integer that started the chain.

That is the central discipline:

Do not inspect the memory access in isolation.

Follow the value.

Follow the size.

Follow the pointer.

Follow the bound.

Find the first invariant that stopped being true.

That is where the vulnerability begins.


← Previous 5 of 13 Next →