PART III

Chapter 11 - The Complete Memory Operation

At this point, the individual pieces can be combined.

A memory operation is not defined solely by:

pointer
+
length.

To reason about it correctly, we need at least:

object
location
amount
lifetime
ownership.

Remember the phrase introduced at the beginning of this book:

I calculated, addressed, allocated, copied, or freed the wrong amount or location of memory.

Each verb describes a stage in a chain.

A mistake at one stage can become a different-looking mistake at a later stage.


1. The complete chain

Consider a program receiving:

count

items from an external source.

It might perform:

bytes = count * sizeof(*items);

then:

buffer = malloc(bytes);

then:

p = buffer + offset;

then:

memcpy(p, input, length);

then later:

free(buffer);

Each operation depends on the correctness of the previous ones.

Conceptually:

input
  ->
integer arithmetic
  ->
allocation size
  ->
object
  ->
pointer arithmetic
  ->
location
  ->
bounds
  ->
copy amount
  ->
memory access
  ->
lifetime
  ->
release

This is the central chain of the book.


2. The first error may not be where the crash occurs

Suppose:

count

is enormous.

The multiplication:

count * sizeof(*items)

overflows.

The allocation becomes too small.

The program then copies the calculated number of bytes.

The copy may or may not immediately cross the allocation boundary.

Later, code accesses:

items[i]

using the original `count`.

The crash occurs there.

The visible failure is an out-of-bounds access.

The original defect was an integer calculation.

This is why debugging from the crash site alone can be misleading.


3. One error can create another

Suppose:

length

is too large.

The program performs:

memcpy(buffer, input, length);

and overwrites:

object->size

The next operation does:

memcpy(object->data, input2, object->size);

Now the second copy has a bad length.

The program has gone from:

one spatial error

to:

corrupted metadata
    ->
another spatial error.

This cascading behavior is common in memory corruption.


4. A bad location can become a bad lifetime

Suppose:

object->next

is a pointer.

An out-of-bounds write changes it.

Later:

free(object->next);

The pointer now identifies something other than the intended allocation.

The original bug was a write to the wrong location.

The later symptom is an invalid or unintended free.

The categories are connected because memory pointers are themselves data stored in memory.


5. A bad lifetime can become a bad location

Suppose:

p

points to an object that has been freed.

The allocator reuses that memory for another object.

A later write through `p` now reaches the new object.

The original bug was:

lifetime.

The resulting effect is:

wrong object/location.

Again, the categories are not isolated.


6. A bad amount can become a bad location

Suppose:

offset = index * element_size;

If the multiplication wraps, the offset may point somewhere unexpected.

The program then performs:

buffer + offset.

The resulting problem is a wrong location.

The underlying defect was arithmetic.

This is why it is useful to trace memory bugs backward through their calculations.


7. A bad allocation can create a later bounds failure

Suppose the intended allocation size is:

count * element_size

but the actual calculation produces:

smaller_size.

The allocation succeeds.

The pointer is valid.

The copy length may even be correct relative to the intended object.

But the physical destination is too small.

The first operation that notices the discrepancy may be:

memcpy.

The allocation created the wrong boundary.

The copy crossed it.


8. A bad copy can corrupt a future allocator operation

Suppose an object contains:

size
pointer

An out-of-bounds write corrupts:

pointer.

Later:

free(object->pointer);

The allocator receives corrupted state.

The memory-management bug is therefore downstream from the original copy.

This pattern is particularly important when investigating security vulnerabilities.

The operation that crashes may be several stages removed from the operation that gave the attacker control.


9. The five questions

For practical analysis, five questions cover much of the ground:

1. What object is this?

2. Where within that object is the operation occurring?

3. How much memory is being accessed?

4. Is the object still alive?

5. Who owns it?

The first three are spatial.

The fourth is temporal.

The fifth is about responsibility.

A valid memory operation requires all of them to line up.


10. The object model

When reviewing code, it helps to write down the object explicitly.

For example:

object:
    allocation of 1024 bytes

logical contents:
    80 records

current position:
    640 bytes

remaining capacity:
    384 bytes

lifetime:
    allocated and live

owner:
    parser context

Now consider:

memcpy(buffer + 640, src, 400);

The source may be valid.

The destination is not.

There are only:

384

bytes remaining.

The operation asks for:

400.

The error becomes obvious once the object is described explicitly.


11. The same operation as a proof

A memory operation can be viewed as requiring a small proof.

For:

memcpy(dst, src, length);

the proof needs to establish:

src identifies a live source object

length bytes are readable from src

dst identifies a live destination object

length bytes are writable from dst

the operation's overlap requirements are satisfied

the size calculation produced the intended length

the pointer calculations produced the intended locations.

This is a much stronger mental model than:

"The pointers look right."

12. The allocation proof

For:

p = malloc(size);

the important facts include:

size represents the intended object size

the calculation producing size is valid

the result is checked appropriately

the program records the actual capacity

later accesses do not assume more storage than was obtained.

The allocation is the point where an abstract size becomes a physical boundary.

If the boundary is wrong, every later access is working from a false premise.


13. The pointer proof

For:

q = p + offset;

the relevant facts include:

p identifies the intended live object

offset has the correct unit

offset is within the permitted range

the arithmetic produces the intended location.

The pointer calculation should not be treated as an opaque transformation.

It is part of the memory-safety proof.


14. The bounds proof

For:

access(q, length);

we need:

q identifies the intended location

and:

length bytes remain available there.

Conceptually:

q + length <= object_end

But the arithmetic used to establish this relationship must itself be safe.

A bounds check that can overflow is not a valid proof of the bound.


15. The lifetime proof

For any pointer access, ask:

Does the object still exist?

This question is independent of the numerical address.

An address can be:

mapped

readable

writable

and still be wrong because the program is using it after the original object's lifetime ended.

Lifetime is therefore a separate dimension of memory safety.


16. The ownership proof

Finally:

Who is responsible for this object?

This determines:

who may free it

when it may be freed

whether another component may retain a pointer

whether a shallow copy creates a shared resource

whether ownership has been transferred.

Without an ownership model, lifetime reasoning becomes fragile as programs become larger.


17. Why bugs cluster at boundaries

Memory bugs often appear where one concept is translated into another:

count -> bytes

bytes -> allocation

allocation -> pointer

pointer -> offset

offset -> range

range -> copy

object -> owner

owner -> lifetime

Each transition introduces an assumption.

The most useful review strategy is therefore not to stare at individual instructions.

Look for the transitions.

Ask:

What does this value mean here?

What did it mean before?

What unit does it use?

What boundary does it describe?

What object does it refer to?

Who is responsible for keeping that object alive?

18. The dangerous word "size"

A variable named:

size

may mean:

allocation size

input size

payload size

element count

byte count

remaining capacity

logical object size

string length

serialized length.

These are not interchangeable.

Many memory bugs begin when two values with the same type but different meanings are treated as equivalent.

The type system sees:

size_t

The programmer must preserve:

what kind of size?

19. The dangerous word "pointer"

Similarly, a variable named:

p

may point to:

the beginning of an allocation

an element inside an array

a structure member

a serialized field

a borrowed object

an object owned elsewhere

freed storage.

The numerical value does not tell us which role it has.

Good memory-safety reasoning therefore tracks pointer meaning, not merely pointer values.


20. The dangerous word "buffer"

A buffer may have:

physical capacity

logical length

remaining capacity

input length

output length

maximum permitted length.

A function that receives:

buffer

and:

length

still needs to know what the length represents and what boundary it is supposed to respect.

The word `buffer` does not establish the range.


21. From memory bug to vulnerability

A memory bug becomes a security vulnerability when an attacker can influence the conditions of the invalid operation and the resulting corruption or disclosure has useful consequences.

A useful sequence is:

attacker influence
    ->
incorrect calculation/location/lifetime
    ->
invalid memory operation
    ->
observable effect
    ->
security consequence.

The observable effect might be:

crash

information disclosure

data corruption

arbitrary memory write

control-flow corruption

privilege-relevant state change.

The final consequence depends on the exact program.


22. Not all memory bugs are equally powerful

Compare:

one-byte out-of-bounds read

with:

attacker-controlled arbitrary write.

Both violate memory safety.

They do not provide the same capability.

A useful vulnerability analysis therefore asks:

What can the attacker influence?

What memory can be accessed?

In which direction?

For how many bytes?

With what values?

How reliably?

Under what conditions?

This is the bridge from bug classification to exploit analysis.


23. The memory primitive

It is useful to describe an exploitable memory bug in terms of the primitive it creates.

For example:

out-of-bounds read

might provide:

disclosure of adjacent memory.

An out-of-bounds write might provide:

corruption of adjacent memory.

A use-after-free might provide:

access to memory reused for another object.

A double free might provide:

corruption or manipulation of allocator state.

The primitive is more informative than the bug name alone.


24. From primitive to target

Suppose an attacker has some form of write primitive.

The next question is:

What can be reached?

If the only reachable bytes are unimportant data, the impact may be limited.

If the attacker can affect:

object metadata

pointers

authorization state

control-flow data

the consequences may be much greater.

Thus exploitability depends on the relationship between:

primitive

and:

target.

25. Reliability matters

A bug that crashes the program once in a thousand attempts is different from one that provides a deterministic write every time.

Security analysis therefore considers:

reliability

repeatability

environmental dependence

allocator behavior

process layout

concurrency.

The underlying memory error may be identical.

The practical exploitability can be very different.


26. Modern defenses change consequences

Modern systems commonly employ defenses such as:

address randomization

non-executable memory

stack protection

hardened allocators

control-flow protections

compiler instrumentation

These can make exploitation harder.

They do not make the underlying memory operation correct.

A program that writes outside an object remains incorrect even if a particular defense causes the program to crash instead of being exploited.

This distinction matters when evaluating vulnerabilities:

bug correctness

versus:

exploitability

are separate questions.


27. The debugging trap

When a memory bug crashes under a debugger, the temptation is to fix the instruction that crashed.

That can be useful.

But the crash may be only the last visible consequence.

Trace backward:

What pointer was used?

How was it calculated?

What size was used?

How was the size calculated?

What allocation established the boundary?

Is the object still alive?

Who owns it?

This backward chain often reaches the real defect.


28. The complete chain

The central chain can now be written in a more complete form:

integer arithmetic
    ->
allocation size
    ->
object boundary
    ->
pointer arithmetic
    ->
object location
    ->
bounds
    ->
amount accessed
    ->
memory operation
    ->
object lifetime
    ->
ownership
    ->
release
    ->
possible reuse
    ->
possible corruption or disclosure.

The stages are not strictly linear.

They form a network.

A bad integer can create a bad allocation.

A bad allocation can create a bad bound.

A bad bound can create a bad pointer.

A bad pointer can create corruption.

Corruption can alter ownership metadata.

That can cause an invalid release.

The invalid release can create a dangling pointer.

The dangling pointer can later access a new object.

One mistake can therefore feed another.


29. The compact rule

When faced with suspicious C memory code, reduce the problem to this:

OBJECT
    What object is this?

LOCATION
    Where inside that object is the operation?

AMOUNT
    How many bytes or elements are involved?

LIFETIME
    Does the object still exist?

OWNERSHIP
    Who is responsible for it?

Then trace every value that establishes those facts.

If one of them cannot be justified, there is a gap in the memory-safety argument.


30. The deeper lesson

The difficult part of C memory safety is rarely the syntax.

The syntax is usually simple:

p[i]

p + offset

malloc(size)

memcpy(dst, src, length)

free(p)

The difficulty is maintaining the relationships between the values.

A correct program must preserve relationships such as:

allocation_size >= required_size

offset <= capacity

length <= remaining_capacity

source_range is live and readable

destination_range is live and writable

pointer identifies the intended object

object remains alive while references exist

ownership is transferred before the previous owner releases it

These are invariants.

Memory bugs occur when the program assumes an invariant that is no longer true.


31. The phrase

The phrase that began this book is a compact description of a large class of C memory failures:

I calculated, addressed, allocated, copied, or freed the wrong amount or location of memory.

It covers several different moments in the life of a memory object.

I calculated the wrong amount:

integer overflow
truncation
unit confusion
incorrect size formulas

I allocated the wrong amount:

undersized allocation
oversized or inconsistent allocation
allocation based on corrupted metadata

I addressed the wrong location:

bad index
bad offset
wrong base pointer
stale pointer
wrong object

I copied the wrong amount:

buffer overflow
out-of-bounds read
truncated object
missing terminator

I freed the wrong thing:

invalid free
double free
premature free
wrong owner
wrong allocation

And each category can feed another.


32. The final review question

When reviewing a potentially dangerous memory operation, ask one question first:

What exact bytes does the program believe it is operating on, and what evidence establishes that those bytes belong to the intended live object?

Then decompose the answer:

Which object?

Which location?

Which amount?

Which lifetime?

Which owner?

If the program cannot answer one of these questions, that is where the investigation should begin.

The machine will execute the pointer arithmetic.

The machine will perform the copy.

The machine will dereference the pointer.

The machine will call the allocator.

It will not know what the programmer intended.

The programmer's job is to preserve the relationship between intention and memory.

That relationship is the real subject of memory safety in C.

← Previous 13 of 13 Appendix I - Making Memory Safety a Property of the Language→