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.


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.


Prevention and further reading

Frequently Asked Questions

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.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #110

Related Articles

critical

How buffer overflow happens in C++ and how to fix it

A critical buffer overflow in `create_hex_string()` within `hmlangw.cpp` let an unconditional 16-iteration loop write past the bounds of a 100-byte `hex` buffer using unchecked `sprintf` calls. The fix replaces `sprintf` with `snprintf` and caps the loop iterations based on the actual destination buffer size, closing off a memory corruption path reachable from serial or network input.

high

How remote memory exhaustion happens in Rust QUIC (Quinn) and how to fix it

A high-severity vulnerability (GHSA-4w2j-m93h-cj5j) in `quinn-proto`, the QUIC protocol implementation underlying the Quinn library, allowed remote attackers to exhaust server memory by sending unbounded out-of-order stream data. The `crosshash` project's `Cargo.lock` pinned the vulnerable `quinn-proto` 0.11.14; upgrading to 0.11.15 closes the gap by bounding how much out-of-order stream data the reassembly buffer will retain.

high

How Denial-of-Service via Unbounded Brace Expansion Happens in Node.js and How to Fix It

A critical denial-of-service vulnerability in the `brace-expansion` package allowed attackers to exhaust process memory through unbounded intermediate array expansion. The fix upgrades the package to patched versions (1.1.18, 2.1.4, 3.0.6, 5.0.9) that implement proper expansion length limits, preventing out-of-memory crashes in production applications.

high

How Inherited libvips Vulnerabilities in sharp Impact Image Processing and How to Fix Them

A critical vulnerability (GHSA-f88m-g3jw-g9cj) was discovered where the sharp image processing library inherited four dangerous libvips vulnerabilities that could be exploited through maliciously crafted images. The fix involved upgrading sharp from version 0.34.5 to 0.35.0, which includes hardened input handling and updated libvips bindings to prevent exploitation of these inherited weaknesses.

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

critical

LDAP Filter Injection in da_unique_email_validator Fixed

The registration-time email uniqueness validator, `da_unique_email_validator`, formatted the submitted email address straight into an LDAP search filter with Python's `%` operator, so filter metacharacters in the email were interpreted as filter syntax. The fix wraps the value in `ldap.filter.escape_filter_chars()` (and imports the `ldap.filter` submodule explicitly), so a submitted address is always treated as a literal attribute value. Any deployment with `ldap login` enabled and a bind accoun