Integer Overflow in wf_cliprdr.c: How Clipboard Data Could Corrupt Memory
Introduction
The libs/clipboard/src/windows/wf_cliprdr.c file handles Windows clipboard redirection — the mechanism that lets users copy and paste between a local machine and a remote session. It's the kind of code that quietly does its job in the background, processing data that arrives from a remote peer. But buried at line 774, a subtle integer overflow flaw meant that a malicious remote peer could send specially crafted clipboard data and corrupt the application's heap.
This post breaks down exactly how that happens, what the fix looks like, and what every C developer should take away from this class of vulnerability.
The Vulnerability Explained
What Was Happening at Line 774
The vulnerable code allocated memory for an array of clipboard stream pointers using calloc:
// VULNERABLE - before the fix (wf_cliprdr.c, line 774)
instance->m_pStreams = (LPSTREAM*)calloc(instance->m_nStreams, sizeof(LPSTREAM));
At first glance, this looks perfectly normal. calloc(count, size) is the idiomatic C way to allocate a zeroed array. The problem is where m_nStreams comes from: it is derived directly from remote clipboard data, with no validation before it reaches this allocation.
The Integer Overflow Mechanics
calloc(count, size) internally computes count * size to determine how many bytes to allocate. On a 64-bit system, sizeof(LPSTREAM) — which is sizeof(void*) — is 8 bytes.
Now consider what happens when an attacker sends a crafted m_nStreams value of, say, SIZE_MAX / 8 + 1 (which is 0x2000000000000001 on a 64-bit system):
count = 0x2000000000000001
size = 8
count * size = 0x2000000000000001 * 8
= 0x10000000000000008 (overflows to 0x8 on 64-bit)
The multiplication wraps around to just 8 bytes — enough for a single pointer. But the code then proceeds to write m_nStreams worth of stream pointers into that allocation, writing far beyond the end of the 8-byte buffer.
This is a classic heap buffer overflow triggered by integer overflow — the allocation appears to succeed, the pointer is non-NULL, and nothing signals an error. The damage happens silently when the streams are populated.
Why This Is Particularly Dangerous
A few factors make this vulnerability especially concerning:
-
Remote trigger: The
m_nStreamsvalue comes from the remote peer's clipboard announcement. No local user interaction is required beyond having an active clipboard-sharing session. -
Silent failure:
callocreturns a valid (but tiny) pointer. There's no NULL check that would catch the problem, because the allocation succeeded — just with the wrong size. -
Heap corruption: Out-of-bounds writes to the heap can overwrite allocator metadata, adjacent objects, or function pointers, potentially enabling arbitrary code execution.
-
Production path: This is not test code or an edge-case handler. Clipboard redirection is a core feature of remote desktop workflows.
Attack Scenario
A malicious remote peer — or a compromised machine on the other end of a remote desktop session — crafts a clipboard format announcement where the stream count field is set to a value near SIZE_MAX / sizeof(LPSTREAM). When the local client processes this announcement and reaches the calloc at line 774, the undersized allocation is made. As the code iterates over the "streams" and writes pointers into the array, it overwrites adjacent heap memory. Depending on what lives next to that allocation, this can crash the process, corrupt application state, or, in a worst case, redirect execution.
The Fix
The fix introduces explicit bounds validation for m_nStreams before it is used in the calloc call. The security invariant is straightforward: if multiplying the count by sizeof(LPSTREAM) would overflow SIZE_MAX, or if the count exceeds a reasonable upper bound for clipboard streams, the allocation must be rejected.
Before: Unchecked Allocation
// BEFORE - no validation of m_nStreams
instance->m_pStreams = (LPSTREAM*)calloc(instance->m_nStreams, sizeof(LPSTREAM));
After: Guarded Allocation
// AFTER - bounds check before allocation
#define MAX_SAFE_STREAM_COUNT 1024
// Reject zero (invalid data)
if (instance->m_nStreams == 0) {
return ERROR_INVALID_DATA;
}
// Reject counts exceeding a reasonable maximum
if (instance->m_nStreams > MAX_SAFE_STREAM_COUNT) {
return ERROR_INVALID_DATA;
}
// Reject counts that would overflow the size calculation
if (instance->m_nStreams > SIZE_MAX / sizeof(LPSTREAM)) {
return ERROR_INVALID_DATA;
}
// Safe to allocate
instance->m_pStreams = (LPSTREAM*)calloc(instance->m_nStreams, sizeof(LPSTREAM));
Why Three Separate Checks?
Each guard addresses a distinct failure mode:
| Check | What It Prevents |
|---|---|
count == 0 |
Allocating a zero-byte buffer then writing into it |
count > MAX_SAFE_STREAM_COUNT |
Legitimate-looking but excessive allocations (DoS / logic abuse) |
count > SIZE_MAX / sizeof(LPSTREAM) |
The integer overflow that triggers the undersized allocation |
The MAX_SAFE_STREAM_COUNT bound of 1,024 deserves special mention. Real-world clipboard operations involve a small number of format streams — dozens at most. A count of 1,025 almost certainly indicates malicious or malformed data. This "semantic" bound catches dangerous inputs that are numerically valid but logically impossible for legitimate clipboard use.
The Regression Test
The fix is accompanied by a comprehensive regression test (tests/test_invariant_wf_cliprdr.c) that encodes the security invariant directly. Key test cases include:
/* Adversarial count values derived from remote clipboard data */
size_t adversarial_counts[] = {
SIZE_MAX,
SIZE_MAX / 2,
SIZE_MAX / LPSTREAM_SIZE,
SIZE_MAX / LPSTREAM_SIZE + 1,
0xFFFFFFFF,
MAX_SAFE_STREAM_COUNT + 1,
/* ... */
};
// INVARIANT: all of these must return NULL (rejected)
void **result = safe_alloc_streams(count);
ck_assert_msg(result == NULL, "SECURITY VIOLATION: ...");
This test will catch any future regression where the bounds check is accidentally removed or weakened.
A CI workflow (.github/workflows/wf-cliprdr-ci.yml) was also added to run these tests automatically on every change to the clipboard code, ensuring the invariant is continuously enforced.
Prevention & Best Practices
1. Never Trust Remote-Supplied Counts
Any value that comes from a network peer, a file, or any external source and is used as an allocation size must be validated. The pattern calloc(remote_value, element_size) is a red flag that should always trigger a review.
2. Use the count > SIZE_MAX / element_size Pattern
This is the idiomatic, portable way to check for overflow before a count * size multiplication in C:
if (count > SIZE_MAX / element_size) {
// overflow would occur — reject
}
Avoid relying on compiler extensions or platform-specific behavior for overflow detection.
3. Add Semantic Bounds
Mathematical overflow checks are necessary but not sufficient. Add a domain-specific upper bound that reflects what is reasonable for your application. For clipboard streams, 1,024 is generous. For other contexts, choose a bound that matches the real-world maximum with a safety margin.
4. Use Static Analysis
Tools like CodeQL, Coverity, and clang-analyzer can flag unvalidated values flowing into allocation functions. The multi-agent AI scanner that caught this issue (V-003) is another example of automated detection that should be part of any security pipeline.
5. Consider Safe Integer Libraries
For C code that performs many size calculations, consider using a safe integer library such as safe-iop or the __builtin_mul_overflow GCC/Clang intrinsic:
size_t total;
if (__builtin_mul_overflow(count, sizeof(LPSTREAM), &total)) {
// overflow detected
}
References
Key Takeaways
m_nStreamsat line 774 was the root cause: A single unvalidated field from remote clipboard data was sufficient to trigger heap corruption. Always trace allocation inputs back to their source.callocis not safe by itself:calloc(count, size)does check for overflow internally on some platforms, but you cannot rely on this — and even when it fails, the behavior is implementation-defined. Validate before calling.- Three-layer validation is the right model: Zero check, semantic maximum, and overflow check together cover the full attack surface for count-based allocations.
- Regression tests should encode invariants, not just behavior: The test suite added here doesn't just test the happy path — it explicitly encodes the security property ("adversarial counts must return NULL") as machine-checkable assertions.
- CI enforcement closes the loop: Adding the
.github/workflows/wf-cliprdr-ci.ymlworkflow means this invariant will be re-verified on every future change to the clipboard code, preventing the fix from being quietly undone.
Conclusion
The integer overflow in wf_cliprdr.c is a textbook example of why remote-supplied numeric values demand the same skepticism as remote-supplied strings. The fix is small — three if statements before a calloc call — but the absence of those checks created a direct path from a malicious clipboard announcement to heap corruption. By validating m_nStreams against zero, a semantic maximum, and the overflow boundary before allocation, the fix closes all three attack vectors simultaneously. If you write C code that allocates memory based on externally-supplied counts, audit those sites now. The pattern is common, the fix is straightforward, and the cost of missing one can be severe.