Recommended resources
The freely available resources this lesson will direct you to.
Welcome back. In the previous lesson, you built a constructor that either hands its caller a complete object or cleans up every partial allocation. That ownership contract gives you a baseline for debugging: when a pointer is lost, used outside its allocation, or used after destruction, you can ask which part of the contract was broken.
This lesson is a hands-on investigation on your Linux VM. You’ll compile a small C program with warnings enabled, run three deliberately faulty paths under sanitizers, read each report, and repair the defects. Allow about 40–45 minutes. These are user-space experiments; the same habit of tracing allocation, access, and release is useful when reviewing storage and kernel code, even though kernel instrumentation is configured differently.
What the tools can—and cannot—tell you
Compiler warnings inspect code as it is built. They can catch some suspicious constructs without running the program, but an absence of warnings does not establish that memory handling is correct. A sanitizer adds checks to a build and observes the paths you actually execute. That makes a good workflow: enable warnings, run targeted cases under instrumentation, and interpret each finding against the ownership contract.
First, watch the leak and use-after-free demonstrations in Detect Errors the Easy Way (with AddressSanitizer) by Jacob Sorber. Focus less on the particular string operation and more on what changes when the program is rebuilt with instrumentation.
Detect Errors the Easy Way (with AddressSanitizer)
In Detect Errors the Easy Way (with AddressSanitizer), Jacob Sorber shows how an ordinary-looking run can conceal a memory leak or a later access to freed storage.
Watch the leak for the build flag and the leak report. Then watch use after free for how the report identifies the invalid access. In both cases, distinguish the line where memory was allocated from the line where the defect becomes visible.
The Clang documentation supplies the precise build and reporting details we’ll use. Read only the indicated passages; the platform-specific options elsewhere are not needed for this lab.
AddressSanitizer — Clang 22.0.0git documentation
Read the Clang AddressSanitizer documentation to see which errors the tool checks, how to build with it, and what information to expect in a report.
In Introduction, read the detection list. In Usage, read the build guidance, noting that the sanitizer flag is needed at the final link as well as compilation and that execution stops at the first detected error. In Symbolizing the Reports, read the symbolizer guidance. Finally, under Memory leak detection, read the Linux default.
AddressSanitizer (ASan) checks accesses against information about valid and invalid memory. Its reports are most useful when you read them as a history: where was the allocation made, where was it released if applicable, and which access failed? Leak detection asks a different question at process exit: which allocations have not been released and are no longer reachable?
Build three reproducible failures
Save this program as memfaults.c. Each command-line argument selects one defect so that ASan’s first-error exit does not hide the others. The malloc failure check keeps an allocation failure from being mistaken for the defect under investigation.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static unsigned char *new_bytes(void)
{
unsigned char *p = malloc(8);
if (p == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
return p;
}
static void leak_case(void)
{
unsigned char *p = new_bytes();
p[0] = 17;
printf("first byte: %u\n", (unsigned)p[0]);
/* Defect: this function returns without releasing p. */
}
static void overflow_case(void)
{
unsigned char *p = new_bytes();
for (size_t i = 0; i <= 8; ++i)
p[i] = (unsigned char)i;
free(p);
}
static void uaf_case(void)
{
unsigned char *p = new_bytes();
unsigned char *alias = p;
p[0] = 9;
free(p);
printf("first byte: %u\n", (unsigned)alias[0]);
}
int main(int argc, char **argv)
{
if (argc != 2) {
fprintf(stderr, "usage: %s leak|overflow|uaf\n", argv[0]);
return EXIT_FAILURE;
}
if (strcmp(argv[1], "leak") == 0)
leak_case();
else if (strcmp(argv[1], "overflow") == 0)
overflow_case();
else if (strcmp(argv[1], "uaf") == 0)
uaf_case();
else
return EXIT_FAILURE;
return EXIT_SUCCESS;
}
Start with a warning-enabled build. Use Clang if it is installed; GCC also supports the ASan build shown here.
clang -std=c11 -O0 -g -Wall -Wextra -Wpedantic \
-fno-omit-frame-pointer -fsanitize=address \
memfaults.c -o memfaults
If you use GCC, replace clang with gcc. The debug information and retained frame pointers help produce readable stack traces; -O0 keeps this small demonstration straightforward. You may get no compiler warnings. Do not treat that as a clean bill of health: for example, the loop bound is syntactically valid, but its last iteration writes past an eight-byte allocation.
If your VM has a GCC version supporting -fanalyzer, you can also try a separate static-analysis pass:
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -fanalyzer \
-c memfaults.c -o memfaults.o
Inspect any diagnostics it produces, but do not require it to find all three bugs. Warnings and static-analysis results are leads to verify; the runs below establish which faulty paths were exercised.
Read the evidence, one process at a time
Run each case separately. On Linux, leak detection is normally enabled with ASan; the explicit setting below makes your intention clear.
ASAN_OPTIONS=detect_leaks=1 ./memfaults leak
./memfaults overflow
./memfaults uaf
For leak, expect a LeakSanitizer report at process exit identifying an allocation of eight bytes. The key stack points to malloc through new_bytes and leak_case. The allocation itself was valid. The defect is that leak_case returned without free(p), leaving no owner able to release that buffer. This resembles an early-return cleanup error from the previous lesson, although here the missing release is on the ordinary path.
For overflow, look for an ASan heap-buffer-overflow and a WRITE of size 1. The allocation is eight bytes, so its valid indices are 0 through 7. On the iteration with i == 8, the assignment writes immediately beyond it. The report’s access stack identifies the failing store; its allocation stack explains the actual buffer size. free(p) is present, but execution does not reach it because ASan stops at the invalid write. That does not make the primary diagnosis a leak: repair the first reported memory error before interpreting consequences of an aborted run.
For uaf, expect heap-use-after-free and a READ of size 1. Read its three locations together: the access through alias, the earlier free(p), and the original allocation. alias and p hold the same address; releasing through one invalidates access through both. Setting p to null after free(p) would not repair the surviving alias.
The kernel-oriented image below illustrates the same lifetime idea using KASAN, the kernel address sanitizer. It is a schematic, not an instruction to use KASAN for this user-space binary.

If a report contains addresses but not useful source locations, confirm that you built with -g; with Clang, having llvm-symbolizer available in your PATH also helps. Tool output varies by compiler and environment, so prioritize the error category, access type and size, and allocation/free/access stacks over matching sample wording exactly. A sanitizer checks executed code paths, not every path the program could take.
Repair and verify
Make one small repair per case, then rebuild with the same warning and sanitizer flags and rerun all three commands.
- In
leak_case, addfree(p)after its last use. - In
overflow_case, change the loop condition toi < 8, so it visits only valid indices. - In
uaf_case, perform the read beforefree(p)if it is required, or remove the read and unneeded alias. No pointer to that allocation may be dereferenced after the release.
A successful verification is not merely “the program printed what I expected.” It is that each selected path finishes without a sanitizer report, and that the code’s ownership story still makes sense by inspection. For real error paths, repeat this check with inputs or failure injection that actually take those paths.
Key takeaways
Warnings are a low-cost first pass, not proof of memory safety. ASan identifies invalid accesses such as out-of-bounds writes and use-after-free; leak detection identifies allocations left unreleased at exit. When reading a report, separate the failing access from the allocation and, for a lifetime error, the release. Then make the smallest ownership or bounds correction and rerun the same test.
Next, you’ll bring this discipline into a timed linked-list problem, where changing links safely depends on preserving ownership and handling edge cases under interview constraints.