Step 61. malloc and free — Borrow from the Heap and Return It

Step 61. malloc and free — Borrow from the Heap and Return It

Level 1 — Programming and the Computer’s Inner Workings | Difficulty ★★★★☆ | Estimated time: 4 hours

Prerequisites: Steps 56–60 complete. You know the four memory regions and the heap’s existence. You can use bash and gcc on WSL Ubuntu.

  • What you need: a WSL Ubuntu terminal, gcc. The verification environment is Ubuntu 24.04, gcc 13.3.0, x86-64.
  • Caution: today’s exercises are 100% safe. Even the incidents we trigger on purpose (leaks, dangling, double free) all happen only inside programs you wrote yourself, and when the program exits, the operating system reclaims all the memory.

int arr[100]; has its size fixed at creation. But real programs often "don’t know how many they’ll need until they run." As many as the user enters, as many as the file contains. So C has a way to borrow memory from the heap mid-run by asking "please lend me this much." Borrowed things must be returned, and C does not return them for you automatically. This freedom is C’s power, and this chore is the hometown of countless famous vulnerabilities. Today we go through the whole cycle — borrow, use, return — plus two flagship incidents, all by hand.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Borrow as much memory as you need mid-run with malloc, used together with sizeof
  • Return used memory with free, honoring the NULL initialization convention
  • Explain memory leaks and dangling pointers, and reproduce them experimentally
  • Complete a program that creates, uses, and returns a dynamic array sized to the input
  • Confirm by measurement that gcc warns you about use-after-free

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment C language, WSL Ubuntu bash, gcc 13.x (verified: 13.3.0, x86-64)
Today’s functions/commands malloc (borrow), free (return), calloc (borrow filled with zeros), gcc -Wall (compile with warnings on), /usr/bin/time -v (measure memory usage)
Concepts needed The heap, dynamic allocation, NULL checks, memory leaks, dangling pointers, use-after-free, double free

2-1. malloc — Borrowing Land from the Heap

malloc (memory allocation) is the function "lend me this much from the heap." malloc(40) borrows 40 bytes and returns the land’s starting address. Borrowed land is yours until you return it.

The decisive difference from arrays is that "you can decide how many slots you need mid-run." That’s why this approach is called dynamic allocation — allocation that moves at run time.

2-2. Teamwork with sizeof

malloc only accepts bytes. To borrow "ten int slots," you must convert to bytes, and sizeof(int) tells you one slot’s size (4 bytes; can vary by environment). So the convention is malloc(sizeof(int) * 10). Memorize this form as a whole. Writing the number directly (malloc(40)) makes code that breaks when the environment changes.

2-3. free — Returning

free(borrowed address) is the return: "I’m done with this land." If you don’t return it, that land stays tied up as "in use" for as long as the program lives. One important fact — even after you return, the pointer variable itself keeps remembering that address. This is the seed of today’s second incident.

2-4. Two Flagship Incidents

Memory leak: borrowing and forgetting to return. Once is small, but keep borrowing inside a loop and the program grows fat and dies. The longer a server program stays up, the more fatal it is.

Dangling pointer and use-after-free: the state where a pointer keeps remembering returned land’s address is a dangling ("hanging") pointer, and going back to that address to read or write is use-after-free. Since returned land may be reused for another purpose at any time, it’s like keeping a key to someone else’s house and walking in and out. A regular cause on real vulnerability (CVE) lists.


3. Follow Along

3-1. Borrow, Use, Return — The Basic Cycle

Input (dyn1.c)

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

int main(void) {
    int *p = malloc(sizeof(int) * 5);
    if (p == NULL) {
        printf("Could not borrow memory.\n");
        return 1;
    }

    for (int i = 0; i < 5; i++) {
        p[i] = (i + 1) * 10;
    }
    for (int i = 0; i < 5; i++) {
        printf("p[%d] = %d (address %p)\n", i, p[i], (void *)&p[i]);
    }

    free(p);
    printf("Return complete.\n");
    return 0;
}

Compile and run

gcc -Wall dyn1.c -o dyn1
./dyn1
p[0] = 10 (address 0x57f7e4a8c2a0)
p[1] = 20 (address 0x57f7e4a8c2a4)
p[2] = 30 (address 0x57f7e4a8c2a8)
p[3] = 40 (address 0x57f7e4a8c2ac)
p[4] = 50 (address 0x57f7e4a8c2b0)
Return complete.

(Verified 2026-09-09. Address values differ on every run.)

How to read the output: the address p that malloc returned is used like an array with p[i] — Step 59’s truth, that an address can be accessed by index, exactly as it was. The addresses being stuck together 4 apart is also the same as arrays. On the heap, too, slots are adjacent. The NULL check is "courtesy for borrow failure (out of memory)," and it’s mandatory in real work. The final free(p) is the return.

Predict: what happens if you change malloc’s 5 to 3 and then write through p[4]? Will a warning appear, will it run? Experiment on purpose. (Hint: C doesn’t check.)

Why: the three-beat rhythm "borrow (NULL check) → use → return" is the lifelong rhythm of heap usage.

3-2. Deciding the Size Mid-Run — What Arrays Can’t Do

Input (dyn2.c)

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

int main(void) {
    int n;
    printf("How many people's scores will you enter? ");
    scanf("%d", &n);

    int *scores = malloc(sizeof(int) * n);
    if (scores == NULL) {
        return 1;
    }

    for (int i = 0; i < n; i++) {
        printf("Score %d: ", i + 1);
        scanf("%d", &scores[i]);
    }

    int total = 0;
    for (int i = 0; i < n; i++) {
        total += scores[i];
    }
    printf("Total %d, average %d\n", total, total / n);

    free(scores);
    return 0;
}

Compile and run

gcc -Wall dyn2.c -o dyn2
./dyn2
How many people's scores will you enter? 3
Score 1: 90
Score 2: 80
Score 3: 70
Total 240, average 80

(Verified 2026-09-09. This is the screen when typing by hand. Note that piping input in — printf "3\n90\n80\n70\n" | ./dyn2 — makes the prompts appear crammed on one line, but the result is the same.)

How to read the output: before running, you don’t know n. A program you can’t make with an array works with malloc. You borrowed exactly the size decided mid-run — this is why it’s called "dynamic."

3-3. Leaks — When You Forget to Return

Input (leak.c)

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

int main(void) {
    for (int i = 0; i < 1000000; i++) {
        int *p = malloc(sizeof(int) * 1000);
        /* free(p); deliberately omitted! */
        if (p == NULL) {
            printf("Borrow failed at iteration %d\n", i);
            return 1;
        }
    }
    printf("Succeeded to the end (exiting in a leaked state)\n");
    return 0;
}

Compile and run

gcc -Wall leak.c -o leak
/usr/bin/time -v ./leak
Succeeded to the end (exiting in a leaked state)
        ...
        Maximum resident set size (kbytes): 3923236

(Verified 2026-09-09. On a WSL environment with 7.6 GB of memory it ran to the end in 2.6 seconds, and the maximum memory this program actually consumed was about 3.9 GB.)

How to read the output: we borrowed 4,000 bytes each iteration and never returned. A million iterations is about 4 GB. /usr/bin/time -v‘s "Maximum resident set size" is the evidence. On environments with less memory, "Borrow failed at iteration N" appears partway. And the moment it exited, the operating system reclaimed all the land, so your computer is fine — leaks are scary in "programs that never end," that is, on servers.

3-4. Dangling Pointers — Going Back After Returning

Input (dangle.c)

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

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 42;
    printf("Before return: %d (address %p)\n", *p, (void *)p);

    free(p);
    printf("Even after return, p remembers the same address: %p\n", (void *)p);
    printf("Read attempt after return: %d\n", *p);

    p = NULL;
    printf("NULL initialization complete. Now a mistake kills it instantly — safer that way.\n");
    return 0;
}

Compile and run

gcc -Wall dangle.c -o dangle
./dangle
dangle.c:11:5: warning: pointer 'p' used after 'free' [-Wuse-after-free]
   11 |     printf("Read attempt after return: %d\n", *p);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
dangle.c:9:5: note: call to 'free' here
    9 |     free(p);
      |     ^~~~~~~
Before return: 42 (address 0x628bb3a5a2a0)
Even after return, p remembers the same address: 0x628bb3a5a2a0
Read attempt after return: 683358810
NULL initialization complete. Now a mistake kills it instantly — safer that way.

(Verified 2026-09-09. The address and the last value differ on every run.)

How to read the output: please look at three things.

  1. The compiler warns: recent gcc tells you with the -Wuse-after-free warning, "you’re using a pointer again after free." Not ignoring warnings is the first line of defense.
  2. Even after return, p remembers the same address: free returns the land; it doesn’t empty the pointer variable.
  3. The read attempt produced 683358810, not 42: the heap manager already reused the returned land for its internal bookkeeping. The old owner’s value is gone. (Some environments show "42 still there" — that’s not safety, it’s a reprieve.)

After returning, making it an "empty hand" with p = NULL is the cleanup convention. Using a NULL pointer dies instantly with a segfault — which is actually safer because the bug gets exposed rather than hidden.

3-5. Return Only Once — double free

Input (dfree.c)

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

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 7;
    free(p);
    free(p);  /* return twice? */
    printf("If you see this line, you're lucky\n");
    return 0;
}

Compile and run

gcc -Wall dfree.c -o dfree
./dfree
free(): double free detected in tcache 2
Aborted (core dumped)

(Verified 2026-09-09. A -Wuse-after-free warning also appeared at compile time, and the run aborted at the second free. In a Korean-locale environment the shell message may appear translated, e.g. "중단됨 (core dumped)".)

How to read the output: returning the same land twice, the heap manager said "this land is already returned?" and aborted the program. tcache is the heap manager’s cache warehouse where returned land is kept briefly. Detection is the lucky case — double free is also a regular on real vulnerability lists.

3-6. calloc — Borrowing Filled with Zeros

One more of malloc’s cousins. calloc borrows while filling every slot with 0.

Input (zero.c)

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

int main(void) {
    int *a = malloc(sizeof(int) * 3);
    int *b = calloc(3, sizeof(int));
    printf("Right after malloc: %d %d %d (may be garbage)\n", a[0], a[1], a[2]);
    printf("Right after calloc: %d %d %d (always 0)\n", b[0], b[1], b[2]);
    free(a);
    free(b);
    return 0;
}

Compile and run

gcc -Wall zero.c -o zero
./zero
zero.c:7:5: warning: '*a' is used uninitialized [-Wuninitialized]
Right after malloc: 0 0 0 (may be garbage)
Right after calloc: 0 0 0 (always 0)

(Verified 2026-09-09. In this run, the malloc side happened to be 0 too. The compiler told us with the -Wuninitialized warning, "you’re reading an uninitialized value.")

How to read the output: malloc lends "just the land." Traces of the previous owner (garbage values) may remain — this time it happened to be 0, but there’s no guarantee. calloc takes the slot count and slot size separately (calloc(3, sizeof(int))) and hands it over wiped clean. A convenient, safe choice when the initial values must be 0.

Think about it: what does "traces of the previous owner remain" mean for security? If you use sensitive data (passwords, keys) and return the land without erasing it, the next code to borrow that land can read the traces. This is one route of information leakage.


4. Missions & Exercises

Mission — A Dynamic Grade Management Program

Extend dyn2.c into a program grade.c that satisfies every requirement below.

  1. Borrow a score array with malloc for as many people as are entered mid-run
  2. After entering scores, compute the highest and lowest scores in addition to the total and average
  3. Print the number (which-th entry) of the person with the highest score
  4. Before free, print and record "the borrowed land’s starting address and byte size"
  5. After free, honor the convention of initializing the pointer to NULL

Exercises

Problem 1. Can you make ten int slots with malloc(10)? If not, write the correct code and explain why in one sentence.

Problem 2. Why is the NULL check right after malloc needed, and what happens if you write to p[i] without checking?

Problem 3. Even after free, the pointer remembered the same address (3-4 verification). Why is it dangerous to keep reading in this state? And how does p = NULL prevent this incident?

Problem 4. In the 3-3 verification, the leaking program used about 3.9 GB yet the computer was fine — why? And explain, along with that, why leaks are nonetheless fatal in server programs.


5. Model Answers & Completion Criteria

Mission Model Answer

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

int main(void) {
    int n;
    printf("How many people's scores will you enter? ");
    scanf("%d", &n);

    int *scores = malloc(sizeof(int) * n);
    if (scores == NULL) {
        return 1;
    }

    for (int i = 0; i < n; i++) {
        printf("Score %d: ", i + 1);
        scanf("%d", &scores[i]);
    }

    int total = 0, max_i = 0, min_i = 0;
    for (int i = 0; i < n; i++) {
        total += scores[i];
        if (scores[i] > scores[max_i]) max_i = i;
        if (scores[i] < scores[min_i]) min_i = i;
    }
    printf("Total %d, average %d\n", total, total / n);
    printf("Highest %d (entry %d), lowest %d (entry %d)\n",
           scores[max_i], max_i + 1, scores[min_i], min_i + 1);
    printf("Borrowed land: start %p, size %zu bytes\n",
           (void *)scores, sizeof(int) * (size_t)n);

    free(scores);
    scores = NULL;
    return 0;
}

How to verify: ① gcc -Wall grade.c -o grade must produce no warnings at all. ② Entering 3 people with scores 90/80/70 must print "Highest 90 (entry 1), lowest 70 (entry 3)." ③ The address must print near the end, and free plus NULL initialization must be in the code after it.

Exercise Answers

Problem 1 answer. No. malloc’s number is in bytes, so malloc(10) is 10 bytes — two and a half int slots. The correct code is malloc(sizeof(int) * 10). Since a data type’s actual size can vary by environment, you should always compute with sizeof.

Problem 2 answer. malloc fails when memory runs short and returns NULL. NULL is a special value meaning "points at no land," and writing to it with p[i] is like writing to nonexistent land — it dies instantly with a segmentation fault. In real work, a malloc without a NULL check is treated as a bug.

Problem 3 answer. Returned land may be reused at any time by the heap manager or other code. In testing, the read value had changed from 42 to 683358810 — even if you luckily see the old value, it’s only a reprieve. If you initialize with p = NULL, the moment you mistakenly use it again it dies instantly with a segfault, exposing the bug instead of hiding it. Dying immediately is safer than quietly corrupting.

Problem 4 answer. Because when the program exits, the operating system reclaims all of that process’s memory. So the leak of a "run it and it ends" program vanishes with its exit. A server program, by contrast, never exits and stays alive, so the leak accumulates over time until it dies of memory exhaustion or slows the whole system down.

Completion Criteria Checklist

  • [ ] I can borrow memory with a size decided mid-run, in the form malloc(sizeof(type) * count)
  • [ ] I never omit the NULL check right after malloc
  • [ ] I can honor the convention of returning with free and initializing to NULL in my code
  • [ ] I can explain the difference between a memory leak and a dangling pointer (use-after-free) in one sentence each
  • [ ] I can explain what double free is, along with the verified error message
  • [ ] I can explain the difference between calloc and malloc (whether it initializes)
  • [ ] Mission: I completed the dynamic grade management program and compiled it without warnings

6. Common Pitfalls & Fixes

Wall 1. Omitting the NULL Check

Symptom: in a low-memory situation, the program dies for no apparent reason.
Cause: malloc can fail, and on failure it returns NULL. Writing to NULL with p[i] is an instant incident.
Fix: always attach an if (p == NULL) check right after borrowing. Every example today is written that way.

Wall 2. Confusing Bytes and Slots

Symptom: you think malloc(10) made ten int slots.
Cause: malloc’s number is bytes. Ten int slots are 40 bytes (can vary by environment).
Fix: always use the form malloc(sizeof(type) * count). Writing numbers directly is code that breaks when the environment changes.

Wall 3. Using It Again After Returning

Symptom: the value still shows after free, so you keep using it — until one day a strange value appears.
Cause: returned land may be reused at any time. The compiler warns you as in the 3-4 verification:

warning: pointer 'p' used after 'free' [-Wuse-after-free]

Fix: compile with -Wall and never ignore this warning. And p = NULL right after free. This one line prevents most dangling incidents.

Wall 4. Returning from a Different Spot Than You Borrowed

Symptom: you receive malloc’s address in p, move it with p++, and then call free(p).
Cause: free demands "that exact address you received when borrowing." Move the address and it can’t find the return spot, and it dies with an error.
Fix: keep the borrowed starting address in a separate variable, or don’t move the pointer — use indexing (p[i]) instead.

Wall 5. Reading an Uninitialized Value

Symptom: this warning appears at compile time (verified 2026-09-09):

warning: '*a' is used uninitialized [-Wuninitialized]

Cause: malloc lends only the land; the contents are exactly the previous owner’s traces.
Fix: if the purpose requires starting from 0, use calloc, or fill in values right after borrowing and only then read.


7. Summary

Today’s Concepts

Concept One-line explanation
Heap The memory region you borrow from mid-run
Dynamic allocation The approach of borrowing with a size decided mid-run
Memory leak The incident of borrowing and not returning, so memory leaks away
Dangling pointer A pointer that keeps pointing at returned land
use-after-free The incident of reading or writing returned land again
double free The incident of returning the same land twice

Today’s Syntax and Commands

Code/command What it does
malloc(sizeof(int) * n) Borrow n int slots’ worth from the heap
if (p == NULL) Borrow-failure check (mandatory)
free(p) Return (with that exact address you received when borrowing)
p = NULL The convention of making an empty hand after returning
calloc(n, sizeof(int)) Borrow filled with zeros
gcc -Wall Compile with warnings on (all of today’s warnings came from here)
/usr/bin/time -v ./program Measure maximum memory usage

A Sense More Important Than Commands

Getting the rhythm "borrow (NULL check) → use → return (NULL initialization)" into your body is today’s core. And the incidents we triggered on purpose today — leaks, dangling, double free — are faces you’ll recognize with an "ah, that incident" every time you study famous vulnerabilities later.

One more thing to remember. When we made lists in Python, we never once returned anything. Python collected "what no one uses anymore" on its own (garbage collection). A language’s convenience is labor someone does on your behalf, and a person who knows that labor and one who doesn’t take different attitudes in front of an error message. If, from today, the question "this memory — who returns it, and when?" springs up automatically when you read code, today is a success.


Once every box is checked, Step 61 is complete. Click the checkbox in the sidebar to save your progress.