Back to Blog
critical SEVERITY7 min read

How heap buffer overflow happens in C dupstr() and how to fix it

A critical heap buffer overflow vulnerability was discovered in the `dupstr()` function inside `tools/strliteral.c`, where `strcpy()` was called on a heap-allocated buffer without first verifying that `malloc()` had succeeded. If `malloc()` returned `NULL`, the subsequent `strcpy()` would write into address zero — corrupting memory and potentially enabling arbitrary code execution. The fix replaces the unsafe `strcpy()` call with a `NULL` check followed by a bounds-safe `memcpy()`, closing the v

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published July 5, 2026•Reviewed July 5, 2026

Answer Summary

This is a heap buffer overflow vulnerability (CWE-122) in the C function `dupstr()` located in `tools/strliteral.c`. The root cause is that `strcpy(dup, str)` was called immediately after `malloc()` without checking whether `malloc()` returned `NULL`, meaning a failed allocation could cause a write to address zero or heap metadata corruption. The fix adds a `NULL` guard after `malloc()` and replaces `strcpy()` with `memcpy(dup, str, len + 1)`, which copies exactly the right number of bytes and cannot overrun the allocation. This is a standard C memory-safety pattern recommended by CERT C and OWASP.

Vulnerability at a Glance

cweCWE-122
fixAdded NULL guard after malloc() and replaced strcpy() with memcpy()
riskHeap corruption leading to potential arbitrary code execution
languageC
root causestrcpy() called on malloc() return value without NULL check
vulnerabilityHeap Buffer Overflow via unchecked strcpy() after malloc()

How heap buffer overflow happens in C dupstr() and how to fix it

Introduction

The tools/strliteral.c file is a utility used during the build process to convert raw string literals into C identifiers and escaped representations. It's the kind of file that rarely gets a second look — small, unglamorous, and seemingly low-risk. But buried inside its dupstr() function at line 19 was a critical heap buffer overflow: strcpy() was called on the return value of malloc() without ever checking whether the allocation had actually succeeded.

This is a deceptively simple mistake that has caused real-world exploits for decades. The C standard is explicit: if malloc() fails, it returns NULL. Writing through a NULL pointer is undefined behavior — and in practice, on modern systems, it can corrupt heap metadata, crash the process, or in adversarial conditions, enable an attacker to redirect control flow.


The Vulnerability Explained

Here is the vulnerable version of dupstr():

static char *dupstr(char *str) {
  size_t len = strlen(str);
  char *dup = malloc(len + 1);
  strcpy(dup, str);   // ← line 22: no NULL check before this write
  dup[len] = '\0';
  return dup;
}

The logic looks reasonable at first glance: calculate the string length, allocate exactly enough space, copy the string in, null-terminate it, return the pointer. The problem is in the ordering and the absence of a guard.

What happens when malloc() fails?

malloc() returns NULL when the system cannot satisfy the allocation request — under memory pressure, in resource-constrained environments, or when a fuzzer or attacker deliberately exhausts the heap. The code does not check for this. It passes NULL directly to strcpy():

strcpy(NULL, str);  // undefined behavior — writes to address 0x0

On most modern operating systems, address 0x0 is not mapped, so this immediately segfaults. But heap allocators are complex. Depending on the allocator implementation (glibc, jemalloc, tcmalloc), a NULL or near-NULL write can silently corrupt allocator bookkeeping structures — free-list pointers, chunk size fields, or canary values — setting up a classic heap exploitation primitive.

The redundant null-termination compounds the issue

Notice that after strcpy(dup, str), there is a second line:

dup[len] = '\0';

strcpy() already writes the null terminator. This line is redundant — and it also dereferences dup a second time without a NULL check, doubling the exposure.

Exploitation scenario

An attacker who controls the input to strliteral.c (via command-line arguments or a crafted input file processed during the build) could provide a string designed to trigger a malloc() failure — for example, by supplying an extremely long string that exhausts available heap memory before dupstr() is called. The resulting strcpy(NULL, ...) corrupts heap metadata. In a more sophisticated scenario targeting a persistent process, this could be chained into arbitrary code execution by overwriting a function pointer stored in a heap-adjacent allocation.


The Fix

The fix makes two targeted changes to dupstr():

Before:

static char *dupstr(char *str) {
  size_t len = strlen(str);
  char *dup = malloc(len + 1);
  strcpy(dup, str);
  dup[len] = '\0';
  return dup;
}

After:

static char *dupstr(char *str) {
  size_t len = strlen(str);
  char *dup = malloc(len + 1);
  if (dup == NULL) {
    return NULL;
  }
  memcpy(dup, str, len + 1);
  return dup;
}

Change 1: NULL guard after malloc()

if (dup == NULL) {
  return NULL;
}

This is the most critical change. Before touching dup for any write operation, the code now verifies that malloc() succeeded. If it did not, the function returns NULL immediately, propagating the allocation failure to the caller rather than writing through a null pointer. This is the standard CERT C pattern (MEM32-C: Detect and handle memory allocation errors).

Change 2: strcpy() replaced with memcpy()

memcpy(dup, str, len + 1);

strcpy() is replaced with memcpy(dup, str, len + 1). Since len was already computed as strlen(str), copying len + 1 bytes copies the string content plus its null terminator in a single, length-bounded operation. memcpy() does not scan for a null terminator and does not write beyond the specified count — it is strictly bounded by the third argument. The redundant dup[len] = '\0' line is also removed, since memcpy() already copies the terminator as part of the len + 1 byte range.

This change eliminates both the unbounded copy risk and the redundant write, making the function's memory behavior fully explicit and auditable.


Key Takeaways

  • strcpy() after malloc() without a NULL check is always a bug — even in small utility functions like dupstr() that appear low-risk.
  • The redundant dup[len] = '\0' after strcpy() was a code smell that signaled the author was uncertain about the copy behavior, which should prompt a closer review of the surrounding logic.
  • memcpy() with a pre-computed length is the correct replacement for strcpy() when the string length is already known — it is bounded, explicit, and does not re-scan for the null terminator.
  • Build-time tools are not exempt from security review — tools/strliteral.c is a production file processed during the build pipeline, and a compromised build tool can be as dangerous as a compromised runtime binary.
  • A single missing NULL check can turn a correct-looking allocation into a heap corruption primitive — the fix is two lines, but the impact of the original bug was critical severity.

How Orbis AppSec Detected This

  • Source: User-controlled string input passed to dupstr() via command-line arguments or input files processed by tools/strliteral.c
  • Sink: strcpy(dup, str) at tools/strliteral.c:22, called on the unchecked return value of malloc(len + 1)
  • Missing control: No NULL check on the malloc() return value before the strcpy() write; no bounds argument passed to the copy function
  • CWE: CWE-122 — Heap-based Buffer Overflow
  • Fix: Added a NULL guard immediately after malloc() and replaced strcpy() with memcpy(dup, str, len + 1) for a bounded, explicit copy

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 dupstr() vulnerability in tools/strliteral.c is a textbook example of how a two-line oversight — missing a NULL check and using an unbounded copy function — can elevate a utility function to a critical security risk. The fix is equally concise: guard the allocation, use memcpy() with an explicit length, and remove the redundant null-termination. These are not exotic techniques; they are foundational C memory-safety practices that every C developer should have in their muscle memory.

The broader lesson is that no file in a production codebase is too small or too peripheral to warrant security review. Build tools, code generators, and utility scripts all represent attack surface. Treat them accordingly.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #9

Related Articles

critical

Aardvark.Cef.Process.Core Shared Memory Handler: Unvalidated memcpy

The shared memory handler in Aardvark.Cef.Process.Core failed to validate that the `length` parameter passed from JavaScript did not exceed the destination buffer size before calling `memcpy`. This allowed a compromised Chromium renderer process to corrupt heap memory by supplying an oversized `handle->length` value to the `openMapping` function. The fix adds explicit bounds checking using `GetArrayBufferByteLength()` before the copy operation.

high

XShmGetImage Heap Corruption: Unvalidated Image Height in streamproxy

The `XShmGetImage` function in the X11 shared memory image path copies pixel data row-by-row using `memcpy` without validating that the source image height matches the destination buffer height. An attacker or compromised server could provide an oversized source image, causing writes beyond the allocated heap buffer and triggering heap corruption or code execution.

critical

SGX Enclave ecall_store_data memcpy Buffer Overflow in 256-Byte

The Intel SGX enclave's trusted bridge functions `ecall_store_data` and `ecall_retrieve_data` used `memcpy()` to move data into and out of a fixed 256-byte `secure_storage` buffer without validating that `data_len` fit within destination boundaries. An attacker providing oversized `data_len` values could corrupt enclave memory, breaking SGX's confidentiality guarantees.

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

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.