Back to Blog
critical SEVERITY7 min read

How unsafe realloc() and memcpy() causes buffer overflow in C email parsing and how to fix it

A critical vulnerability in `email/receive.cpp` allowed attackers to trigger a buffer overflow by sending crafted emails with large payloads. The `writefunc()` callback failed to validate that `realloc()` succeeded before writing to the reallocated pointer, risking NULL pointer dereference and potential code execution. The fix adds a simple bounds check that validates the realloc result before use.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published June 3, 2026•Reviewed June 3, 2026

Answer Summary

This is a critical buffer overflow vulnerability (CWE-476: NULL Pointer Dereference / CWE-680: Integer Overflow to Buffer Overflow) in C's email receive callback. The `writefunc()` function in `email/receive.cpp:103` calls `realloc()` but immediately uses the returned pointer without checking if allocation failed. An attacker can send a large email payload to exhaust memory, causing `realloc()` to return NULL, then the subsequent `memcpy()` writes to a NULL pointer offset, crashing the application or enabling code execution. The fix stores the `realloc()` result in a temporary variable, validates it succeeded, and only updates the original pointer on success.

Vulnerability at a Glance

cweCWE-476 (NULL Pointer Dereference), CWE-680 (Integer Overflow to Buffer Overflow)
fixCheck realloc() result before assignment and return early on allocation failure
riskCritical - Remote code execution or denial of service via crafted email payloads
languageC
root causeMissing validation of realloc() return value before dereferencing the pointer
vulnerabilityUnsafe realloc() without bounds checking leading to NULL pointer dereference and buffer overflow

How unsafe realloc() and memcpy() causes buffer overflow in C email parsing and how to fix it

Introduction

In the email receive component, we discovered a critical vulnerability in email/receive.cpp that could allow attackers to crash the application or execute arbitrary code by sending specially crafted emails with large payloads. The vulnerability lived in the writefunc() callback function at line 103—a function responsible for buffering incoming email data during network transfers.

The problem was deceptively simple: the code called realloc() to grow a buffer but never checked whether the allocation succeeded. This is a classic C memory management mistake that becomes catastrophic in network-facing code where an attacker controls the input size. An adversary could send an email with a payload larger than available system memory, force realloc() to fail and return NULL, and then watch as the application blindly wrote data to a NULL pointer offset—triggering a crash or worse.

This matters because email/receive.cpp is production code handling untrusted network data. Unlike internal utilities, this code runs on the attack surface and processes input from potentially hostile email servers.


The Vulnerability Explained

The Unsafe Pattern

Let's look at the vulnerable code before the fix:

size_t writefunc(void *ptr, size_t size, size_t nmemb, cstring *s)
{
  size_t new_len = s->len + size*nmemb;
  s->ptr = static_cast<char *>(realloc (s->ptr, new_len+1));  // ← UNSAFE
  memcpy(s->ptr+s->len, ptr, size*nmemb);                    // ← DEREFERENCE
  s->ptr[new_len] = '\0';
  s->len = new_len;
  return size*nmemb;
}

The problem: On line 5 (in the original code), realloc() is called and its result is directly assigned to s->ptr. If memory allocation fails, realloc() returns NULL. The code then proceeds to line 6 and calls memcpy(s->ptr+s->len, ...), which dereferences a NULL pointer (or NULL + offset), causing undefined behavior.

Why This Happens

The writefunc() callback is used by libcurl to buffer incoming email data during IMAP/POP3 transfers. As email data arrives in chunks, writefunc() is called repeatedly with size and nmemb parameters representing the chunk size. The code calculates new_len = s->len + size*nmemb to determine the new buffer size and calls realloc() to expand the buffer.

The developer likely assumed realloc() would always succeed—a common but dangerous assumption in C. In reality, realloc() can fail if:
- The system has exhausted available memory
- The requested size overflows or is invalid
- The memory allocator hits internal limits

Attack Scenario

An attacker could:

  1. Send a crafted email with enormous attachments to the application's email receiving endpoint
  2. Trigger repeated writefunc() calls with large chunk sizes, each requesting more memory
  3. Exhaust system memory or trigger the allocator to refuse allocation
  4. Cause realloc() to return NULL while s->ptr still points to the old (now potentially freed) buffer
  5. Trigger memcpy(s->ptr+s->len, ...) to write to a NULL or invalid address, causing:
    - Segmentation fault → Denial of Service (application crash)
    - Heap corruption → Potential code execution if the NULL pointer happens to be in a writable memory region

The Orbis AppSec multi-agent AI scanner confirmed this pattern as exploitable and flagged it as V-001.


The Fix

What Changed

The fix is surgical and focused:

size_t writefunc(void *ptr, size_t size, size_t nmemb, cstring *s)
{
  size_t new_len = s->len + size*nmemb;
  char *new_ptr = static_cast<char *>(realloc (s->ptr, new_len+1));  // ← Temporary variable
  if (!new_ptr) return 0;                                             // ← Validation
  s->ptr = new_ptr;                                                   // ← Safe assignment
  memcpy(s->ptr+s->len, ptr, size*nmemb);
  s->ptr[new_len] = '\0';
  s->len = new_len;
  return size*nmemb;
}

Three critical changes:

  1. Line 4: Store the realloc() result in a temporary variable new_ptr instead of directly assigning to s->ptr
  2. Line 5: Validate that new_ptr is not NULL before proceeding
  3. Line 6: Only update s->ptr after confirming allocation succeeded

How This Solves the Problem

By using a temporary variable, the code preserves the original s->ptr if realloc() fails. If allocation fails:
- new_ptr is NULL
- The if (!new_ptr) return 0; check catches this
- s->ptr remains unchanged (still pointing to the old buffer)
- memcpy() is never called with a NULL pointer
- The function returns 0 to signal failure to libcurl

This maintains the security invariant: memcpy() is only executed if the buffer is valid.

Regression Testing

The PR includes a comprehensive regression test (tests/test_invariant_receive.cpp) that verifies the security invariant:

TEST_P(ReallocSecurityTest, ReallocFailureDoesNotCauseNullDeref) {
    // Invariant: After writefunc, s->ptr must be valid (non-NULL) or 
    // the function must handle allocation failure gracefully without crashing

    size_t result = writefunc(test_data.data(), 1, data_size, &s);

    // Security invariant: if writefunc returns non-zero (success),
    // the buffer must be valid and properly null-terminated
    if (result > 0) {
        ASSERT_NE(s.ptr, nullptr) << "Buffer must not be NULL after successful write";
        ASSERT_EQ(s.len, data_size) << "Length must match written data";
        ASSERT_EQ(s.ptr[s.len], '\0') << "Buffer must be null-terminated";
    }
}

This test runs with adversarial inputs (0-byte writes, large allocations) to ensure the fix doesn't regress.


Key Takeaways

  • Never assign realloc() directly to the original pointer. Use a temporary variable to preserve the old pointer if allocation fails.
  • The writefunc() callback in email/receive.cpp now validates realloc() before dereferencing, preventing NULL pointer dereference on memory exhaustion.
  • Network-facing code is particularly vulnerable because attackers can craft inputs (large email payloads) to trigger allocation failures.
  • This vulnerability could enable both DoS (crash) and potential code execution depending on memory layout and attacker control.
  • Static analysis tools can detect this pattern automatically—enable them in your CI/CD pipeline to catch similar issues before production.

How Orbis AppSec Detected This

Source: Email data arriving from libcurl's network receive callback (writefunc() parameter ptr with attacker-controlled size and nmemb)

Sink: realloc(s->ptr, new_len+1) at email/receive.cpp:103 followed by memcpy(s->ptr+s->len, ...) at line 106

Missing control: No validation that realloc() succeeded (returned non-NULL) before dereferencing the pointer

CWE: CWE-476 (NULL Pointer Dereference) / CWE-680 (Integer Overflow to Buffer Overflow)

Fix: Store realloc() result in temporary variable, validate it's non-NULL, and return early on allocation failure

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

This critical vulnerability demonstrates why memory management in C requires vigilance. The fix—a simple three-line change—transforms unsafe code into a security boundary that holds under adversarial input. For developers maintaining C code, especially code that processes untrusted network data, the lesson is clear: always validate allocation results, use temporary variables for realloc(), and test with adversarial input sizes.

The Orbis AppSec multi-agent AI scanner caught this issue through pattern matching on unchecked realloc() usage, proving that automated security scanning can find real vulnerabilities in production code. By integrating such tools into your development workflow, you can prevent similar issues from reaching production.

Secure coding in C is possible—it just requires discipline, validation, and the right tools.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1107

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.