One Block, One Owner

Last lesson ended by handing you a word. make_squares allocated a block, filled it, returned the pointer, and main was left holding the only address of memory that would go on existing until somebody freed it. Ownership is the name for that obligation: at every moment, for every live block, exactly one piece of code is responsible for the free. Not zero pieces of code, and not two. What makes this a discipline rather than a feature is that C will not check it for you. There is no annotation to write and nothing in the type system that distinguishes an int * you must free from an int * you must leave alone, so ownership lives entirely in conventions, in function names, and in what you wrote down. A chapter that opened with array indexing closes on a habit, because on the heap the habit is the only thing there is.

Every heap bug this chapter has shown you is that one rule broken in one of three directions. Free a block twice and it had two owners, each believing the obligation was its own; last lesson's sanitizer report called that attempting double-free. Read a block after freeing it and you used memory you no longer owned, which was heap-use-after-free. Both of those are undefined behaviour, which is why the sanitizer stops the program dead rather than letting it carry on: once a program has done either one, the standard has withdrawn all promises about what it means, and the fact that it might still print the right answer is the trap rather than the consolation.

The third direction is this lesson's subject, and it is different in kind. A leak is a live block with no owner left: the last pointer to it was overwritten, or went out of scope, or was never handed to anybody, so the matching free can never be written by anyone. Here is the precision worth carrying: a leak is not undefined behaviour. A leaking program is well-defined C. Every statement in it means exactly what the standard says it means, the output is the output you were promised, and nothing about the run is unpredictable. The program has simply lost memory it can never return, and the loss is permanent for as long as the process lives. That is a real cost and in some programs a fatal one, since a server leaking a few hundred bytes per request grows until the allocator or the operating system refuses it more, but it is a cost of the ordinary kind, the kind you can reason about. Undefined behaviour is not a cost you can reason about. Keep the two failures in separate boxes.

What a Leak Looks Like Here

The next program is wrong on purpose, and the mistake is one line that is not there.

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int count = 0;

    if (scanf("%d", &count) != 1 || count <= 0)
    {
        printf("expected a positive count\n");
        return 0;
    }

    int *values = malloc(count * sizeof *values);

    if (values == NULL)
    {
        printf("could not allocate\n");
        return 0;
    }

    int total = 0;

    for (int i = 0; i < count; ++i)
    {
        if (scanf("%d", &values[i]) != 1)
        {
            printf("expected %d numbers\n", count);
            return 0;
        }

        total += values[i];
    }

    printf("total %d\n", total);

    free(values);

    return 0;
}

Give it 3 and then 5 6 7 and it prints total 18, frees its block on the last line, and there is nothing at all to report. The happy path is correct. Give it 3 and then 5 6 oops and scanf converts nothing on the third number, line 29 returns, and the block allocated on line 14 is still live with values the only address of it. values is a local in a frame that is about to be destroyed, so the moment that return 0; executes the address is gone from the program and no free is possible any more. The program prints expected 3 numbers on standard output as intended, and this arrives on standard error:

=================================================================
==17==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 12 byte(s) in 1 object(s) allocated from:
    #0 0xffff9ef14488 in malloc (/usr/local/lib64/libasan.so.8+0xd4488)
    #1 0x000000400aac in main /tmp/leak.c:14

SUMMARY: AddressSanitizer: 12 byte(s) leaked in 1 allocation(s).

Read the order of that carefully, because it is the leak's whole character. The program printed expected 3 numbers, which is the message it meant to print, and then it ran to completion and exited with status 0. Only then, after main had returned, did LeakSanitizer walk the heap looking for blocks nothing points at any more and write its findings to standard error. The sanitizer is not stopping anything here and there is nothing left to stop; it is filing a report about a program that already finished successfully. Compare that with last lesson's heap-use-after-free, which arrived in the middle of the run and ended it. The difference in when the diagnostic appears is the difference between undefined behaviour and a leak, made visible.

The report itself is three facts. 12 byte(s) is how much was lost, which is 3 * sizeof(int) and confirms this is the block you think it is. in 1 object(s) is how many separate allocations were orphaned, and it is the number to watch, because one object leaked once is a bug while one thousand objects leaked from the same line is a loop with no free in it. The stack below is the allocation stack, not the leak site: it shows where the block was born, malloc at frame #0 and main /tmp/leak.c:14 at frame #1, and leak.c:14 is the malloc line. That is the one place the sanitizer can point at, because there is no line where the leak happened, only lines where it failed to be prevented. Direct means nothing else on the heap points at this block either; you will meet indirect leaks when a leaked block holds pointers to further blocks.

One platform note you need before the exercises. That run exited 0, and a grader looking only at exit status and output would call it a pass. This platform does not: an exercise with fail_on_memory_leak set fails on a leak report even though the output is byte-perfect and the status is zero. This lesson's exercise has it set. The fix for the program above is one line, free(values); placed immediately above that return 0; on the failure path, and the point of the rest of this lesson is what to do when the fix is not one line.

Three Ways to Lose the Last Pointer

Leaks come in a small number of shapes. The first is overwriting the only pointer: p = malloc(...) followed later by another p = malloc(...) with no free in between orphans the first block at the instant of the second assignment, and the fix is to free before you reassign. The second is the early return just demonstrated, where the happy path frees correctly and an error path skips over it, which is by far the most common of the three in real code because the happy path is the one you test. The third is allocating in a loop: an allocation inside a loop body with the free outside it leaks every block but the last, and the object count in the report is your loop count. All three are the same failure, the last pointer to a live block disappearing, and all three are prevented by the same question asked at every return and every assignment: what do I own right now, and who frees it? One honest footnote on the first shape if you go and try it: this platform compiles at -O2, and a block that is allocated, never meaningfully read, and then orphaned can be deleted outright by the optimizer, so the report you expect may not appear. The leak is still a leak in the language; there was just nothing left of it by the time the program ran.

Handing the Obligation Over

Ownership becomes interesting the moment a block crosses a function boundary, and there are exactly two things that can happen. Either the obligation travels with the pointer or it does not, and nothing in the code says which, so the convention has to be loud. A function that returns a heap pointer transfers ownership to its caller: make_squares last lesson allocated, filled and returned, and from the return onwards the block was main's problem and main's free. The make_ prefix is doing real work in that name and is worth adopting, because a reader who sees make_something should expect to be handed a block along with the duty to release it, and the function's comment should say so in as many words. There is no other mechanism available.

A function that takes a pointer merely to look at the data does not take ownership, and here C gives you one small piece of help: const int *values from chapter 3 promises the function will not write through the pointer, which strongly implies it is a reader rather than a keeper. int sum_values(const int *values, int count) borrows the block for the length of the call and the caller still owns it afterwards, so a free inside sum_values would be an error even though nothing would stop you writing one. The rule of thumb that keeps this straight is to allocate and free in the same function whenever you can, and when you cannot, make the transfer obvious in the name and explicit in a comment. That is not a satisfying answer compared with a compiler that checks, and it is the honest one: in C, ownership is documentation that you are obliged to keep true.

Two Blocks and One Way Out

The discipline gets its real test when a function holds more than one block at once, because then every early exit has to give back a different set of them. Take a function that allocates values, then allocates doubled, then fills both from input. If the second malloc fails, the first block is live and owned by this function, so it must be freed before returning; skipping that is the leak from the last section, arriving on the day the machine is short of memory rather than the day you tested. If scanf then fails halfway through the fill, both blocks are live and both must go. Written out straight, that means free(values); above the second failure's return, and free(doubled); free(values); above the fill failure's return, plus the pair on the success path at the bottom. Three exits, five free calls, and every future exit you add is another chance to forget one. This is the situation, and only this one, where C's most disreputable statement earns its keep.

#include <stdio.h>
#include <stdlib.h>

int sum_doubles(int count)
{
    int *values = NULL;
    int *doubled = NULL;
    int total = -1;

    values = malloc(count * sizeof *values);

    if (values == NULL)
    {
        goto cleanup;
    }

    doubled = malloc(count * sizeof *doubled);

    if (doubled == NULL)
    {
        goto cleanup;
    }

    total = 0;

    for (int i = 0; i < count; ++i)
    {
        if (scanf("%d", &values[i]) != 1)
        {
            total = -1;
            goto cleanup;
        }

        doubled[i] = values[i] * 2;
        total += doubled[i];
    }

cleanup:
    free(doubled);
    free(values);

    return total;
}

int main(void)
{
    int total = sum_doubles(3);

    if (total < 0)
    {
        printf("expected three numbers\n");
        return 0;
    }

    printf("doubled total is %d\n", total);

    return 0;
}

Given 5 6 7 that prints doubled total is 36, and given 5 6 oops it prints expected three numbers, and neither run leaks a byte. Every exit from sum_doubles now goes through the same three lines, so there is exactly one place in the function where a free is written and exactly one place to check when you are reading it back. That is the payoff: not fewer keystrokes, but a single point of truth about what this function owns. The total = -1 sentinel is how the failure reaches main, since the function now has only one return. Notice the two details that make the pattern safe. Both pointers are initialized to NULL at the top, so if the first malloc fails and jumps straight to cleanup, free(doubled) is called on a null pointer, which last lesson established is a defined no-op. And the label frees in the reverse of allocation order, which costs nothing here and is the habit that stays correct when one block's size or contents depend on another. This is the one accepted use of goto in modern C, and it comes with rules that are not negotiable. Jump forward only, never backwards, because a backward goto is a loop written badly and there is no case where it beats while. Use a single cleanup label at the end of the function rather than a scatter of them, so there is one exit to reason about. Never jump into a block, into a loop body or an if, since that skips the initialization the block was counting on. Within those rules the pattern is not a loophole in a prohibition; it is what the C standard library and the Linux kernel do, and you will meet it in the first real C code you read. Outside those rules, goto remains exactly as bad as its reputation.

Key Takeaways

  • Ownership is the discipline that at every moment, exactly one piece of code is responsible for freeing each live block. C has nothing in its type system to express it, so it lives in conventions, function names and comments, and keeping it true is your job.
  • A leak is a live block whose last pointer is gone, so no free is possible. Unlike double free and use after free, a leak is not undefined behaviour: the program is well-defined and its output is trustworthy, it has just lost memory permanently. Real cost, different kind.
  • On this platform a leaking program runs to completion and exits 0, and LeakSanitizer prints its report to standard error afterwards. Read the byte count, the object count (many objects usually means a loop), and the stack, which points at the allocation rather than at any leak site. An exercise with fail_on_memory_leak fails on that report despite the zero exit status.
  • Three shapes cover most leaks: overwriting the only pointer, an early return that skips the free, and allocating in a loop with the free outside it. Ask at every return: what do I own right now?
  • Returning a heap pointer transfers ownership to the caller, which the make_ prefix and a comment should announce. Taking a const T * parameter does not: the callee borrows and the caller still frees.
  • When one function holds several blocks, jump forward to a single cleanup label that frees in reverse order and returns. Initialize the pointers to NULL so the label is safe from any exit. Forward only, one label, never into a block: that is goto's one legitimate job.