How NULL Pointer Dereference from Unchecked malloc() Happens in C and How to Fix It
Vulnerability at a Glance
| Field | Detail |
|---|---|
| Vulnerability | NULL Pointer Dereference / Unchecked malloc() Return |
| CWE | CWE-476 |
| Language | C |
| Risk | Heap corruption or potential arbitrary code execution |
| Root Cause | malloc() return value not checked before use as memcpy() destination |
| Fix | NULL check with safe exit immediately after malloc() |
Direct Answer
This is a NULL pointer dereference vulnerability (CWE-476) in bench/tokenizer/tokenizer.c. When malloc() fails, it returns NULL. Without a check, the immediately following memcpy() writes to address zero — causing heap corruption or, in adversarial conditions, potential code execution. The fix adds if (!src) { fprintf(stderr, "malloc failed\n"); exit(1); } directly after the malloc() call, ensuring the program terminates safely instead of continuing with an invalid pointer.
Introduction
The bench/tokenizer/tokenizer.c file exists to benchmark tokenizer performance — it builds a large synthetic source string by repeating a base pattern N_REPEAT times. It's the kind of utility code that rarely gets security scrutiny because it lives in the bench/ directory, not in the hot path of production logic. But a subtle, dangerous flaw was hiding in plain sight in its make_source() function: a call to malloc() whose return value was never checked before being handed directly to memcpy().
This is the kind of bug that compiles cleanly, passes most tests, and only reveals itself under memory pressure — or under deliberate exploitation.
The Vulnerability Explained
Here is the vulnerable code in make_source(), starting at line 11:
// VULNERABLE — bench/tokenizer/tokenizer.c (before fix)
static char* make_source(int* len) {
int base_len = (int)strlen(BASE);
*len = base_len * N_REPEAT;
char* src = malloc((size_t)*len + 1);
// ⚠️ No NULL check here!
for (int i = 0; i < N_REPEAT; i++) memcpy(src + i * base_len, BASE, (size_t)base_len);
src[*len] = 0;
return src;
}
The problem is on line 13: malloc() is called to allocate (size_t)*len + 1 bytes, and the result is stored in src. Immediately on the next line, src is used as the destination in memcpy(src + i * base_len, BASE, ...) — without ever checking whether src is NULL.
What happens when malloc() fails?
malloc() returns NULL when the system cannot satisfy the allocation request. This can happen due to:
- System-wide memory exhaustion
- Address space limits (e.g., running inside a constrained container or sandbox)
- Heap fragmentation in long-running processes
- Deliberate resource exhaustion by an attacker (e.g., via other allocations in the same process)
When src is NULL and the code executes memcpy(src + i * base_len, BASE, base_len), it computes a destination address of NULL + offset — which on most platforms is a very low memory address, typically within the first page. Writing to this address causes undefined behavior in C, which in practice means:
- Segmentation fault / crash — the most common outcome on modern OSes with null page protection
- Heap metadata corruption — on systems or configurations where address zero is mapped
- Potential code execution — in environments (embedded, kernel space, certain OS configurations) where the null page is accessible or can be mapped by an attacker
Why this is more than "just a crash"
The PR description frames this correctly as an exploit primitive: a code pattern that, while not independently exploitable in every environment today, can be chained with other weaknesses. Automated exploit-development tools increasingly look for exactly these patterns — a controlled write to a predictable address — as building blocks for larger attack chains. Removing this primitive raises the bar against such tooling.
For a concrete scenario: imagine this tokenizer benchmark is invoked as part of a CI pipeline or test harness that processes user-supplied input files to determine benchmark size. An attacker who can influence the size parameter (making *len astronomically large) could trigger a malloc() failure, then observe the crash behavior or leverage the NULL write in a more permissive execution environment.
The Fix
The fix is a single, precisely targeted line added immediately after the malloc() call:
// FIXED — bench/tokenizer/tokenizer.c (after fix)
static char* make_source(int* len) {
int base_len = (int)strlen(BASE);
*len = base_len * N_REPEAT;
char* src = malloc((size_t)*len + 1);
if (!src) { fprintf(stderr, "malloc failed\n"); exit(1); } // ✅ Added
for (int i = 0; i < N_REPEAT; i++) memcpy(src + i * base_len, BASE, (size_t)base_len);
src[*len] = 0;
return src;
}
Before vs. After
char* src = malloc((size_t)*len + 1);
+ if (!src) { fprintf(stderr, "malloc failed\n"); exit(1); }
for (int i = 0; i < N_REPEAT; i++) memcpy(src + i * base_len, BASE, (size_t)base_len);
Why this fix works
The if (!src) check intercepts the NULL return from malloc() before src is ever used as a pointer destination. If allocation fails:
- A diagnostic message is written to
stderrso the failure is observable in logs exit(1)terminates the process cleanly with a non-zero exit code, signaling failure to any calling harness- The
memcpy()on the following line is never reached with an invalid pointer
This approach is appropriate for a benchmark utility where there is no meaningful recovery path from an allocation failure — the program cannot produce valid benchmark results without the source buffer. In library code or production services, you might prefer returning NULL and propagating the error upward rather than calling exit(), but for a benchmark driver, a clean abort with a clear error message is the right call.
The PR also notes that line 15 uses a similar pattern (src[*len] = 0 — a write through src) and should be reviewed for the same reason. The NULL check on line 13 covers both uses since both are unreachable if src is NULL after the fix.
Prevention & Best Practices
1. Always check malloc() return values in C
This is a foundational rule in C programming. Every call to malloc(), calloc(), or realloc() can return NULL, and every such call must be followed by a check:
// Pattern: always check, always handle
void* ptr = malloc(size);
if (ptr == NULL) {
// handle error: return error code, log, exit, etc.
}
2. Consider wrapper functions for allocation
In larger C codebases, a common pattern is a xmalloc() wrapper that handles the NULL check internally:
void* xmalloc(size_t size) {
void* ptr = malloc(size);
if (!ptr) {
fprintf(stderr, "fatal: out of memory (requested %zu bytes)\n", size);
exit(EXIT_FAILURE);
}
return ptr;
}
This ensures the check is never accidentally omitted and centralizes the error-handling policy.
3. Use static analysis tools
Several tools can catch this pattern automatically:
- Clang Static Analyzer: Detects use of
malloc()return without NULL check - cppcheck: Reports
nullPointerwarnings for unchecked allocation results - Coverity: Identifies "resource leak" and "null dereference" after failed allocations
- Semgrep: Can be configured with C-specific rules for unchecked
malloc()
4. Enable compiler warnings
Compile with -Wall -Wextra and consider -fanalyzer (GCC 10+) which performs interprocedural analysis and can flag this pattern.
5. Reference standards
- CERT C Coding Standard: Rule MEM32-C — Detect and handle memory allocation errors
- CWE-476: NULL Pointer Dereference
- CWE-252: Unchecked Return Value
- OWASP: Memory Management vulnerabilities in C/C++
Key Takeaways
malloc()inmake_source()at line 13 ofbench/tokenizer/tokenizer.cwas used without a NULL check — a pattern that is unsafe regardless of how unlikely allocation failure seems in testing environments.- Benchmark and utility code deserves the same security scrutiny as production code — the
bench/directory is not a safe harbor from memory safety rules. - A NULL pointer write via
memcpy()is not always "just a crash" — in constrained or adversarially controlled environments, it can be an exploitable primitive. - The fix is one line, but its absence represents a class of vulnerability (CWE-476) that has contributed to real-world exploits in C and C++ projects.
- The PR correctly identified line 15 as a related risk — once you find one unchecked use of an allocation result, audit all subsequent uses of that pointer in the same function.
How Orbis AppSec Detected This
- Source: The
malloc()call atbench/tokenizer/tokenizer.c:13allocating(size_t)*len + 1bytes — which can fail and returnNULLunder memory pressure or adversarial conditions. - Sink: The
memcpy(src + i * base_len, BASE, (size_t)base_len)call at line 14, which usessrcas a destination pointer without any intervening NULL check. - Missing control: No validation of the
malloc()return value between the allocation on line 13 and the first use ofsrcon line 14. - CWE: CWE-476 (NULL Pointer Dereference), related to CWE-252 (Unchecked Return Value).
- Fix: Added
if (!src) { fprintf(stderr, "malloc failed\n"); exit(1); }immediately after themalloc()call, ensuring the process exits cleanly before any write through a potentially NULL pointer.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
The unchecked malloc() in make_source() is a textbook example of how a single missing line in C can introduce a memory safety vulnerability with real exploitation potential. The fix is minimal and surgical — one if (!src) check — but its absence left the door open to heap corruption and undefined behavior. In C, malloc() failures are not hypothetical edge cases; they are part of the contract of the language, and every allocation must be treated as potentially fallible.
Security-conscious C development means treating every pointer as guilty until proven non-NULL. Static analysis tools, compiler warnings, and automated security scanning (like Orbis AppSec) make it practical to enforce this discipline at scale, catching these patterns before they reach production.