C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
57: Common Memory Bugs: Leaks, Use-After-Free, Double-Free
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:
- Implement the
close_sessionfunction so it frees both the session name and the session struct without leaving a leak. - Fix the
clear_all_sessionsfunction to ensure it doesn't attempt to free the same memory twice or access memory after it has been freed. - Ensure that pointers are set to
NULLafter 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;
}
There are no comments for now.