PART III

Chapter 10 - Freeing the Wrong Memory

Allocation creates memory.

Freeing ends its lifetime.

The apparent simplicity of:

free(p);

hides an important requirement:

p

must identify the allocation that the program is entitled to release.

Unlike `memcpy`, `free` does not take an explicit size.

The allocator already has to know the allocation's size.

This makes the correctness question different.

The central question is:

> Does this pointer identify a currently allocated object that may be released here?


1. Freeing is about ownership

Suppose:

p = malloc(100);

The program now has an allocation.

Eventually:

free(p);

may release it.

But not every function that receives `p` necessarily owns the right to release it.

Consider:

void process(char *p)
{
    ...
}

The interface does not tell us whether `process` should:

use p

or:

free p

or:

retain p

or:

return ownership elsewhere.

Ownership has to be established by the program's contract.


2. Freeing an interior pointer

Consider:

p = malloc(100);

q = p + 20;

free(q);

The pointer `q` identifies an interior location.

It does not identify the original allocation in the form required for releasing it.

The fact that `q` points into allocated storage does not make it a valid argument to `free`.

This is a useful distinction:

pointer into allocation

versus:

pointer returned by the allocation operation.

The latter is what the allocator expects for releasing the allocation.


3. Freeing a stack object

Consider:

char buffer[100];

free(buffer);

The object was not obtained from the corresponding allocation mechanism.

Its lifetime is governed by automatic storage duration.

It must not be passed to `free`.

This is another example of why an address alone does not determine what operation is valid.

The storage's origin matters.


4. Freeing static storage

Similarly:

static char buffer[100];

cannot be released with:

free(buffer);

The object has static storage duration.

There is no matching dynamic allocation to release.

The general principle is:

> Free the object according to the mechanism that created its lifetime.


5. Freeing the same object twice

Consider:

p = malloc(100);

free(p);
free(p);

The second operation attempts to release an object whose lifetime has already ended.

This is a double free.

The first `free` is valid.

The second is not.

Double free is therefore closely related to use-after-free.

Both involve a program continuing to treat an ended allocation as though it were still available.


6. Why double free can be dangerous

Modern allocators contain defenses against many forms of double free.

But the underlying bug is still dangerous because the allocator's internal state may be affected.

Historically, heap-management attacks often attempted to turn allocator metadata corruption into control over later allocations.

Modern implementations have made many classic techniques harder.

That does not make double free harmless.

It remains a memory-management violation whose security consequences depend on allocator behavior, program structure, and available attacker control.


7. Freeing through multiple owners

Consider:

p = malloc(100);

a->data = p;
b->data = p;

If both `a` and `b` believe they own `data`, either one may eventually execute:

free(data);

The second owner can then attempt to free the same allocation again.

The problem is not necessarily in either `free` call individually.

The problem is that ownership was duplicated.

A pointer value can be copied freely.

Ownership cannot safely be duplicated without a corresponding lifetime protocol.


8. Shallow copies create ownership problems

Consider:

struct object {
    char *data;
};

Then:

copy = original;

makes:

copy.data == original.data

Both structures now contain the same pointer.

If both structures later execute:

free(data);

the allocation may be freed twice.

Alternatively, one may free it while the other continues using it.

The original copy operation was valid at the byte level.

The resulting ownership model was not.

This is a recurring pattern in C:

representation copying

does not automatically imply:

ownership copying.

9. Ownership transfer

Sometimes copying a pointer is intentional because ownership is being transferred.

For example:

new_owner->data = old_owner->data;
old_owner->data = NULL;

The exact details vary, but the principle is:

one owner stops being responsible
while another becomes responsible.

The important invariant is that there should be one clear authority responsible for eventually releasing the allocation.

Ownership transfer is a protocol.

It should not be left implicit.


10. Freeing too early

Suppose:

owner->data = malloc(100);

A function receives:

owner->data

and decides to free it.

If the owner still expects the object to exist, the program has created a dangling pointer.

The problem is therefore not necessarily an invalid `free` argument.

The `free` itself may have been valid.

The error is that the object was released before all users were finished with it.

This distinction is crucial:

invalid free

versus:

premature free.

11. Freeing too late

The opposite problem is a memory leak.

Suppose an allocation is no longer reachable by the program, but is never released.

The memory remains allocated even though no useful owner can access it.

A leak is not usually an immediate memory-corruption vulnerability.

But repeated leaks can exhaust resources.

In long-running or remotely accessible programs, resource exhaustion can become a denial-of-service problem.

Thus lifetime has two failure directions:

too early

and:

too late.

12. The wrong allocator

Some programs use multiple allocation systems.

For example:

malloc/free

and:

another_allocator/another_free

may have different ownership and metadata requirements.

A pointer allocated by one mechanism generally cannot simply be released through an unrelated mechanism.

The important question is:

Which allocator created this object?

The answer determines which release operation is appropriate.


13. Custom allocators

Large programs often implement:

arenas
pools
slabs
object caches
region allocators.

In such systems, `free` may not be the operation that ends an object's lifetime.

For example, an object might be released by:

pool_release(object);

or:

arena_destroy(arena);

The memory-safety reasoning remains the same.

The program needs to know:

who owns the object

what operation ends its lifetime

when that operation may occur.

14. Freeing the wrong allocation

Suppose a function manages several related allocations:

header
payload
metadata

and accidentally frees:

metadata

when it intended:

payload.

The pointer may be perfectly valid.

The allocation may be live.

The `free` operation may satisfy the allocator.

Yet the program has destroyed the wrong object.

The subsequent failure may occur when code later uses the freed payload.

This is a wrong-location error expressed through lifetime management.


15. Freeing based on corrupted pointers

Consider:

free(object->data);

If an earlier memory corruption changed:

object->data

the `free` operation may receive a pointer that no longer identifies the intended allocation.

Thus a memory corruption can become a second memory-management bug.

The chain may look like:

out-of-bounds write
    ->
pointer corruption
    ->
invalid free
    ->
allocator corruption
    ->
later memory corruption.

This is why memory bugs can amplify one another.


16. Null is safe for `free`

One useful property of C's standard allocation interface is that:

free(NULL);

has no effect.

This makes a common cleanup pattern possible:

free(p);
p = NULL;

and allows cleanup code to call `free` on a pointer that may already be NULL.

But NULL safety does not solve ownership problems.

If:

p

is non-NULL and stale, `free(p)` remains problematic.

The useful invariant is:

p is either NULL
or identifies the allocation that this code owns.

17. Cleanup paths are particularly dangerous

Complex functions often have multiple exits:

success
failure
timeout
cancellation
validation failure

Each path may perform cleanup.

This creates opportunities for:

double free

missing free

premature free

freeing the wrong object

using an object after cleanup.

A common way to reason about such code is to identify the ownership state at every exit.

For each allocation, ask:

Who owns it here?

Has it already been released?

Will another cleanup path release it?

18. Error handling changes lifetime

Consider:

p = malloc(...);

if (step1() < 0)
    goto cleanup;

if (step2() < 0)
    goto cleanup;

...

cleanup:
free(p);

This pattern can be robust if ownership is clear.

But if some step transfers ownership:

consumer_take(p);

then the cleanup path may no longer be allowed to free it.

The ownership state has changed.

A cleanup label does not magically know that.

The code must preserve the ownership invariant.


19. Freeing is not erasing

Consider:

free(p);

The call ends the allocation's lifetime.

It does not guarantee that the old bytes are immediately erased.

It does not guarantee that the memory is immediately unmapped.

It does not guarantee that stale copies of the pointer disappear.

This matters because:

free

is fundamentally a lifetime operation.

It is not a secure erasure primitive.

If sensitive data needs explicit clearing, that is a separate requirement.


20. The allocator may retain the bytes

After:

free(p);

the old contents may remain physically present for some time.

A later allocation may reuse the same memory.

Therefore:

free

does not mean:

the old bytes instantly ceased to exist physically.

It means the program no longer has the right to treat the old allocation as a live object.

This distinction helps explain why use-after-free and stale-data problems are possible.


21. The review method

For every release operation, ask:

Who allocated this object?

Which mechanism allocated it?

Who owns it now?

Is the object still live?

Is this the original allocation pointer?

Has it already been released?

Does any other live pointer still depend on it?

Does any other cleanup path also release it?

These questions reveal most ownership and lifetime mistakes.


22. The central lesson

`free` is not simply:

give this address back to the allocator.

It is:

end the lifetime of this particular allocation.

That operation is valid only when the pointer, allocation mechanism, ownership, and lifetime all agree.

The dangerous cases include:

freeing an interior pointer

freeing non-dynamic storage

freeing twice

freeing too early

freeing the wrong object

freeing through the wrong allocator

freeing an allocation whose ownership has already moved.

The deeper lesson is that allocation and release are a matched pair.

The program needs to preserve the relationship:

allocation
    ->
ownership
    ->
use
    ->
ownership transfer, if any
    ->
release
    ->
no further use.

Breaking that sequence creates dangling pointers, double frees, leaks, and corrupted allocator state.


← Previous 12 of 13 Next →