Skip to Content
Course content

57: Common Memory Bugs: Leaks, Use-After-Free, Double-Free

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you're comfortable with malloc and free, but here is the reality: in a professional C codebase, memory bugs are the primary source of crashes and security vulnerabilities. They aren't usually obvious. Your program might run perfectly for ten minutes and then suddenly explode, or worse, it might "work" while silently corrupting data in the background.

To see how this happens, let's build a simple user session manager. We'll create a User struct that holds a name and an ID. Since names vary in length, we'll allocate the name on the heap.

Tracking a User Session

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

typedef struct {
    char *name;
    int id;
} User;

User* create_user(const char *name, int id) {
    User *u = malloc(sizeof(User));
    u->name = malloc(strlen(name) + 1);
    strcpy(u->name, name);
    u->id = id;
    return u;
}

void process_users() {
    for (int i = 0; i < 1000; i++) {
        User *u = create_user("Alice", i);
        printf("Processing user %d\n", u->id);
        // I'm focusing on the logic here, so I'll skip the cleanup for a second.
    }
}

If you ran process_users, it would look fine. But I just wrote a classic memory leak. Every time the loop iterates, I allocate memory for the User struct and the name string, but I never call free. Because u is a local pointer, once the loop iteration ends, I lose the address to that memory. It's still occupied, but I can no longer reach it. In a long-running server, this is how you eat up all the RAM until the OS kills your process.

The Trap of the Dangling Pointer

Now, let's try to fix the leak. I'll add a destroy_user function. But while doing so, I'm going to demonstrate a Use-After-Free (UAF) bug. This happens when you free memory but keep the pointer around and try to use it later.

void destroy_user(User *u) {
    free(u->name);
    free(u);
}

void session_demo() {
    User *u = create_user("Bob", 42);
    destroy_user(u);
    
    // Oops. I'm accessing the memory after I told the OS I'm done with it.
    printf("User ID was: %d\n", u->id); 
}

The scary thing here is that printf might actually print "42". Why? Because free doesn't usually "wipe" the memory; it just tells the allocator that this block is available for others. If no other part of your program has claimed that memory yet, the old data is still there. This makes UAF bugs incredibly hard to debug because they are non-deterministic.

Cleaning Up the Wrong Way

Here is where I usually trip up when I'm tired. Let's look at how I'd write a cleanup function and a mistake I actually made in a project a few years ago. I'll try to be "thorough" and end up causing a Double-Free or a crash.

void bad_cleanup(User *u) {
    free(u);        // I free the container first...
    free(u->name); // ...then I try to free the member inside it.
}

I just made a huge mistake. By calling free(u) first, I've invalidated the pointer to the User struct. When I then try to access u->name to free it, I'm dereferencing a pointer to memory I no longer own. That's a Use-After-Free leading directly into a free() call on a garbage address. If I had called free(u->name) twice, that would be a Double-Free, which usually triggers an immediate SIGABRT (abort) because the heap manager detects the corruption.

The golden rule for nested allocations is: Free from the inside out.

void correct_cleanup(User *u) {
    if (u == NULL) return;
    
    free(u->name); // First, free the internal heap memory
    free(u);        // Finally, free the struct itself
}

One last pro tip: always set your pointer to NULL immediately after freeing it if the pointer remains in scope. free(NULL) is a safe no-op in C, but free(some_random_address) is a disaster. It turns a potential Use-After-Free into a predictable null-pointer crash, which is much easier to find in a debugger.




📋 Practical Task

Fixing the Leaky Session Manager

You are given a program that manages a list of active sessions. The program is currently leaking memory every time a session is closed and is prone to a crash during the final cleanup phase. Your task is to:

  1. Implement the close_session function so it frees both the session name and the session struct without leaving a leak.
  2. Fix the clear_all_sessions function to ensure it doesn't attempt to free the same memory twice or access memory after it has been freed.
  3. Ensure that pointers are set to NULL after being freed to prevent dangling pointer usage.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *session_id;
    int timeout;
} Session;

Session* open_session(const char *id) {
    Session *s = malloc(sizeof(Session));
    s->session_id = malloc(strlen(id) + 1);
    strcpy(s->session_id, id);
    s->timeout = 3600;
    return s;
}

void close_session(Session *s) {
    // TODO: Implement this to prevent memory leaks
}

void clear_all_sessions(Session **sessions, int count) {
    for (int i = 0; i < count; i++) {
        close_session(sessions[i]);
        // TODO: Prevent double-free if this function is called twice
    }
}

int main() {
    Session *active_sessions[2];
    active_sessions[0] = open_session("SESS_001");
    active_sessions[1] = open_session("SESS_002");

    clear_all_sessions(active_sessions, 2);
    
    return 0;
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.