PART II

Chapter 6 - Reading the Wrong Amount

Reading memory is often treated as the harmless side of memory safety.

A write can corrupt something.

A read merely observes it.

That distinction is useful, but it can also be misleading.

A read still has to identify a valid range of memory. If the program reads beyond the intended object, it has violated the boundary of that object even if no byte is modified.

The immediate result may be a crash.

It may also be a value that the program was never supposed to see.

In security-sensitive programs, that second possibility can be especially important.

The central question is:

How many bytes does this operation read,
and why is that many bytes available?

1. A read is a range operation

Consider:

value = buffer[index];

The operation is not simply:

read buffer

It is:

locate buffer[index]
read the object stored there

For an array of `count` elements, the basic condition is:

index < count

For a byte range:

offset < size

For a multi-byte access:

offset + access_size <= size

The exact expression varies with the object.

The principle does not.

A valid starting address is not enough.

The complete range being read must be valid.


2. Out-of-bounds reads

Suppose:

char buffer[16];

and:

value = buffer[16];

The index is one past the last element.

There are 16 valid byte positions:

0 through 15

The access attempts to read position:

16

The problem is not that the number 16 is inherently invalid.

It is invalid relative to this particular object.

This distinction is important throughout C:

> Bounds belong to objects, not to integers.


3. Reading past the end can look harmless

Consider:

int value = array[index];

If `index` is out of bounds, the program may appear to continue normally.

It might read:

zero

or:

some plausible integer

or:

data belonging to another object.

Nothing about the resulting value tells you whether the access was valid.

This makes out-of-bounds reads particularly easy to miss.

The program can produce a perfectly ordinary value from an invalid memory access.


4. The adjacent object problem

Imagine memory conceptually arranged as:

+----------------+
| object A       |
+----------------+
| object B       |
+----------------+

If code reads one byte beyond object A, the address may fall inside object B.

The CPU does not necessarily know that the program intended to remain inside A.

The address is simply an address.

This is one reason memory safety cannot be reduced to:

"Does this address point to mapped memory?"

The stronger question is:

"Does this access belong to the object the program intended to access?"

A readable address can still be the wrong address.


5. Information disclosure

An out-of-bounds read can expose data that should not have been included in the result.

Depending on the surrounding program, that data might include:

pointers
object metadata
configuration data
authentication state
previously processed input
cryptographic material
memory addresses

The exact consequence depends on what happens to be adjacent to the accessed object.

This is why an out-of-bounds read should not be dismissed merely because it does not overwrite memory.

The program may be giving an attacker information about memory that was never intended to be observable.


6. A read can become a write later

Consider:

value = buffer[index];

The immediate operation is a read.

But suppose `value` is then used as:

length

or:

index

or:

pointer offset

or:

allocation size.

The invalid read can therefore influence subsequent memory operations.

A useful chain is:

out-of-bounds read
    ->
attacker-controlled or unintended value
    ->
derived calculation
    ->
later memory operation

This is another reason to follow data flow rather than classifying bugs solely by the instruction that first went wrong.


7. Length fields are especially important

Consider a packet containing:

length
data

A parser might do:

length = packet->length;
value = packet->data[length - 1];

The programmer may believe that the length field establishes the boundary.

It does not.

The length field is merely a value stored somewhere.

The program still needs to establish that the complete packet actually contains that many bytes.

A malicious or corrupted length can therefore cause the program to read beyond the received data.

The general distinction is:

claimed size

versus:

available size.

The second must be established independently.


8. `strlen` is an unbounded search

Consider:

n = strlen(buffer);

`strlen` does not receive the size of the buffer.

It searches for a null byte.

That means the caller is effectively asserting:

A null byte exists within readable memory reachable from buffer.

If that assumption is false, `strlen` can read beyond the intended object.

This makes functions that search for terminators particularly important during review.

The hidden length of the operation does not make the operation bounded.


9. The terminator is part of the contract

For a valid C string, the terminating null byte must be accessible.

Suppose:

char buffer[8];

contains seven characters but no null terminator.

The buffer may contain seven meaningful bytes.

It is not necessarily a valid C string.

Calling:

strlen(buffer);

asks the function to continue reading until a null byte is found.

If the null byte is outside the object, the search has already crossed the boundary.

The problem is therefore not `strlen` itself.

The problem is that the caller supplied a pointer without establishing the precondition required by the operation.


10. Bounded functions are not automatically safe

Consider:

strncpy(dst, src, n);

The presence of `n` makes the operation bounded in a particular sense.

But the programmer still needs to reason about:

n

the source range, and the destination range.

If `n` exceeds the destination capacity, the destination can still be overrun.

If `src` does not contain enough readable bytes, the operation still has to be understood in terms of the function's actual semantics.

The general lesson is:

> A bound supplied to an API is useful only if it is a bound on the resource the API actually needs to access.


11. Multi-byte reads

Consider:

uint32_t value = *(uint32_t *)(buffer + offset);

The program does not merely read one byte at `offset`.

It reads an object of the relevant type.

Therefore the available range must be large enough for the complete access.

Conceptually:

offset + sizeof(uint32_t) <= buffer_size

A check that establishes only:

offset < buffer_size

is insufficient.

There may be enough memory for the first byte but not enough for the complete object.

This distinction becomes especially important when reading structured binary data.


12. Parsing fields from a buffer

Suppose a parser does:

type = read_u32(buffer + offset);
offset += 4;

The parser has to maintain an invariant such as:

offset + 4 <= buffer_size

before the read.

After the read, the new offset must still describe a position within the input.

The parser is therefore maintaining a moving boundary.

Every field consumes part of the available range.

A single incorrect length can desynchronize the entire parser.


13. One bad length can move every later read

Suppose a message contains:

header
length
payload
next field

The parser reads the length and advances:

offset += length;

If `length` is too large, the parser may skip over the intended payload and interpret later bytes incorrectly.

If `length` is too small, it may interpret payload bytes as the next field.

The immediate result may not be an out-of-bounds read.

Instead, the parser's model of where it is in the input becomes wrong.

That can cause later reads to access the wrong locations.

Thus a size error can eventually become a location error.


14. Unsigned values do not guarantee safe lengths

A common mistake is to assume that using `size_t` or another unsigned type makes a length safe.

It does not.

For example:

length = user_value;

If `user_value` represents an invalid negative value before conversion, converting it to an unsigned type can produce a very large positive value.

The type is appropriate for representing sizes.

That does not mean every value of that type is a valid size.

The boundary still has to be established.


15. Subtraction creates another class of errors

Consider:

remaining = size - offset;

The expression assumes:

offset <= size

If that relationship is false and the type is unsigned, the result can wrap to a very large value.

Code might then use:

remaining

as a read length.

The original problem was an invalid relationship between two positions.

The visible failure becomes an enormous length.

Again:

bounds
    ->
arithmetic
    ->
memory operation.

The categories overlap because the underlying reasoning is connected.


16. Reading an object is not the same as reading its representation

Suppose:

struct header *h;

and code reads:

h->length

The program is not merely reading some bytes.

It is treating the memory as a valid `struct header` object.

That requires the relevant memory to be available and appropriately represented for that access.

This becomes particularly important when interpreting raw input as structures.

A buffer containing enough bytes does not automatically mean that every possible typed access into it is appropriate.


17. Reading beyond an array is different from reading uninitialized data

These problems are often confused.

An out-of-bounds read accesses memory outside the object's permitted range.

An uninitialized read accesses an object or bytes whose value has not been properly initialized.

Both can produce unintended values.

They are conceptually different failures.

For this book, the important distinction is:

Is the range wrong?

or:

Is the value within the range not properly established?

The two can also combine.


18. Why out-of-bounds reads matter in exploits

A read primitive can be valuable because it can reveal information needed for a later attack.

For example, a program may contain randomized addresses.

If an attacker can obtain an unintended pointer value from memory, that information may reduce the uncertainty surrounding the process layout.

The important security property is not merely:

"the program read one extra byte."

It is:

"the attacker gained information that the program was not supposed to disclose."

The severity therefore depends heavily on what can be read and how the result can be controlled or observed.


19. The review question

When reviewing a read, ask:

What object is being read?

Where does the read begin?

How many bytes does it consume?

What establishes the available range?

Is that range physical or merely logical?

Can the size calculation overflow or underflow?

Can the starting offset be attacker-controlled?

What happens to the value after it is read?

These questions turn a seemingly harmless read into a concrete range analysis.


20. The central lesson

A read is not safe merely because:

the pointer is non-NULL

or:

the address is mapped

or:

the program does not crash.

The relevant question is:

> Does the complete range being read belong to the intended live object?

That is the boundary between an ordinary read and an out-of-bounds read.

And when the answer is no, the next question is:

What did the program learn from memory it was not entitled to read?

That leads naturally to the other side of the operation.

Writing memory has the same range problem, but the consequences can be more direct.


← Previous 8 of 13 Next →