PART III
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?
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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?
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.
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.
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.
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.
`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.