Back to Blog
critical SEVERITY8 min read

How NULL pointer dereference from unchecked malloc() happens in C and how to fix it

A critical memory safety vulnerability was discovered in `bench/tokenizer/tokenizer.c` where `malloc()` was called without checking its return value before passing the pointer to `memcpy()`. If allocation fails and `malloc()` returns NULL, the subsequent `memcpy()` writes to address zero, causing heap corruption or potential arbitrary code execution. The fix adds a single NULL check immediately after allocation, exiting cleanly on failure rather than proceeding with a dangerously invalid pointer

O
By Orbis AppSec
Published August 28, 2026Reviewed August 28, 2026

Answer Summary

This is a NULL pointer dereference vulnerability (CWE-476) in C, found in `bench/tokenizer/tokenizer.c` at line 13. When `malloc()` fails to allocate memory it returns NULL, and 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 rather than continuing with an invalid pointer.

Vulnerability at a Glance

cweCWE-476
fixAdded NULL check with safe exit immediately after malloc() call on line 13
riskHeap corruption or potential arbitrary code execution if malloc() returns NULL before memcpy()
languageC
root causemalloc() return value not checked before use as memcpy() destination
vulnerabilityNULL Pointer Dereference / Unchecked malloc() Return

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:

  1. Segmentation fault / crash — the most common outcome on modern OSes with null page protection
  2. Heap metadata corruption — on systems or configurations where address zero is mapped
  3. 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:

  1. A diagnostic message is written to stderr so the failure is observable in logs
  2. exit(1) terminates the process cleanly with a non-zero exit code, signaling failure to any calling harness
  3. 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 nullPointer warnings 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() in make_source() at line 13 of bench/tokenizer/tokenizer.c was 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 at bench/tokenizer/tokenizer.c:13 allocating (size_t)*len + 1 bytes — which can fail and return NULL under memory pressure or adversarial conditions.
  • Sink: The memcpy(src + i * base_len, BASE, (size_t)base_len) call at line 14, which uses src as 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 of src on 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 the malloc() 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.


References

Frequently Asked Questions

What is a NULL pointer dereference vulnerability?

A NULL pointer dereference occurs when a program uses a pointer that has not been validated and may be NULL (address zero). In C, writing to a NULL pointer via memcpy() or similar functions causes undefined behavior, typically a crash, heap corruption, or in some environments, exploitable memory corruption.

How do you prevent unchecked malloc() in C?

Always check the return value of malloc() immediately after the call. If it returns NULL, handle the error gracefully — either by returning an error code, logging and exiting, or retrying the allocation. Never pass an unchecked malloc() result to memcpy(), strcpy(), or any memory-writing function.

What CWE is NULL pointer dereference?

NULL pointer dereference is classified as CWE-476 (NULL Pointer Dereference). Related weaknesses include CWE-252 (Unchecked Return Value) and CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer).

Is a crash enough reason to fix an unchecked malloc()?

Yes — and it's often more than just a crash. While the most common outcome is a segmentation fault, in certain memory layouts and environments a NULL dereference can be leveraged for privilege escalation or code execution. Beyond exploitability, crashes in benchmarking or production tooling can mask deeper issues and degrade reliability.

Can static analysis detect unchecked malloc() return values?

Yes. Tools like Coverity, cppcheck, Clang's static analyzer, and Semgrep rules for C can all detect patterns where malloc()'s return value is used without a NULL check. Orbis AppSec's multi_agent_ai scanner flagged exactly this pattern in tokenizer.c at line 13.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #110

Related Articles

critical

How Buffer Overflow via strcpy() Happens in C++ XML Parsers and How to Fix It

A critical buffer overflow vulnerability was discovered in `buildroot-external/package/libxmlparser/xmlParser.cpp`, where the `toXMLString` function used `_tcscpy()` to write XML escape sequences into a destination buffer without any bounds checking. An attacker supplying a crafted XML document could overflow the buffer and potentially execute arbitrary code. The fix replaces all five unsafe `_tcscpy()` calls with `memcpy()` calls that copy only the exact number of bytes required for each escape

high

How insecure string copy functions happen in C and how to fix it

A high-severity buffer overflow vulnerability was discovered in `tools/claude-vscode-wrapper.c`, where an unbounded `strcpy()` call copied a file path into a fixed-size `MAX_PATH` buffer without any size validation. The fix replaces `strcpy()` with `snprintf()` and swaps `malloc()` for `calloc()`, ensuring both string operations and memory allocation are bounds-safe and zero-initialized.

critical

How Heap Buffer Overflows Happen in C++ ZIP Extraction and How to Fix Them

A critical heap buffer overflow vulnerability was discovered in `TKLiveSync/unzip.cpp`, where ZIP archive entry names were copied into a `PATH_MAX`-sized heap buffer using `strcpy()` without any length validation. Since the ZIP specification allows entry names up to 65,535 bytes — far exceeding typical `PATH_MAX` values of 1,024 to 4,096 bytes — a crafted archive could overflow the buffer and corrupt heap memory. The fix replaces the unsafe `strcpy`/`dirname` pattern with `std::string` operation

medium

How Integer Overflow happens in C++ image processing and how to fix it

A signed integer overflow in OpenCV's `bilateralFilter.cpp` allowed the buffer size calculation `cal_width * cal_height * cn` to wrap around to a small or negative value, causing `padding.resize()` to allocate far less memory than needed. Subsequent `memcpy` operations would then write beyond the allocated buffer, creating a heap corruption primitive. The fix is a single targeted cast to `size_t` that promotes the multiplication to unsigned 64-bit arithmetic before any overflow can occur.

high

How Remote Memory Exhaustion happens in Rust QUIC libraries and how to fix it

A high-severity vulnerability in `quinn-proto` 0.11.14 allowed remote attackers to exhaust server memory by sending carefully crafted out-of-order QUIC stream data, with no authentication required. The fix — upgrading to `quinn-proto` 0.11.15 — introduces bounds on the stream reassembly buffer, preventing unbounded memory growth. Applications built with Tauri or any Rust project depending on Quinn should apply this patch immediately.

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package, where unescaped line terminators could allow attackers to execute arbitrary code. The fix upgrades shell-quote from version 1.8.2 to 1.9.0 using npm overrides to ensure the patched version is used throughout the dependency tree, closing this dangerous attack vector.