Memory leaks and ownership
Every block from lesson 31 needs a free, and the hard part is not the call but knowing which function should make it. A leak is a block whose pointer was lost before the free; a use after free is the opposite mistake, a pointer kept after the block was returned. Ownership rules prevent both.
A leak you can count
A leak is invisible in the output of a program that ends immediately, because the operating system reclaims everything at exit. A counter in two small wrappers makes it visible: every successful allocation increases it and every free decreases it, so the final number is the number of blocks still owned by nobody.
#include <stdio.h>
#include <stdlib.h>
static int outstanding = 0;
static void *track_malloc(size_t size)
{
void *block = malloc(size);
if (block != NULL) {
outstanding++;
}
return block;
}
static void track_free(void *block)
{
if (block != NULL) {
outstanding--;
}
free(block);
}
static void leak_one(void)
{
int *value = track_malloc(sizeof *value);
if (value != NULL) {
*value = 1;
}
}
static void clean_one(void)
{
int *value = track_malloc(sizeof *value);
if (value != NULL) {
*value = 2;
track_free(value);
}
}
int main(void)
{
leak_one();
leak_one();
clean_one();
printf("outstanding: %d\n", outstanding);
return 0;
}
gcc -Wall -Wextra leak.c -o leak
./leak
outstanding: 2
Three blocks were allocated and one was released, so two remain. The pointer in leak_one went out of scope at the return, and with it the only reference to the block. The leak grows with every call, which matters in a server that handles requests for days.
Who frees
The rule that keeps a project consistent is simple: the function that allocates either frees the block before returning or transfers ownership to its caller, and the caller's documentation says so. The example follows the transfer rule: make_number allocates, main receives the pointer and calls release.
#include <stdio.h>
#include <stdlib.h>
static int outstanding = 0;
static int *make_number(int value)
{
int *number = malloc(sizeof *number);
if (number != NULL) {
outstanding++;
*number = value;
}
return number;
}
static void release(int *number)
{
if (number != NULL) {
outstanding--;
}
free(number);
}
int main(void)
{
int *number = make_number(42);
if (number == NULL) {
return 1;
}
printf("%d\n", *number);
release(number);
printf("outstanding: %d\n", outstanding);
return 0;
}
gcc -Wall -Wextra owner.c -o owner
./owner
42
outstanding: 0
The counter returns to zero, which is the invariant of the whole program. In a larger project the same rule is written in the header next to the declaration, and the review question for every allocation is who calls the matching free.
One exit path frees
A function with several failure returns needs a free on each of them, and a forgotten one is exactly the leak the counter shows. The goto cleanup from lesson 15 gives every path one exit, so the free exists once.
#include <stdio.h>
#include <stdlib.h>
static int process(int fail)
{
int *value = malloc(sizeof *value);
int status = 0;
if (value == NULL) {
return 1;
}
*value = 7;
if (fail) {
status = 2;
goto cleanup;
}
printf("value: %d\n", *value);
cleanup:
free(value);
return status;
}
int main(void)
{
printf("status: %d\n", process(1));
printf("status: %d\n", process(0));
return 0;
}
gcc -Wall -Wextra cleanup.c -o cleanup
./cleanup
status: 2
status: 2
value: 7
status: 0
The failing call still prints its status, and the successful call prints the value between the two status lines. The label sits after the normal work, so the success path falls into it and every path reaches the same free.
Free and clear
After free the pointer still holds the old address, and a second free through it is the crash from lesson 31. Assigning NULL immediately makes a second free harmless and a later dereference a predictable null-pointer fault instead of a silent write into reused memory.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *value = malloc(sizeof *value);
if (value == NULL) {
return 1;
}
*value = 3;
free(value);
value = NULL;
printf("%s\n", value == NULL ? "freed and cleared" : "still set");
free(value);
printf("second free is harmless\n");
return 0;
}
gcc -Wall -Wextra nullfree.c -o nullfree
./nullfree
freed and cleared
second free is harmless
The habit costs one line and removes a whole class of double frees. free(NULL) is defined by the standard to do nothing, which is what makes the second call safe.
The analyzer, use after free
The leak on an early return is invisible to the compiler's normal warnings, but GCC has a static analyzer that follows the paths. With -fanalyzer it reports the allocation, the branch and the return, and names the leak:
#include <stdio.h>
#include <stdlib.h>
static int process(int fail)
{
int *value = malloc(sizeof *value);
if (value == NULL) {
return 1;
}
*value = 7;
if (fail) {
return 2;
}
printf("value: %d\n", *value);
free(value);
return 0;
}
int main(void)
{
printf("status: %d\n", process(1));
return 0;
}
gcc -fanalyzer -Wall -Wextra early.c -o early 2>&1 | head -n 5
early.c: In function 'process':
early.c:13:16: warning: leak of 'value' [CWE-401] [-Wanalyzer-malloc-leak]
13 | return 2;
| ^
'process': events 1-7
After those lines the analyzer prints the seven events of its path from the allocation to the leaking return. The build itself continues and the program runs, which is why the analyzer is a review tool rather than a gate; reading its first warning is usually enough to find the missing free.
A pointer used after the free is caught by a normal warning. The program builds, and the read returns whatever the allocator left in the block:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *value = malloc(sizeof *value);
if (value == NULL) {
return 1;
}
*value = 5;
free(value);
printf("%d\n", *value);
return 0;
}
gcc -Wall -Wextra uaf.c -o uaf
uaf.c: In function 'main':
uaf.c:13:5: warning: pointer 'value' used after 'free' [-Wuse-after-free]
13 | printf("%d\n", *value);
| ^~~~~~~~~~~~~~~~~~~~~~
uaf.c:12:5: note: call to 'free' here
12 | free(value);
| ^~~~~~~~~~~
The program is not run here, because the printed value depends on what the allocator does with a freed block and may differ between runs. The two lines of the warning are the reliable information: the use and the free that made it invalid.
Exercises
- Add a counter to a program of your own that allocates in a loop and frees every other block, and check the number at the end.
- Rewrite the leaky process from the analyzer section with a cleanup label and run -fanalyzer again.
- Explain what happens when a function returns a pointer to a block and the caller overwrites it with a new allocation before freeing the first one.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a memory leak?
Answer
A block whose pointer was lost before
freeThe block is still reserved, but no pointer remains to release it, so it cannot be freed for the rest of the run.
-
What ownership rule prevents leaks and use after free?
Answer
The allocator frees or transfers ownership to the caller
A function either releases the block before returning or documents that the caller now owns it and must free it.
-
Why assign
NULLimmediately afterfree?Answer
So a second free is harmless
free(NULL)does nothing, so a second release is safe, while a dereference of the cleared pointer fails predictably. -
What does a
cleanuplabel with onefreegive a function?Answer
A single exit path that every branch reaches
All failure paths jump to the same label, so the release exists once and cannot be forgotten on one branch.
-
What does this program print?
#include <stdio.h> #include <stdlib.h> static int outstanding = 0; static void *track_malloc(size_t size) { void *block = malloc(size); if (block != NULL) outstanding++; return block; } static void track_free(void *block) { if (block != NULL) outstanding--; free(block); } static void leak_one(void) { int *value = track_malloc(sizeof *value); if (value != NULL) *value = 1; } static void clean_one(void) { int *value = track_malloc(sizeof *value); if (value != NULL) { *value = 2; track_free(value); } } int main(void) { leak_one(); leak_one(); clean_one(); printf("outstanding: %d\n", outstanding); return 0; }Answer
outstanding: 2
Three blocks are allocated and one is released, so two remain.
-
What does this program print?
#include <stdio.h> #include <stdlib.h> static int outstanding = 0; static int *make_number(int value) { int *number = malloc(sizeof *number); if (number != NULL) { outstanding++; *number = value; } return number; } static void release(int *number) { if (number != NULL) outstanding--; free(number); } int main(void) { int *number = make_number(42); if (number == NULL) return 1; printf("%d\n", *number); release(number); printf("outstanding: %d\n", outstanding); return 0; }Answer
42 / outstanding: 0
The allocation and the release are balanced, so the counter returns to zero.
-
What does this program print for the successful call?
#include <stdio.h> #include <stdlib.h> static int process(int fail) { int *value = malloc(sizeof *value); int status = 0; if (value == NULL) return 1; *value = 7; if (fail) { status = 2; goto cleanup; } printf("value: %d\n", *value); cleanup: free(value); return status; } int main(void) { printf("status: %d\n", process(1)); printf("status: %d\n", process(0)); return 0; }Answer
status: 2 / value: 7 / status: 0
The failing call prints status 2, while the successful one prints the value before the cleanup label returns 0.
-
What does this program print?
#include <stdio.h> #include <stdlib.h> int main(void) { int *value = malloc(sizeof *value); if (value == NULL) return 1; *value = 3; free(value); value = NULL; printf("%s\n", value == NULL ? "freed and cleared" : "still set"); free(value); printf("second free is harmless\n"); return 0; }Answer
freed and cleared / second free is harmless
The pointer is
NULL, so the second free is a no-op and the message prints. -
This program is built with
gcc -std=c17 -fanalyzer -Wall -Wextra early.c -o early. What does the analyzer report?#include <stdio.h> #include <stdlib.h> static int process(int fail) { int *value = malloc(sizeof *value); if (value == NULL) return 1; *value = 7; if (fail) return 2; free(value); return 0; } int main(void) { printf("%d\n", process(1)); return 0; }Answer
leak of 'value' [CWE-401] [-Wanalyzer-malloc-leak]
The analyzer follows the path from the allocation to the early return and reports the leaked block.
-
This program is built with
gcc -std=c17 -Wall -Wextra uaf.c -o uaf. What does GCC warn about?#include <stdio.h> #include <stdlib.h> int main(void) { int *value = malloc(sizeof *value); if (value == NULL) return 1; *value = 5; free(value); printf("%d\n", *value); return 0; }Answer
pointer 'value' used after 'free' [-Wuse-after-free]
The warning points at the read and at the free that invalidated the pointer.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev