CVE-2026-56190: tracking an RDP clipboard lifetime bug through the patch
A static patch diff of rdpcorets.dll shows how Microsoft closed a manager teardown gap affecting CLIPRDR proxy streams and data objects.
- Published
Microsoft fixed CVE-2026-56190 in the July 2026 security updates. The advisory describes a use of an uninitialized resource in Windows RDP that can lead to remote code execution. Microsoft assigns a CVSS 3.1 base score of 9.8 and an Important severity rating.
The patch points to the RDP clipboard virtual channel, specifically the proxy objects used for CLIPRDR file-content transfers. The vulnerable implementation could leave a live proxy object attached to a manager that was already terminating. The fix adds a second, authoritative lifetime-tracking collection and synchronizes teardown of the manager back-pointer.
This is a static binary analysis. It identifies the patched lifetime mechanism and its protocol-facing call graph, but it does not include a crash, a working trigger, or dynamic proof of the pre-authentication ordering.
Advisory facts
| Field | Value |
|---|---|
| CVE | CVE-2026-56190 |
| Component | Windows RDP, rdpcorets.dll |
| Impact | Remote code execution |
| Weakness | CWE-908, Use of Uninitialized Resource |
| CVSS | 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| Microsoft severity | Important |
| Publicly disclosed / exploited | No / No at publication |
| Release | July 14, 2026 |
Microsoft’s FAQ says exploitation requires specially crafted RDP traffic sent to a system where Network Level Authentication (NLA) is disabled. It describes interaction with improperly initialized memory, memory corruption, and attacker influence over memory writes.
Diff pair and target substitution
The requested target was Windows Server 2022, build train 20348. Winbindex did not
index rdpcorets.dll for that train, so I used the affected and indexed Windows 11
24H2 build train 26100 to bracket the fix:
| Vulnerable-side binary | Fixed binary | |
|---|---|---|
| Version | 10.0.26100.8737 |
10.0.26100.8875 |
| Update | KB5095093, June 2026 preview | KB5101650, July 2026 security update |
| SHA-256 | 95180187876b5da66a059203fc50fd186d2a160893ed6bc45992044157f514f2 |
b93d385b7b2a7e236be780e682f19ed1b2d17944a2a56052ea0bcc213e0f5967 |
The equivalent Server 2022 update listed by MSRC is KB5099540, build
10.0.20348.5386. The mechanism below is therefore demonstrated directly on Windows
11 24H2, not by diffing the Server 2022 binaries. Confirming the same machine-code
change on the 20348 train remains useful follow-up work.
Signal in the diff
The raw diff contained a large amount of offset noise. CProxyStream and related
objects grew by 0x28 bytes, shifting fields across otherwise unchanged methods. That
also confused function matching: several fixed-only methods appeared in the wrong
added/deleted direction until I enumerated functions in each Ghidra program.
The security-relevant additions are concentrated in one lifetime cluster:
CProxyStreamManager::RegisterProxyStreamObjectForTrackingCProxyStreamManager::UnregisterProxyStreamObjectForTrackingCProxyStreamManager::NotifyProxyStreamObjectsOnMgrTermination- matching tracking and notification methods for
CProxyDataObjectManager - a per-stream critical section initialized by
CProxyStream::CProxyStream - feature-gated branches using
Feature_3624641849
The new WIL feature appears only in the fixed binary and gates the stream and data object lifecycle changes.
Vulnerable code
The snippets below are transcribed excerpts from Ghidra decompiler output. Statements
are preserved from the analyzed binary and elisions are marked, but these are not
Microsoft’s source code or complete functions. Types, local names, templates, and some
this adjustments are decompiler recovery artifacts.
In the vulnerable CProxyStreamManager::Terminate at 0x1800c9540, teardown walks an
ID-indexed SmartArray beginning at index 1. Only objects found in those slots receive
OnManagerTerminating:
EnterCriticalSection((LPCRITICAL_SECTION)(this + 0x30));
iVar1 = *(int *)(this + 0x6c);
iVar5 = 1;
if (1 < iVar1) {
do {
local_res8 = (CProxyStream *)0x0;
iVar4 = SmartArray<CProxyStream,long>::GetAt
((SmartArray<CProxyStream,long> *)(this + 0x68),iVar5,&local_res8);
pCVar2 = local_res8;
if ((iVar4 != 0) && (local_res8 != (CProxyStream *)0x0)) {
CProxyStream::OnManagerTerminating(local_res8);
(**(code **)(**(longlong **)(pCVar2 + 0x20) + 0x10))();
iVar4 = SmartArray<CProxyStream,long>::AddAt
((SmartArray<CProxyStream,long> *)(this + 0x68),iVar5,
(CProxyStream *)0x0);
}
iVar5 = iVar5 + 1;
} while (iVar5 < iVar1);
}
LeaveCriticalSection((LPCRITICAL_SECTION)(this + 0x30));
*(undefined8 *)(this + 0x60) = 0;
*(uint *)(this + 0x14) = *(uint *)(this + 0x14) | 4;
The vulnerable CProxyStream::OnManagerTerminating at 0x1800c8354 clears its manager
back-pointer at offset 0x38 without taking a stream-local lock:
*(undefined8 *)(this + 0x38) = 0;
return;
That design assumes the ID array is a complete inventory of live streams at teardown. The fixed binary indicates that Microsoft no longer relied on that assumption: it adds a separate collection populated during object creation rather than waiting for an ID-array slot to be acquired.
Exact fix
The fixed CProxyStreamManager::Terminate at 0x1800ca5c0 calls a new notifier before
walking the legacy array. When the feature is enabled, the old per-slot notification is
skipped because the exhaustive pass has already notified every tracked object:
EnterCriticalSection((LPCRITICAL_SECTION)(this + 0x30));
bVar2 = wil::details::FeatureImpl<__WilFeatureTraits_Feature_3624641849>::
__private_IsEnabled(/* ... */);
if (bVar2) {
NotifyProxyStreamObjectsOnMgrTermination(this + -8);
}
/* legacy SmartArray walk */
if ((iVar4 != 0) && (local_res8 != (CProxyStream *)0x0)) {
bVar2 = wil::details::FeatureImpl<__WilFeatureTraits_Feature_3624641849>::
__private_IsEnabled(/* ... */);
if (!bVar2) {
CProxyStream::OnManagerTerminating(this_00);
}
/* release and clear the legacy slot */
}
The new notifier at 0x1800c90e4 holds a dedicated manager lock, enumerates the new
DynArray, notifies each non-null stream, clears its slot, and finally resets the
logical count:
EnterCriticalSection((LPCRITICAL_SECTION)(this + 0x88));
iVar1 = *(int *)(this + 0xb4);
uVar2 = 0;
if (0 < iVar1) {
lVar5 = 0;
do {
local_res8 = (CProxyDataObject *)0x0;
if ((uVar2 < *(uint *)(this + 0xb4)) &&
(this_00 = *(CProxyStream **)(*(longlong *)(this + 0xb8) + lVar5 * 8),
this_00 != (CProxyStream *)0x0)) {
CProxyStream::OnManagerTerminating(this_00);
iVar3 = DynArray<CProxyDataObject*___ptr64,unsigned_long>::AddAt
((DynArray<CProxyDataObject*___ptr64,unsigned_long> *)
(this + 0xb0),uVar2,&local_res8);
}
uVar2 = uVar2 + 1;
lVar5 = lVar5 + 1;
} while (lVar5 < iVar1);
}
*(undefined4 *)(this + 0xb4) = 0;
LeaveCriticalSection((LPCRITICAL_SECTION)(this + 0x88));
The template type recovered as CProxyDataObject* in this function is inconsistent
with the symbol and accessed values; this is a decompiler typing artifact. The method
is the stream notifier and calls CProxyStream::OnManagerTerminating.
Registration and removal both modify the tracking array under the same manager lock:
/* RegisterProxyStreamObjectForTracking, 0x1800c98d4 */
EnterCriticalSection((LPCRITICAL_SECTION)(this + 0x88));
iVar1 = DynArray<CProxyDataObject*___ptr64,unsigned_long>::AddAt
((DynArray<CProxyDataObject*___ptr64,unsigned_long> *)(this + 0xb0),
param_2,(CProxyDataObject **)&local_res10);
LeaveCriticalSection((LPCRITICAL_SECTION)(this + 0x88));
/* UnregisterProxyStreamObjectForTracking, 0x1800ca740 */
local_res8 = (CProxyDataObject *)0x0;
EnterCriticalSection((LPCRITICAL_SECTION)(this + 0x88));
iVar1 = DynArray<CProxyDataObject*___ptr64,unsigned_long>::AddAt
((DynArray<CProxyDataObject*___ptr64,unsigned_long> *)(this + 0xb0),
param_1,&local_res8);
LeaveCriticalSection((LPCRITICAL_SECTION)(this + 0x88));
Finally, the fixed constructor initializes a critical section at stream offset 0x40,
and the fixed OnManagerTerminating at 0x1800c9230 uses it around the back-pointer
clear:
/* CProxyStream::CProxyStream, 0x1800c7ba0 */
if (bVar1) {
InitializeCriticalSection((LPCRITICAL_SECTION)(this + 0x40));
}
/* CProxyStream::OnManagerTerminating, 0x1800c9230 */
if (bVar1) {
EnterCriticalSection((LPCRITICAL_SECTION)(this + 0x40));
*(undefined8 *)(this + 0x38) = 0;
LeaveCriticalSection((LPCRITICAL_SECTION)(this + 0x40));
}
else {
*(undefined8 *)(this + 0x38) = 0;
}
The same pattern is mirrored for CProxyDataObject and its manager.
Root cause
CProxyStream objects proxy file-content reads over CLIPRDR and keep a back-reference
to their manager. In the vulnerable build, manager termination notified only streams
currently present in the manager’s ID-indexed SmartArray. The patch is consistent
with a live stream being able to exist outside that collection because creation,
external COM lifetime, and acquisition of an ID-array slot are distinct events; the
static analysis does not prove the concrete ordering that creates this state.
If teardown missed such a stream, its manager back-pointer was not cleared. A later
operation could then use state belonging to a manager whose channel resources had
already been torn down. The new stream-local lock specifically coordinates
OnManagerTerminating with the destructor’s snapshot and reference of that manager
pointer before unregistration; the analyzed read path does not acquire this lock.
The patch closes both gaps:
- Every stream is registered at creation and unregistered at destruction in a separate birth-to-death tracking array.
- Manager termination walks that authoritative array and notifies every live stream.
- Each stream coordinates manager notification with destructor-side manager snapshot/unregistration through a dedicated critical section.
- The same lifecycle discipline is applied to clipboard data objects.
This is consistent with Microsoft’s CWE-908 classification: a proxy can otherwise use a manager resource after that resource has entered an invalid post-termination state.
Static reachability evidence
Cross-references in the vulnerable and fixed Ghidra programs support the mechanism:
- The vulnerable
OnManagerTerminatinghas one caller, the legacy manager teardown. The fixed build adds the exhaustive notifier as a second caller. CProxyStream::CreateInstancecalls the new registration method, while the destructor calls unregistration. ID-array acquisition occurs separately throughAcquireProxyStreamandSendFileContentsRequest.- Peer-controlled file-content response bytes flow through
CClipBase::OnFileContentsResponse,OnStreamDataReceived, andCProxyStream::FillReceiveBuffer. - Teardown flows from
CClipBase::TerminatetoCProxyStreamManager::Terminateand, in the fixed build, to the new notifier.
These facts establish the changed lifetime invariant and protocol-facing data path. They do not establish a concrete packet sequence that wins the teardown/use ordering.
Confidence and remaining work
Confidence in the patched mechanism’s CVE mapping is high: the diff brackets Microsoft’s fixed build, the new WIL feature gates only the affected proxy-object lifecycle, and the tracking-plus-locking change maps directly to the advisory’s uninitialized-resource description. Confidence in the exact packet ordering and failure sequence is lower because neither has been reproduced dynamically.
Dynamic validation is still pending. The useful next checks are:
- reproduce a fault on
26100.8737and a negative result on26100.8875; - confirm the ordering is reachable before logon with NLA disabled;
- extract and diff the Server 2022
20348pair directly.
No trigger or exploit code is included here.