Back to Blog
critical SEVERITY8 min read

How integer overflow in _wopendir() happens in C Windows dirent and how to fix it

A critical integer overflow vulnerability in `include/compat/dirent_msvc.h` allowed an attacker-controlled directory path length to wrap the `sizeof(wchar_t) * n + 16` allocation calculation, resulting in a dangerously undersized heap buffer. Subsequent writes to that buffer caused a heap overflow, enabling potential memory corruption or code execution on Windows systems. The fix adds a pre-allocation bounds check and proper errno signaling to safely reject overflow-inducing inputs.

O
By Orbis AppSec
Published June 15, 2026Reviewed June 15, 2026

Answer Summary

This is a heap buffer overflow (CWE-120) in the `_wopendir()` function inside `include/compat/dirent_msvc.h`, caused by an unchecked integer overflow in the expression `sizeof(wchar_t) * n + 16` used as the argument to `malloc()`. On Windows, if an attacker supplies a directory path whose length `n` is close to `SIZE_MAX / sizeof(wchar_t)`, the multiplication wraps around to a small value, causing `malloc()` to allocate a tiny buffer that is then overwritten. The fix adds a pre-malloc guard `if (n <= (SIZE_MAX - 16) / sizeof(wchar_t))` to reject overflow-inducing sizes and calls `_set_errno(ENOMEM)` to signal failure cleanly.

Vulnerability at a Glance

cweCWE-120
fixAdded `if (n <= (SIZE_MAX - 16) / sizeof(wchar_t))` guard before the malloc call and added `_set_errno(ENOMEM)` in the failure branch
riskHeap corruption, potential arbitrary code execution on Windows hosts
languageC (Windows / MSVC compatibility header)
root cause`sizeof(wchar_t) * n + 16` is passed directly to `malloc()` without checking for integer overflow when `n` is attacker-influenced
vulnerabilityInteger overflow leading to heap buffer overflow in malloc() size calculation

How integer overflow in _wopendir() happens in C Windows dirent and how to fix it

Summary

A critical integer overflow vulnerability in include/compat/dirent_msvc.h allowed an attacker-controlled directory path length to wrap the sizeof(wchar_t) * n + 16 allocation calculation, resulting in a dangerously undersized heap buffer. Subsequent writes to that buffer caused a heap overflow, enabling potential memory corruption or code execution on Windows systems. The fix adds a pre-allocation bounds check and proper errno signaling to safely reject overflow-inducing inputs.


Introduction

The include/compat/dirent_msvc.h file is a POSIX compatibility shim that brings opendir/readdir/closedir semantics to Windows MSVC builds. Deep inside its _wopendir() function — the wide-character implementation that handles Windows-native wchar_t directory paths — a single unguarded arithmetic expression created a textbook heap buffer overflow condition.

The vulnerable line, at line 168 of the original file, reads:

dirp->patt = (wchar_t*) malloc(sizeof(wchar_t) * n + 16);

Here, n is the return value of GetFullPathNameW(dirname, 0, NULL, NULL) — the number of wide characters needed to represent the full path. On its own this looks reasonable. But n is ultimately derived from the dirname argument passed by the caller, and if an attacker can influence that string, they can supply a path length that causes sizeof(wchar_t) * n to silently overflow SIZE_MAX, wrapping around to a tiny number. malloc() then happily allocates that tiny buffer, and the code proceeds to write a full-length path into it — corrupting the heap.


The Vulnerability Explained

What actually overflows

sizeof(wchar_t) is 2 on Windows. The multiplication sizeof(wchar_t) * n is performed in size_t arithmetic. On a 32-bit system, SIZE_MAX is 0xFFFFFFFF. If n equals (SIZE_MAX / 2) + 1 — which is 0x80000000 — the product becomes:

2 * 0x80000000 = 0x100000000

In 32-bit size_t arithmetic that wraps to 0x00000000. Add 16 and you get an allocation of exactly 16 bytes. On 64-bit Windows the threshold is larger but equally reachable given that GetFullPathNameW accepts arbitrarily long input strings.

The vulnerable code, verbatim from the diff:

/* VULNERABLE - no overflow check */
dirp->patt = (wchar_t*) malloc(sizeof(wchar_t) * n + 16);
if (dirp->patt) {
    /*
     * ... code that writes sizeof(wchar_t) * n bytes into dirp->patt ...
     */
}

The if (dirp->patt) check only guards against malloc() returning NULL. It does not guard against malloc() succeeding with a dangerously small allocation — which is exactly what happens after an overflow.

How an attacker exploits this

On Windows, directory paths can be supplied through many channels: file open dialogs, network share paths (\\server\share\...), environment variables, or application-level path configuration. An attacker who can influence the dirname argument to _wopendir() — for example, by supplying a crafted UNC path or by setting a working directory through an application's configuration — can trigger the following sequence:

  1. Supply a path string long enough that GetFullPathNameW returns n ≈ SIZE_MAX / sizeof(wchar_t).
  2. sizeof(wchar_t) * n + 16 overflows to a small value, say 16 or 32 bytes.
  3. malloc(16) succeeds and returns a valid 16-byte heap chunk.
  4. The subsequent GetFullPathNameW call writes the full multi-kilobyte path into that 16-byte buffer.
  5. Adjacent heap metadata and allocations are overwritten — classic heap corruption.

Heap corruption of this kind can be leveraged for denial of service (crash), data leakage from adjacent heap regions, or, with sufficient heap-grooming effort, arbitrary code execution.

Why this file is particularly sensitive

dirent_msvc.h is a production compatibility header included in Windows builds. It is not test scaffolding. Any code path on Windows that calls opendir() with a user-influenced path goes through _wopendir(). This is a broad attack surface in applications that enumerate files or directories based on external input.


The Fix

The patch introduces two targeted changes, both visible in the diff:

Change 1: Pre-allocation overflow guard (line 166–167)

Before:

/* Allocate room for absolute directory name and search pattern */
dirp->patt = (wchar_t*) malloc(sizeof(wchar_t) * n + 16);

After:

/* Allocate room for absolute directory name and search pattern */
if (n <= (SIZE_MAX - 16) / sizeof(wchar_t))
    dirp->patt = (wchar_t*) malloc(sizeof(wchar_t) * n + 16);

The guard n <= (SIZE_MAX - 16) / sizeof(wchar_t) performs the overflow check using only safe division — no multiplication that could itself overflow. Breaking it down:

  • SIZE_MAX - 16 computes the largest value that can still have 16 added to it without overflow.
  • Dividing by sizeof(wchar_t) gives the maximum n for which sizeof(wchar_t) * n + 16 fits in size_t.
  • If n exceeds that threshold, the malloc call is skipped entirely, leaving dirp->patt as NULL.

The existing if (dirp->patt) check immediately below then correctly handles the NULL case and falls through to the error path.

Change 2: Explicit errno on allocation failure (line 219)

Before:

} else {
    /* Cannot allocate memory for search pattern */
    error = 1;
}

After:

} else {
    /* Cannot allocate memory for search pattern */
    _set_errno(ENOMEM);
    error = 1;
}

Previously, when dirp->patt was NULL (whether from a real OOM or from the new overflow guard), the error path set error = 1 but left errno undefined. Callers checking errno after a failed opendir() would get stale or misleading values. Adding _set_errno(ENOMEM) ensures that the POSIX contract is honored: a failed allocation signals ENOMEM, allowing callers to distinguish "directory not found" from "allocation failure."

Why stdint.h was added

The diff also adds #include <stdint.h> at the top of the file. SIZE_MAX is defined in <stdint.h> (or <stddef.h> depending on the platform). Without this include, the guard expression (SIZE_MAX - 16) / sizeof(wchar_t) would fail to compile on toolchains that do not implicitly provide SIZE_MAX. This is a necessary portability companion to the fix.


Prevention & Best Practices

1. Always check multiplication before passing to malloc()

The canonical safe-allocation pattern in C is:

/* Safe: check before multiply */
if (count > 0 && element_size > 0 && count <= (SIZE_MAX - overhead) / element_size) {
    ptr = malloc(element_size * count + overhead);
}

Never write malloc(a * b + c) when any of a, b, or c come from external input without this kind of guard.

2. Prefer calloc() for array allocations

calloc(nmemb, size) performs the overflow check internally on conforming implementations:

/* calloc checks nmemb * size overflow internally */
ptr = calloc(n, sizeof(wchar_t));

This does not cover the + 16 overhead pattern, but for pure array allocations it is safer by default.

3. Use compiler-assisted overflow detection during development

On GCC/Clang, -fsanitize=integer (UBSan) detects integer overflow at runtime during testing. On MSVC, /RTC1 catches some arithmetic issues. These are not substitutes for correct code but catch regressions quickly.

4. Treat all lengths from OS APIs as untrusted

GetFullPathNameW returns a length derived from caller-supplied input. Any value returned from an API that processes external data — file names, network paths, environment variables — must be treated as potentially adversarial before use in arithmetic.

5. Reference standards

  • CWE-120: Buffer Copy without Checking Size of Input — https://cwe.mitre.org/data/definitions/120.html
  • CWE-190: Integer Overflow or Wraparound — https://cwe.mitre.org/data/definitions/190.html
  • CWE-122: Heap-Based Buffer Overflow — https://cwe.mitre.org/data/definitions/122.html
  • OWASP: Memory Management Cheat Sheet

Key Takeaways

  • sizeof(wchar_t) * n is not safe without an overflow check when n comes from GetFullPathNameW() or any other API that processes caller-supplied paths.
  • A successful malloc() return does not mean the buffer is large enough — if the size argument overflowed, the buffer is silently undersized.
  • The fix pattern n <= (SIZE_MAX - overhead) / element_size is the correct idiom for pre-multiplication overflow detection in C without introducing further overflow risk.
  • Always set errno explicitly in error paths — leaving it undefined after a failure violates POSIX contracts and makes callers unable to distinguish error types.
  • Production compatibility headers like dirent_msvc.h are high-value targets because they sit below application logic and handle OS-level resources with attacker-influenced inputs.

How Orbis AppSec Detected This

  • Source: The dirname argument to _wopendir(), which originates from caller-supplied directory path strings (e.g., user input, environment variables, network paths on Windows).
  • Sink: malloc(sizeof(wchar_t) * n + 16) at include/compat/dirent_msvc.h:168, where n is the unvalidated return value of GetFullPathNameW(dirname, 0, NULL, NULL).
  • Missing control: No bounds check on n before the multiplication. The expression sizeof(wchar_t) * n + 16 was passed directly to malloc() without verifying it would not overflow size_t.
  • CWE: CWE-120 — Buffer Copy without Checking Size of Input (also related: CWE-190 Integer Overflow, CWE-122 Heap-Based Buffer Overflow).
  • Fix: Added the guard if (n <= (SIZE_MAX - 16) / sizeof(wchar_t)) immediately before the malloc() call to reject any n that would cause arithmetic overflow, and added _set_errno(ENOMEM) to the failure branch for correct error signaling.

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

Integer overflow in heap allocation size calculations is one of the most subtle and dangerous classes of C vulnerability. The bug in _wopendir() is a perfect illustration: the code looks correct at a glance — it allocates memory, checks the pointer, and proceeds. But the allocation size itself was never validated, and an attacker with control over path length could silently reduce a multi-kilobyte allocation to 16 bytes.

The fix is small — a single conditional and an errno assignment — but its impact is significant. It closes a potential heap corruption vector in every Windows build that calls opendir() with external input. If you maintain C code on Windows that processes file paths, audit every malloc() call where the size involves multiplication by a caller-influenced value. The pattern if (n <= (SIZE_MAX - overhead) / element_size) is your friend.


References

Frequently Asked Questions

What is an integer overflow in a malloc() size calculation?

It occurs when arithmetic used to compute a heap allocation size wraps around the maximum value of `size_t`, producing a much smaller number than intended. The resulting undersized buffer is then overwritten, causing a heap buffer overflow.

How do you prevent integer overflow in malloc() size calculations in C?

Always validate that the computed size will not exceed `SIZE_MAX` before passing it to `malloc()`. Use the pattern `if (n <= (SIZE_MAX - overhead) / element_size)` to check safe bounds before multiplying.

What CWE is integer overflow leading to buffer overflow?

CWE-120 (Buffer Copy without Checking Size of Input) covers this class of vulnerability. Related identifiers include CWE-190 (Integer Overflow or Wraparound) and CWE-122 (Heap-Based Buffer Overflow).

Is checking the return value of malloc() enough to prevent this vulnerability?

No. If the size argument overflows before being passed to malloc(), the allocator may succeed but return a buffer that is far too small. The overflow must be detected before the call, not after.

Can static analysis detect integer overflow in malloc() size arguments?

Yes. Tools like Semgrep, CodeQL, and dedicated C analyzers can flag patterns where user-influenced values are multiplied and added before being passed to allocation functions without overflow checks.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1304

Related Articles

critical

How buffer overflow in URL parsing happens in C++ HTTP client and how to fix it

A critical buffer overflow vulnerability in the HTTP client's URL parsing function allowed attackers to overflow a stack-allocated host buffer through specially crafted URLs with excessively long hostnames. The vulnerability enabled arbitrary code execution by overwriting the return address. The fix adds proper bounds validation before the memcpy() operation to ensure the hostname length never exceeds the destination buffer size.

critical

How buffer overflow happens in C xxd utility and how to fix it

A critical buffer overflow vulnerability was discovered in the xxd utility's `xxdline()` function where `strcpy()` was used without bounds checking on file input. An attacker could craft a malicious hex dump file with oversized lines to trigger memory corruption. The fix replaces the unsafe `strcpy()` with `snprintf()` to enforce buffer size limits.

critical

How buffer overflow in memcpy() happens in C/C++ embedded firmware and how to fix it

A critical buffer overflow vulnerability was discovered in the ESP32-based micro-journal firmware where `memcpy()` calls used `strlen()` without bounds checking, allowing oversized USB descriptor strings to corrupt adjacent memory. The fix replaces unbounded `strlen()` with `strnlen()` calls that enforce the destination buffer sizes (8, 16, and 4 bytes respectively), preventing heap/stack corruption from malicious USB devices.

high

How Denial of Service via crafted URI templates happens in Ruby addressable and how to fix it

A high-severity Denial of Service vulnerability (CVE-2026-35611) was discovered in the Ruby `addressable` gem versions prior to 2.9.0, which could allow attackers to crash or hang applications by sending specially crafted URI templates. The fix upgrades the dependency from version 2.8.7 to 2.9.0 across the Gemfile, Gemfile.lock, and gemspec in a Fastlane project, eliminating the vulnerable code path entirely.

critical

How Server-Side Request Forgery (SSRF) happens in Python requests.get() and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `models/common.py` where `requests.get()` fetched images from arbitrary URLs without validating whether the target resolved to internal infrastructure. An attacker could supply URLs targeting AWS metadata endpoints (169.254.169.254), private networks, or localhost services through the Flask REST API. The fix introduces DNS-resolution-based validation using Python's `socket.getaddrinfo()` and `ipaddress` module to block

critical

How heap buffer overflow happens in C WiFi frame capture and how to fix it

A critical buffer overflow vulnerability in the ESP32 WiFi frame capture feature (feat_capture_hs.c) allowed attackers within WiFi range to craft oversized 802.11 frames that would overflow heap buffers and achieve remote code execution. The fix adds explicit length validation before memcpy operations and rejects oversized frames rather than silently truncating them.