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
Published July 5, 2026Reviewed 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.


Prevention & Best Practices

1. Always check malloc() return values

In C, every call to malloc(), calloc(), or realloc() can return NULL. Treat this as a contract: if you do not check the return value, you do not own the pointer.

// ❌ Wrong
char *buf = malloc(size);
strcpy(buf, input);

// ✅ Correct
char *buf = malloc(size);
if (buf == NULL) return NULL;
memcpy(buf, input, size);

2. Prefer memcpy() over strcpy() when length is already known

If you have already called strlen() to compute the length, use that information. memcpy() with an explicit byte count is both safer and faster than strcpy(), which must scan for the null terminator at runtime.

3. Use compiler and sanitizer tooling

  • AddressSanitizer (ASan): Compile with -fsanitize=address to catch heap buffer overflows at runtime during testing.
  • UndefinedBehaviorSanitizer (UBSan): Compile with -fsanitize=undefined to catch NULL pointer dereferences.
  • Valgrind: Run test suites under Valgrind to detect invalid memory accesses.
  • clang-analyzer / cppcheck: Static analysis tools that flag unchecked malloc() return values.

4. Adopt safer string APIs

Where possible, use strlcpy() (BSD/macOS) or strncpy() with explicit length limits. For new code, consider using snprintf() for formatted string construction, which always null-terminates within the specified buffer size.

5. Apply CERT C guidelines

CERT C Secure Coding Standard rule MEM32-C specifically addresses this pattern: Detect and handle memory allocation errors. Integrating CERT C rules into code review checklists prevents this class of bug from reaching production.

Relevant standards

  • CWE-122: Heap-based Buffer Overflow
  • CWE-476: NULL Pointer Dereference
  • OWASP: Buffer Overflow
  • CERT C: MEM32-C, STR31-C

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 reviewtools/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.


References

Frequently Asked Questions

What is a heap buffer overflow in C?

A heap buffer overflow occurs when a program writes more data into a heap-allocated buffer than it was allocated to hold, or writes into a NULL pointer returned by a failed malloc(), corrupting adjacent memory or triggering a crash.

How do you prevent heap buffer overflow in C?

Always check the return value of malloc() before using the pointer, and use bounds-aware copy functions like memcpy() or strlcpy() instead of strcpy(), which has no length limit.

What CWE is heap buffer overflow?

Heap buffer overflow is classified as CWE-122 (Heap-based Buffer Overflow), a subset of CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer).

Is malloc() size calculation enough to prevent heap buffer overflow?

No. Correctly sizing the malloc() call is necessary but not sufficient — you must also verify that malloc() did not return NULL before writing to the buffer, and use copy functions that respect the allocated size.

Can static analysis detect heap buffer overflow from unchecked malloc()?

Yes. Static analysis tools like Semgrep, Coverity, and clang-analyzer can flag patterns where the return value of malloc() is used without a NULL check, and where strcpy() is used in place of safer alternatives.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #9

Related Articles

medium

How stack buffer overflow happens in C PMenu_Do_Update() and how to fix it

A stack buffer overflow in `PMenu_Do_Update()` within `src/menu/menu.c` allowed repeated unchecked `sprintf()` calls to overflow a fixed 1400-byte stack buffer when menu entries contained long text strings. The fix replaces every `sprintf(string + strlen(string), ...)` call with `snprintf(string + len, sizeof(string) - len, ...)`, tracking remaining buffer space and preventing writes beyond the buffer boundary.

critical

How buffer overflow happens in C TinyGSM sprintf and how to fix it

A critical buffer overflow vulnerability was discovered in `TinyGsmClientSequansMonarch.h` at line 515, where `sprintf` was writing a two-character hex string into a buffer only two bytes large — leaving no room for the null terminator. The fix replaces `sprintf` with `snprintf` and increases the buffer to three bytes, preventing a one-byte overflow that could corrupt adjacent memory in embedded firmware.

critical

How out-of-bounds array access happens in C kernel modules and how to fix it

A high-severity out-of-bounds array access vulnerability was discovered in the natflow_conntrack.c kernel module where the `ct->proto.tcp.state` value was used directly as an array index without bounds validation. An attacker capable of manipulating TCP connection state could trigger reads beyond the `tcp_conntrack_names[]` array, potentially leaking kernel memory or causing system crashes. The fix adds a simple bounds check using `ARRAY_SIZE()` before array access.

critical

How buffer overflow in strcpy() happens in C configuration parsing and how to fix it

A critical buffer overflow vulnerability in `src/rpconfig.h` allowed attackers to corrupt memory by providing configuration values exceeding buffer size limits. The `rpcSetText()` function used `strcpy()` to copy user-controlled data into a fixed 256-byte buffer without bounds checking, enabling stack corruption and potential code execution. Replacing `strcpy()` with `strncpy()` and enforcing a 255-byte limit eliminated the overflow risk.

critical

How buffer overflow happens in C gdb-server and how to fix it

A critical buffer overflow vulnerability was discovered in `src/st-util/gdb-server.c` where unbounded `memcpy()` and `strcpy()` calls could write beyond allocated buffer boundaries when processing user-supplied command-line arguments. The fix replaces all unsafe string operations with bounds-checked alternatives like `snprintf()` and `memcpy()` with explicit length validation.

critical

How argument injection happens in Node.js Copilot tool bridge and how to fix it

A high-severity argument injection vulnerability was discovered in the Copilot tool bridge (`bridge.ts`) where user-controlled `request.args` were passed directly to `tool.execute()` without any validation or sanitization. The fix introduces Zod schema validation at line 108, ensuring that tool arguments are parsed against a declared `inputSchema` before execution. This prevents malformed or malicious payloads — including prototype pollution attempts — from reaching the underlying tool implement