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 Orbis AppSec
Published May 28, 2026Reviewed June 3, 2026

Answer Summary

This is a critical heap buffer overflow vulnerability (CWE-122) in C code within `phlib/nativefile.c`, caused by multiple `memcpy` calls that copied filename and extended-attribute (EA) data into fixed-size structures without verifying source lengths against destination buffer boundaries. The vulnerability also involved integer overflow in size calculations used for heap allocations. The fix replaces unchecked arithmetic with Windows' safe integer helpers `RtlULongAdd` and `RtlULongMult`, which return failure codes if overflow would occur, preventing both the integer overflow and the subsequent out-of-bounds memory write.

Vulnerability at a Glance

cweCWE-122
fixReplace unchecked addition/multiplication with RtlULongAdd/RtlULongMult; validate all lengths before memcpy
riskHeap memory corruption enabling arbitrary code execution or privilege escalation
languageC
root causememcpy calls copied variable-length filename/EA data into fixed-size structures without bounds checking; size arithmetic could overflow before allocation
vulnerabilityHeap Buffer Overflow via Unchecked memcpy and Integer Overflow

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.

Frequently Asked Questions

What is a heap buffer overflow?

A heap buffer overflow occurs when a program writes more data into a heap-allocated buffer than it was sized to hold, corrupting adjacent memory. In this case, memcpy copied attacker-controlled filename or EA data past the end of a fixed-size structure.

How do you prevent heap buffer overflow in C?

Always validate that the source length does not exceed the destination buffer size before calling memcpy or similar functions. Use safe integer arithmetic helpers (like RtlULongAdd on Windows or checked_add in other environments) to prevent size calculations from wrapping around.

What CWE is heap buffer overflow?

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

Is bounds checking on the final memcpy enough to prevent this vulnerability?

No. If the size passed to malloc/HeapAlloc was itself computed with overflowing arithmetic, the allocation may be too small even before memcpy runs. Both the size calculation and the copy operation must be validated independently.

Can static analysis detect this type of vulnerability?

Yes. Static analysis tools like Semgrep, CodeQL, and PVS-Studio can flag memcpy calls where the length argument is derived from user-controlled input without prior validation, as well as integer arithmetic that lacks overflow checks.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2910

Related Articles

critical

How buffer overflow happens in C tar header parsing and how to fix it

A critical buffer overflow vulnerability was discovered in `microtar/microtar.c` where the `raw_to_header()` and `header_to_raw()` functions used unbounded `strcpy()` and `sprintf()` calls to copy tar header fields. Malicious tar files with non-null-terminated name fields could overflow destination buffers, potentially leading to code execution. The fix replaces all unsafe string operations with bounded alternatives: `memcpy()` with explicit null-termination and `snprintf()` instead of `sprintf(

critical

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

A critical buffer overflow vulnerability was discovered in `src/firmware/src/net/ieee80211.c` at line 1584, where the `ieee80211_input()` function processed raw 802.11 data frames without verifying that the incoming frame was large enough to contain a valid `ieee80211_frame` header. An attacker within wireless range could craft undersized or malformed frames to trigger memory corruption, potentially leading to remote code execution on the firmware. The fix adds a single, targeted bounds check th

high

How buffer overflow from unsafe string copy functions happens in C network interface code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `generic/eth-impl.c`, where unsafe `strncpy()` and `sprintf()` calls could write beyond buffer boundaries when handling network interface names and device filenames. The fix replaced these dangerous functions with bounded `snprintf()` calls that guarantee null-termination and prevent memory corruption.

critical

How buffer overflow in FuzzIxml.c sprintf() happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in `fuzzer/FuzzIxml.c` where `sprintf()` wrote a PID-formatted filename into a fixed 256-byte stack buffer without any bounds checking. The fix replaces `sprintf()` with `snprintf()`, explicitly passing the buffer size to prevent any overflow. While exploitation in this specific fuzzer context requires local access, the pattern is a textbook example of CWE-120 that developers should recognize and eliminate everywhere it appears.

critical

How buffer overflow happens in C HTML parsing and how to fix it

A critical buffer overflow vulnerability in `include/html_parse.h` allowed attackers to overflow buffers by providing malicious HTML input exceeding buffer capacity. The fix adds proper bounds checking before memcpy() operations to prevent memory corruption and potential code execution.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.