PART III

Chapter 9 - Using Memory After Its Lifetime

So far, we have treated memory as something with a location and a size.

There is another property that matters just as much:

lifetime.

A pointer can have the right address.

The access can have the right size.

The arithmetic can be correct.

And the access can still be invalid because the object no longer exists.

This gives us another dimension of the problem:

Where is the memory?
How much memory is there?
Does the object still exist?

A memory object has to satisfy all three.


1. Allocation creates an object

Consider:

p = malloc(size);

The important result is not merely an address.

The program has obtained storage that can be used for an object, subject to the relevant rules.

Conceptually:

allocation
    ->
object exists
    ->
pointer identifies object
    ->
accesses are possible

The object's lifetime begins as part of this process.

The program must then eventually stop using the object and release its storage.

That creates a temporal boundary in addition to the spatial boundary.


2. Freeing ends the lifetime

Consider:

p = malloc(size);

...

free(p);

After the `free`, the program cannot continue to use `p` as though the allocated object still existed.

The pointer value may still be stored in the variable.

That does not mean the object still exists.

This distinction is fundamental:

pointer value

is not the same thing as:

live object.

A pointer can remain in memory after the object it once identified has ceased to exist.


3. The stale pointer

Consider:

p = malloc(100);

...

free(p);

...

memcpy(dst, p, 10);

The copy length is only 10 bytes.

The address was originally valid.

The allocation was originally large enough.

But the object has been freed.

The problem is therefore not:

wrong amount

or:

wrong address.

It is:

wrong lifetime.

The pointer identifies storage that is no longer a live object available for the intended access.


4. Why this bug is called use-after-free

The pattern is:

use
->
free
->
use again

The second use occurs after the lifetime has ended.

The operation might be:

read
write
pointer dereference
function call through a stored function pointer
structure member access
copy
comparison
size calculation based on object contents.

The exact operation does not matter.

The key property is that the program relies on an object after its lifetime has ended.


5. A use-after-free is not necessarily an immediate crash

After:

free(p);

the allocator may leave the bytes unchanged for some time.

A later access through `p` might therefore appear to work.

This is one reason use-after-free bugs can be intermittent.

The memory may contain:

old contents

or:

allocator metadata

or:

contents belonging to a new allocation.

The program has lost the guarantee that the memory represents the original object.


6. Reuse makes the problem more interesting

Suppose:

p = malloc(100);
free(p);

Then:

q = malloc(100);

The allocator may return the same region for `q`.

Now:

p

and:

q

may have the same numerical address.

But they do not represent the same logical object.

The old object has ended.

A new object now occupies the storage.

Using `p` as though it still referred to the old object can therefore affect the new object.

This is one of the reasons use-after-free bugs can become security vulnerabilities.


7. The stale pointer can become a write primitive

Consider:

p = malloc(100);

...

free(p);

q = malloc(100);

...

p[0] = attacker_value;

If the allocator reused the same storage for `q`, the write through `p` may modify `q`.

The attacker has effectively gained a way to modify an object that the program did not intend to expose through `p`.

The exact result depends on allocator behavior and object layout.

But the general pattern is important:

expired pointer
    ->
reused storage
    ->
access to a different object.

8. The stale pointer can become an information leak

The same idea works for reads.

Suppose:

p = malloc(100);

...

free(p);

q = malloc(100);

...

send_to_user(p, 100);

If the storage has been reused, the read may expose data belonging to `q`.

The original pointer still has a plausible address.

The size is still exactly 100.

The problem is that the memory no longer belongs to the object the program believes it is reading.

This is another example of why location and lifetime cannot be considered independently.


9. Setting the pointer to NULL

A common defensive technique is:

free(p);
p = NULL;

This is useful because an accidental later dereference is more likely to fail obviously rather than access a reused allocation.

But this technique does not solve the general problem.

Suppose there are two pointers:

p
q

both referring to the same allocation.

Then:

free(p);
p = NULL;

does not change `q`.

The object is still freed.

`q` is now stale.

This is why ownership and aliasing matter.


10. Aliasing makes lifetime difficult

Consider:

p = malloc(100);
q = p;

Now there are two pointer variables.

There is still only one allocation.

If code does:

free(p);

then:

q

does not automatically become NULL.

It still contains the old pointer value.

The program therefore has to know that `q` is an alias of the object whose lifetime has ended.

This is one reason ownership discipline is so valuable in C.

Without it, the question:

"Who may free this object?"

can become surprisingly difficult.


11. Ownership

A useful design concept is ownership.

An owner is the part of the program responsible for determining:

who creates the object

who may release it

how long it remains available

which other code may temporarily reference it.

C does not enforce ownership in the type system.

The program has to establish it by convention and design.

Many lifetime bugs are really failures of ownership reasoning.


12. Borrowing

A function may receive a pointer without becoming responsible for freeing it.

For example:

void process(struct object *p);

The caller might retain ownership.

The function may temporarily use:

p

but must not assume that it can:

free(p);

unless the interface explicitly establishes that responsibility.

Conversely, if the caller frees the object while the function still intends to use it, the function can suffer a use-after-free.

The lifetime contract needs to be clear.


13. Returning pointers to dead objects

Consider:

char *get_name(void)
{
    char buffer[32];

    ...
    return buffer;
}

The pointer returned by the function refers to an object whose lifetime ends when the function returns.

The returned address may still look perfectly plausible.

But the object no longer exists.

This is a lifetime error without `free`.

The lesson is important:

> Lifetime is not synonymous with heap allocation.

Automatic objects also have lifetimes.


14. Stack lifetime

Consider:

struct item *get_item(void)
{
    struct item item;

    ...
    return &item;
}

The address can be formed correctly.

The object can be accessed correctly while the function is executing.

After the function returns, the object's lifetime has ended.

The pointer is therefore invalid for later use.

This is often called a dangling pointer.

The same concept applies to other objects whose lifetimes have ended.


15. Static storage is different

Objects with static storage duration have a much longer lifetime.

For example:

static struct object object;

A pointer to such an object does not become invalid merely because the function that produced the pointer returned.

This illustrates why the lifetime question must be answered from the object's storage duration and ownership model.

The pointer itself does not tell us enough.


16. Lifetime and bounds interact

Suppose:

p = malloc(100);

free(p);

q = malloc(20);

memcpy(p, src, 20);

The copy length is:

20

The old allocation had:

100

bytes.

But that is no longer relevant.

The old object's lifetime has ended.

The fact that the new allocation contains only 20 bytes may introduce another issue, but the first problem is already sufficient:

p is stale.

This illustrates an important ordering principle.

A range can be:

correctly sized

and:

correctly located

relative to an object that no longer exists.

The access is still invalid.


17. Lifetime and pointer arithmetic interact

Suppose:

p = malloc(100);

q = p + 50;

free(p);

The pointer:

q

was derived from the allocation.

After the allocation is freed, `q` cannot be treated as a pointer into a still-live 100-byte object.

The lifetime ended for the allocation as a whole.

This is why a pointer into the middle of an object is not independent of the lifetime of the object containing it.


18. Lifetime and copies interact

Consider:

struct object {
    char *data;
    size_t length;
};

A shallow copy:

copy = *original;

copies the pointer.

It does not create a new lifetime for the data.

If:

free(original.data);

then:

copy.data

may now be stale as well.

This is one reason copying structures containing pointers requires understanding ownership.

The bytes of the structure may have been copied correctly.

The resources represented by those bytes have not necessarily been duplicated.


19. Reference counting

Some programs solve shared lifetime problems with reference counting.

Conceptually:

acquire reference
    ->
reference count increases

release reference
    ->
reference count decreases

count reaches zero
    ->
object is freed

The important invariant is:

object remains alive while references that may use it remain outstanding.

Reference counting is not automatically correct.

It can fail through:

missing increments

missing decrements

duplicate decrements

reference cycles

races.

But the underlying purpose is clear:

make lifetime explicit.

20. Lifetime bugs are temporal bounds violations

Spatial memory safety asks:

Is this address inside the object?

Lifetime safety asks:

Does the object still exist?

These are related forms of boundary checking.

A spatial boundary looks like:

beginning -------- end

A temporal boundary looks like:

creation -------- destruction

A valid access must fall inside both.

Conceptually:

correct location
+
correct size
+
live object
=
valid memory access.

21. The review method

For any pointer, ask:

What object does it identify?

Who owns that object?

When was it created?

When can it be destroyed?

Can this code execute after destruction?

Are there aliases?

Can another operation free or replace the object?

These questions are often more productive than searching for `free()` calls in isolation.

The dangerous operation may be many functions away from the code that actually destroys the object.


22. The central lesson

Memory is not simply:

address + size.

It is:

object
+
location
+
size
+
lifetime.

A pointer becomes dangerous when the program forgets one of those dimensions.

A stale pointer may still contain the correct address.

The allocation may once have been large enough.

The access may still use the correct length.

None of that restores the object's lifetime.

Once an object has ceased to exist, the program must stop treating its old pointers as references to that object.

That brings us to the other half of lifetime management:

freeing memory correctly.
← Previous 11 of 13 Next →