Back to Blog
critical SEVERITY9 min read

Heap Corruption via Unchecked memcpy: How Integer Overflow Bugs Corrupt Memory in Windows File Operations

A critical buffer overflow vulnerability was discovered in `phlib/nativefile.c`, where multiple `memcpy` calls copied filename and extended-attribute data into fixed-size structures without verifying that source lengths didn't exceed destination buffer boundaries. An attacker supplying an oversized filename or EA name could corrupt adjacent heap memory, potentially enabling arbitrary code execution. The fix replaces unchecked arithmetic with Windows' safe integer helpers (`RtlULongAdd`, `RtlULon

O
By orbisai0security
May 28, 2026

Heap Corruption via Unchecked memcpy: How Integer Overflow Bugs Corrupt Memory in Windows File Operations

Introduction

Buffer overflows are among the oldest and most dangerous vulnerability classes in systems programming — and they keep showing up in production code. This post breaks down a critical-severity heap buffer overflow discovered in phlib/nativefile.c, a native file-operation helper used in a Windows application built on the Process Hacker / SystemInformer codebase.

The root cause is deceptively simple: arithmetic used to calculate allocation sizes was not protected against integer overflow, and the lengths of caller-supplied strings were never validated before being copied into those allocations. The result? A carefully crafted filename or extended-attribute (EA) name could silently corrupt heap memory adjacent to the allocated buffer.

If you write C or C++ code that touches the Windows Native API — or any low-level file I/O — this vulnerability pattern is one you need to recognize on sight.


The Vulnerability Explained

What Is a Heap Buffer Overflow?

A heap buffer overflow occurs when code writes data beyond the end of a heap-allocated buffer. Unlike stack overflows (which famously overwrite return addresses), heap overflows corrupt adjacent heap metadata or other live allocations. This can lead to:

  • Arbitrary code execution if an attacker can control what gets written and where
  • Privilege escalation in kernel-adjacent or privileged processes
  • Denial of service through process crashes or corrupted state
  • Information disclosure if heap layout leaks sensitive data from adjacent allocations

CWE Reference: This vulnerability maps to CWE-122: Heap-based Buffer Overflow and CWE-190: Integer Overflow or Wraparound.


The Vulnerable Code: Two Separate Sites

The vulnerability manifested in two places inside phlib/nativefile.c.

Site 1 — PhMoveFile: Rename Information Buffer

// VULNERABLE — before the fix
renameInfoLength = sizeof(FILE_RENAME_INFORMATION) + fileNameLength + sizeof(UNICODE_NULL);
renameInfo = PhAllocateStack(renameInfoLength);

FILE_RENAME_INFORMATION is a Windows Native API structure used when renaming files via NtSetInformationFile. It has a flexible trailing array for the new filename. The code calculates the required buffer size by adding three values together using plain C arithmetic:

  • sizeof(FILE_RENAME_INFORMATION) — fixed structure size
  • fileNameLength — caller-supplied, derived from a UNICODE_STRING
  • sizeof(UNICODE_NULL) — a two-byte null terminator

The problem: If fileNameLength is close to ULONG_MAX (0xFFFFFFFF), the addition wraps around to a small number. The allocation succeeds — allocating a tiny buffer — but the subsequent memcpy writes the full, large filename into it, blowing past the buffer boundary and corrupting the heap.

Site 2 — PhSetFileExtendedAttributes: EA Information Buffer

// VULNERABLE — before the fix
infoLength = sizeof(FILE_FULL_EA_INFORMATION) + (ULONG)Name->Length + sizeof(ANSI_NULL);
if (Value) infoLength += (ULONG)Value->Length + sizeof(ANSI_NULL);

FILE_FULL_EA_INFORMATION holds extended attributes for NTFS files. The same pattern repeats: unchecked addition of caller-supplied Name->Length and Value->Length values. A SIZE_T-to-ULONG cast ((ULONG)Name->Length) silently truncates on 64-bit systems if Name->Length exceeds 32 bits, and the additions that follow can still overflow.

There's a secondary issue here too: the EA name length is stored in a UCHAR field (EaNameLength, max 255) and the value length in a USHORT field (EaValueLength, max 65535). If the caller provides lengths that don't fit in those narrower types, the truncated values written into the structure will be inconsistent with the actual data copied — a recipe for downstream memory corruption.


Attack Scenario

Consider a privileged file manager or security tool that calls PhMoveFile with a destination path derived from user input (a rename dialog, a command-line argument, or a network-sourced path). An attacker who can influence the destination filename can supply a string whose byte length, when added to sizeof(FILE_RENAME_INFORMATION), wraps around to a small value. The allocator hands back a tiny buffer; the memcpy copies gigabytes (or just a few extra bytes) past it.

In practice, exploitability depends on:
- Whether the attacker controls input to these functions
- The heap layout at the time of the overflow
- Whether the process runs with elevated privileges (common for file utilities)

Even without a full exploit chain, a reliable crash (denial of service) is trivially achievable once this primitive is in hand.


The Fix

The fix replaces every unchecked arithmetic expression with Windows safe integer helper functions that return an error status on overflow, and adds narrowing conversion checks before assigning lengths to smaller-typed struct fields.

Fix 1 — PhMoveFile: Safe Size Calculation

// FIXED — after the patch
status = RtlULongAdd(sizeof(FILE_RENAME_INFORMATION), fileNameLength, &renameInfoLength);
if (!NT_SUCCESS(status))
    goto CleanupExit;

status = RtlULongAdd(renameInfoLength, sizeof(UNICODE_NULL), &renameInfoLength);
if (!NT_SUCCESS(status))
    goto CleanupExit;

renameInfo = PhAllocateStack(renameInfoLength);
if (!renameInfo) return STATUS_NO_MEMORY;

RtlULongAdd (from <intsafe.h>) performs the addition and returns STATUS_INTEGER_OVERFLOW if the result would exceed ULONG_MAX. The code now bails out cleanly before any allocation occurs, rather than allocating a dangerously undersized buffer.

Fix 2 — PhSetFileExtendedAttributes: Safe Size Calculation + Narrowing Checks

// FIXED — after the patch

// Step 1: Validate Name->Length fits in ULONG, then in UCHAR
status = RtlSIZETToULong(Name->Length, &nameLength);
if (!NT_SUCCESS(status))
    return status;
status = RtlULongToUChar(nameLength, &eaNameLength);  // must fit in UCHAR (max 255)
if (!NT_SUCCESS(status))
    return status;

// Step 2: Build infoLength safely
status = RtlULongAdd(sizeof(FILE_FULL_EA_INFORMATION), nameLength, &infoLength);
if (!NT_SUCCESS(status))
    return status;
status = RtlULongAdd(infoLength, sizeof(ANSI_NULL), &infoLength);
if (!NT_SUCCESS(status))
    return status;

// Step 3: Validate Value->Length fits in ULONG, then in USHORT
if (Value)
{
    status = RtlSIZETToULong(Value->Length, &valueLength);
    if (!NT_SUCCESS(status))
        return status;
    status = RtlULongToUShort(valueLength, &eaValueLength);  // must fit in USHORT (max 65535)
    if (!NT_SUCCESS(status))
        return status;

    status = RtlULongAdd(infoLength, valueLength, &infoLength);
    // ... continues
}

This fix addresses three distinct hazards in one location:
1. Integer overflow in the size arithmetic (via RtlULongAdd)
2. Truncating cast from SIZE_T to ULONG (via RtlSIZETToULong)
3. Narrowing mismatch between logical length and struct field width (via RtlULongToUChar / RtlULongToUShort)


Why RtlULongAdd and Friends?

Microsoft ships a comprehensive safe-integer library in <intsafe.h> specifically to address this class of bug. Each function:
- Performs the operation
- Checks for overflow/underflow
- Returns S_OK / STATUS_INTEGER_OVERFLOW accordingly
- Writes the result only if safe

Function Operation Overflow condition
RtlULongAdd a + b → ULONG Result > ULONG_MAX
RtlSIZETToULong SIZE_T → ULONG Value > ULONG_MAX
RtlULongToUChar ULONG → UCHAR Value > 255
RtlULongToUShort ULONG → USHORT Value > 65535

Using these functions is the Windows-idiomatic equivalent of Rust's checked_add() or C++'s std::numeric_limits checks.


Prevention & Best Practices

1. Never Use Unchecked Arithmetic for Allocation Sizes

Any expression of the form sizeof(X) + userSuppliedLength in C is a potential integer overflow. Treat every such expression as suspect until proven safe.

Unsafe pattern:

size_t bufLen = sizeof(HEADER) + userLen;  // wraps if userLen is huge
void *buf = malloc(bufLen);
memcpy(buf, userdata, userLen);            // overflow

Safe pattern (POSIX/Linux):

if (userLen > SIZE_MAX - sizeof(HEADER)) { return -EINVAL; }
size_t bufLen = sizeof(HEADER) + userLen;
void *buf = malloc(bufLen);
if (!buf) return -ENOMEM;
memcpy(buf, userdata, userLen);

Safe pattern (Windows Native API):

ULONG bufLen;
if (!NT_SUCCESS(RtlULongAdd(sizeof(HEADER), userLen, &bufLen)))
    return STATUS_INTEGER_OVERFLOW;
void *buf = ExAllocatePool(PagedPool, bufLen);

2. Validate Narrowing Conversions Explicitly

When you assign a wider integer to a narrower field (e.g., ULONGUCHAR), always check that the value fits. Silent truncation is a logic bug that can become a security bug.

// BAD
info->EaNameLength = (UCHAR)nameLen;  // silently truncates 300 → 44

// GOOD
if (nameLen > UCHAR_MAX) return STATUS_INVALID_PARAMETER;
info->EaNameLength = (UCHAR)nameLen;

3. Use Compiler and Sanitizer Tooling

Tool What it catches
AddressSanitizer (ASan) Heap/stack buffer overflows at runtime
UndefinedBehaviorSanitizer (UBSan) Integer overflow (with -fsanitize=integer)
/RTC1 (MSVC) Runtime checks for stack corruption
/analyze (MSVC SAL) Static analysis of buffer size annotations
CodeQL Dataflow analysis for tainted size expressions
Coverity / Polyspace Commercial static analysis

For Windows kernel and native-mode code specifically, Driver Verifier and Application Verifier with heap checking enabled will catch overflows at the point of occurrence rather than at a later, confusing crash site.

4. Annotate Buffer Parameters with SAL

Microsoft's Source Annotation Language (SAL) lets you document — and statically verify — buffer size constraints:

// Tell /analyze that DestBuffer has DestSize bytes available
void CopyData(
    _Out_writes_bytes_(DestSize) PVOID DestBuffer,
    _In_ ULONG DestSize,
    _In_reads_bytes_(SrcSize) PCVOID SrcBuffer,
    _In_ ULONG SrcSize
);

The MSVC static analyzer will flag calls where SrcSize > DestSize.

5. Apply the Principle of Least Trust to Lengths

Any length value that originates from outside the immediate function — a caller argument, a network packet, a file on disk, a user-mode buffer in kernel code — must be treated as untrusted until validated. This is especially true in:

  • File system drivers and filter drivers
  • IPC message handlers
  • Parsers for binary file formats
  • Any code that bridges user-mode and kernel-mode

6. Relevant Standards and References


Conclusion

This vulnerability is a textbook example of why integer arithmetic in allocation size calculations is a security-critical operation, not a mundane implementation detail. The original code looked completely reasonable at a glance — three values added together to size a buffer — but the absence of overflow checks turned it into a heap corruption primitive.

The fix is equally instructive: it doesn't require a dramatic architectural change. Replacing three lines of arithmetic with a handful of RtlULongAdd calls and narrowing-conversion checks is all it takes to close the vulnerability. The Windows safe-integer API exists precisely for this purpose, and using it consistently is a low-friction habit that pays significant security dividends.

Key takeaways for developers:

  1. Every addition or multiplication that feeds into an allocation size is a potential integer overflow — treat it that way.
  2. Every narrowing cast (wider int → narrower int) is a potential truncation bug — validate before casting, don't cast and hope.
  3. Use platform-provided safe integer libraries (<intsafe.h> on Windows, __builtin_add_overflow on GCC/Clang, checked_add in Rust) rather than rolling your own checks.
  4. Run AddressSanitizer and UBSan in your CI pipeline — they catch these bugs cheaply before they reach production.
  5. Automated security scanning (as used to detect this issue) is a valuable layer of defense, but it works best when paired with developer education about why these patterns are dangerous.

Secure coding is a discipline, not a checklist. Understanding the mechanism of vulnerabilities like this one is what allows you to spot novel instances of the same pattern in code that no scanner has seen before.


Fixed by the OrbisAI Security automated remediation pipeline. Learn more at orbisappsec.com.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2910

Related Articles

critical

Heap Buffer Overflow in Audio Ring Buffer: How a Missing Bounds Check Could Crash Your App

A critical heap buffer overflow vulnerability was discovered in `audio_backend.c`, where the audio ring buffer's `memcpy` operations lacked bounds validation before writing PCM data. Without checking that incoming data sizes fell within the allocated buffer's capacity, a maliciously crafted audio file could corrupt adjacent heap memory, potentially enabling arbitrary code execution. The fix adds a concise pre-flight validation guard that rejects out-of-range write requests before any memory oper

critical

Critical Heap Buffer Overflow in SSDP Control Point: How Unbounded String Operations Put Networks at Risk

A critical heap buffer overflow vulnerability was discovered and patched in the SSDP control point implementation (`ssdp_ctrlpt.c`), where multiple unbounded `strcpy` and `strcat` operations constructed HTTP request buffers without any length validation. Network-received SSDP response fields — including service type strings and location URLs — could be crafted by an attacker to exceed buffer boundaries, potentially enabling arbitrary code execution or denial of service. The fix replaces the unsa

critical

Heap Buffer Overflow in OPDS Parser: How a Misplaced Variable Nearly Opened the Door to Remote Code Execution

A critical heap buffer overflow vulnerability was discovered in `lib/OpdsParser/OpdsParser.cpp`, where the buffer allocation size was calculated *after* a fixed chunk size was used to allocate memory, meaning the actual bytes read could exceed the allocated buffer. On embedded devices parsing untrusted OPDS catalog data from the network, this flaw could allow a remote attacker to corrupt heap memory and potentially achieve arbitrary code execution. The fix was elegantly simple: move the `toRead`

critical

Heap Buffer Overflow in BLE MIDI: How a Missing Bounds Check Opens the Door to Remote Exploitation

A critical heap buffer overflow vulnerability was discovered in the BLE MIDI packet assembly code of `blemidi.c`, where attacker-controlled packet length values could trigger writes beyond allocated heap memory. The fix adds an integer overflow guard before the `malloc` call, ensuring that maliciously crafted BLE MIDI packets can no longer corrupt heap memory. This vulnerability is particularly dangerous because it is remotely exploitable by any nearby Bluetooth device — no physical access requi

critical

Heap Overflow in TOML Parser: How Integer Overflow Leads to Memory Corruption

A critical heap buffer overflow vulnerability was discovered and patched in the centitoml TOML parser, where missing integer overflow validation on a `MALLOC(len+1)` call could allow an attacker to trigger memory corruption via a crafted TOML configuration file. The vulnerability (CWE-190) is reachable through community-distributed mod or map files that the game loads from its `config/` directory, making it a realistic attack vector for remote code execution. A targeted one-line guard now preven

critical

Critical Memory Safety Bug: Free of Uninitialized Memory in Rust Telemetry (CVE-2021-29937)

CVE-2021-29937 is a critical memory safety vulnerability in the Rust `telemetry` crate (versions prior to 0.1.3) that allows freeing uninitialized memory, leading to undefined behavior, potential crashes, and possible code execution. The fix involves upgrading the crate from version 0.1.0 to 0.1.3, which patches the unsafe memory handling at the root cause. Despite Rust's reputation for memory safety, this vulnerability demonstrates that `unsafe` code blocks can still introduce serious bugs that