Back to Blog
critical SEVERITY5 min read

How stack buffer overflow happens in C memcpy() with caller-controlled length and how to fix it

A critical stack buffer overflow vulnerability was discovered in GDI/Comdlg32.cpp where the `memcpy()` function used a caller-controlled `lStructSize` field without validation, allowing attackers to write beyond stack-allocated buffers. The fix applies a simple `min()` check across four affected dialog functions to ensure copy operations never exceed the destination buffer size.

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

Answer Summary

This is a stack buffer overflow vulnerability (CWE-120) in C/C++ Windows dialog code where `memcpy()` copies data using an attacker-controlled length field (`lStructSize`) without bounds checking. The fix wraps the copy length with `min(lpOpenFile->lStructSize, sizeof(OpenFile))` to ensure the copy never exceeds the stack-allocated destination buffer, preventing stack smashing attacks in the `GetOpenFileName` and `GetSaveFileName` wrapper functions.

Vulnerability at a Glance

cweCWE-120
fixClamp copy length to min(lStructSize, sizeof(destination))
riskRemote code execution via stack smashing
languageC/C++
root causeUsing caller-controlled lStructSize as memcpy length without validation
vulnerabilityStack Buffer Overflow

Introduction

In the GDI/Comdlg32.cpp file, which provides wrapper functions for Windows common dialog operations, a critical stack buffer overflow vulnerability lurked in four seemingly innocent memcpy() calls. The functions comdlg_GetOpenFileNameA, comdlg_GetOpenFileNameW, comdlg_GetSaveFileNameA, and comdlg_GetSaveFileNameW all shared the same dangerous pattern: they trusted a caller-supplied size field to determine how many bytes to copy onto the stack.

At lines 57, 80, 103, and 126, the code performed:

memcpy(&OpenFile, lpOpenFile, lpOpenFile->lStructSize);

This single line is a textbook example of why you should never trust caller-controlled data for memory operations. The lStructSize field in the OPENFILENAME structure is meant to indicate the structure's size for versioning purposes, but nothing stops a malicious caller from setting it to an arbitrarily large value.

The Vulnerability Explained

What Made This Code Dangerous

The vulnerable pattern appeared in the comdlg_GetOpenFileNameA function at line 57:

BOOL WINAPI comdlg_GetOpenFileNameA(LPOPENFILENAMEA lpOpenFile)
{
    if (lpOpenFile && (lpOpenFile->Flags & OFN_ENABLEHOOK))
    {
        OPENFILENAMEA OpenFile = {};
        memcpy(&OpenFile, lpOpenFile, lpOpenFile->lStructSize);  // VULNERABLE!
        UpdateOpenFileNameStruct(OpenFile);
        return GetOpenFileName(&OpenFile);
    }
    // ...
}

Here's the problem: OpenFile is a stack-allocated structure with a fixed size determined at compile time by sizeof(OPENFILENAMEA). However, the memcpy() call uses lpOpenFile->lStructSize as the copy length—a value completely controlled by the caller.

Attack Scenario

An attacker could craft a malicious OPENFILENAME structure like this:

// Attacker's code
std::vector<uint8_t> malicious_buffer(0x1000, 0x41);  // 4KB of 'A's
OPENFILENAMEA* crafted = reinterpret_cast<OPENFILENAMEA*>(malicious_buffer.data());
crafted->lStructSize = 0x1000;  // Claim the structure is 4KB
crafted->Flags = OFN_ENABLEHOOK;  // Trigger the vulnerable code path

// Embed shellcode or ROP gadgets at the right offset to overwrite return address
// ...

comdlg_GetOpenFileNameA(crafted);  // BOOM - stack smash

When memcpy() executes with a 4KB length but only a ~100-byte destination buffer, it writes far beyond OpenFile, overwriting:
- Other local variables
- Saved frame pointer
- Return address (critical for exploitation)
- Potentially reaching into adjacent stack frames

This is a classic stack smashing attack that can lead to arbitrary code execution.

Why This Pattern Repeated Four Times

The same vulnerable pattern existed in all four dialog wrapper functions:
- comdlg_GetOpenFileNameA (line 57) - ANSI version of file open
- comdlg_GetOpenFileNameW (line 80) - Unicode version of file open
- comdlg_GetSaveFileNameA (line 103) - ANSI version of file save
- comdlg_GetSaveFileNameW (line 126) - Unicode version of file save

Copy-paste programming propagated the vulnerability across all variants of the dialog functions.

The Fix

The fix is elegant in its simplicity: clamp the copy length to never exceed the destination buffer size.

Before (Vulnerable)

memcpy(&OpenFile, lpOpenFile, lpOpenFile->lStructSize);

After (Fixed)

memcpy(&OpenFile, lpOpenFile, min(lpOpenFile->lStructSize, sizeof(OpenFile)));

The min() macro ensures that even if lStructSize is set to a billion bytes, the actual copy will never exceed sizeof(OpenFile)—the exact size of the stack-allocated destination buffer.

Applied Across All Four Locations

The fix was applied consistently to all vulnerable call sites:

Line 57 (GetOpenFileNameA):

-       memcpy(&OpenFile, lpOpenFile, lpOpenFile->lStructSize);
+       memcpy(&OpenFile, lpOpenFile, min(lpOpenFile->lStructSize, sizeof(OpenFile)));

Line 80 (GetOpenFileNameW):

-       memcpy(&OpenFile, lpOpenFile, lpOpenFile->lStructSize);
+       memcpy(&OpenFile, lpOpenFile, min(lpOpenFile->lStructSize, sizeof(OpenFile)));

Line 103 (GetSaveFileNameA):

-       memcpy(&OpenFile, lpOpenFile, lpOpenFile->lStructSize);
+       memcpy(&OpenFile, lpOpenFile, min(lpOpenFile->lStructSize, sizeof(OpenFile)));

Line 126 (GetSaveFileNameW):

-       memcpy(&OpenFile, lpOpenFile, lpOpenFile->lStructSize);
+       memcpy(&OpenFile, lpOpenFile, min(lpOpenFile->lStructSize, sizeof(OpenFile)));

Why This Fix Works

The security invariant is now enforced: buffer writes never exceed the declared destination size. Even with a maliciously crafted lStructSize of 0xFFFFFFFF, the copy is bounded to exactly sizeof(OPENFILENAME) bytes—safe for the stack-allocated buffer.

Key Takeaways

  • Never use lStructSize or similar caller-controlled fields directly as memcpy() lengths — always clamp to sizeof(destination)
  • The min() pattern is your friendmin(user_length, sizeof(buffer)) is a defensive programming essential
  • Copy-paste vulnerabilities multiply — when fixing one instance, search for the same pattern throughout the codebase (this fix addressed 4 locations)
  • Stack-allocated buffers are especially dangerous — overflow can directly corrupt return addresses, enabling RCE
  • Regression tests should encode security invariants — the added test suite verifies the fix with adversarial inputs including sizeof(OPENFILENAME) * 10 and 0xFFFF

How Orbis AppSec Detected This

  • Source: The lpOpenFile->lStructSize field in caller-provided OPENFILENAME structures
  • Sink: memcpy(&OpenFile, lpOpenFile, lpOpenFile->lStructSize) at lines 57, 80, 103, and 126 in GDI/Comdlg32.cpp
  • Missing control: No validation that lStructSize does not exceed sizeof(OpenFile) before the copy operation
  • CWE: CWE-120 (Buffer Copy without Checking Size of Input)
  • Fix: Added min(lpOpenFile->lStructSize, sizeof(OpenFile)) to clamp the copy length to the destination buffer size

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 vulnerability demonstrates a fundamental truth in systems programming: never trust external data for memory operations. The lStructSize field, while seemingly innocuous as a versioning mechanism, became an attack vector when used directly as a memcpy() length.

The fix—a simple min() call—is both minimal and complete. It preserves functionality for legitimate callers while neutralizing malicious inputs. More importantly, the accompanying regression test suite ensures this security property is maintained as the code evolves.

When working with C/C++ code that handles external structures, always ask: "What happens if this size field is larger than I expect?" If the answer involves writing past buffer boundaries, you've found a vulnerability waiting to happen.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #564

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.