Security Research
CVE-2026-50321. How A Normal WinUSB Feature Exposed A Pipe-Lifetime Race
CVE-2026-50321: a WinUSB pipe-context race in winusb.sys triggers reliable BSODs and exposes kernel UAF primitives behind an EoP fix.

USB interfaces can declare multiple alternate settings, each with its own set of endpoints. Switching between them forces WinUSB to tear down the old pipe contexts and build new ones. That teardown is normal and should rebuild pipe state. In winusb.sys, that switch handed a freed kernel object to a parallel IOCTL path instead. The bug was found using the Delphos Labs platform and reported to Microsoft via MSRC.
Microsoft acknowledged the vulnerability, classified it as a Windows USB Driver elevation-of-privilege, and assigned CVE-2026-50321 to it. Shortly after, on July 14, 2026, Microsoft released a fix which we manually verified. The bug sits in winusb.sys, where WinUSB_FreePipe frees a pipe context and only nulls the shared pointer afterward. Other IOCTL paths can overlap that teardown, read the stale pointer from the pipe array, and dereference freed kernel memory.
The advisory is rated Important. Microsoft's CVSS is 7.8 with the vector below, and I use that score here because Microsoft is the assigning CNA and the source of the advisory. NVD also analyzed the issue and lists the same Microsoft vector as its secondary score, while its own primary CVSS entry rates the bug 7.0 with AC:H.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H/E:U/RL:O/RC:CWhy does a generic Windows USB driver matter?
WinUSB is the generic Windows driver that ships with the OS and is used by many vendor-specific USB devices. It is broad by design: hardware vendors can ship devices without writing a custom kernel driver, and user-mode applications can communicate with those devices through the WinUSB API.
That makes lifetime bugs in winusb.sys operationally relevant. A device does not need a strange endpoint type, a malformed descriptor, or an extreme packet size to reach this path. It only needs an interface with two or more alternate settings so that WinUsb_SetCurrentAlternateSetting() forces WinUSB to tear down and rebuild pipe contexts.
In the tested setup, exploitation required a controlled WinUSB device and a local user-mode process that opened the device and raced the relevant WinUSB APIs. In practice, that could mean a malicious USB gadget paired with local code execution, or a lower-privileged local process abusing an already attached compatible WinUSB device.
The installed population is every supported Windows build (even ARM-builds). The reachable population is the subset with a compatible device attached and an attacker already running local code.
The interesting part of CVE-2026-50321 is how ordinary the inputs are.
The confirmed impact is denial of service. The deeper issue is that the freed object is a 0xF0-byte NonPagedPool allocation whose fields are later read and written through GetPipePolicy and SetPipePolicy. Without Special Pool, the freed allocation remains mapped and can be reclaimed, which opens a path toward controlled kernel pool corruption.
How did Delphos find and validate it?
Delphos surfaced the bug during an automated review of Windows kernel drivers. The initial signal was simple and specific: the pipe teardown path released object memory while a shared pointer to that object remained reachable through another structure.
That was only the starting point. After triage, Delphos reconstructed the relevant WinUSB paths, identified the parallel device-control dispatch, and traced which IOCTL handlers could still access the same pipe context while alternate-setting changes were freeing it. The same approach surfaced the RxGK RESPONSE path in DirtyCBC's page-cache poisoning in the Linux kernel. An invariant was proposed and ranked automatically, then confirmed and proven exploitable by hand.
I then validated the claim manually. I built a WinUSB test device with multiple alternate settings, wrote host-side PoCs to race the relevant APIs, enabled Driver Verifier, and reproduced the crashes. The dumps are what turned the initial static finding into a reportable vulnerability.
Delphos found the vulnerability. The proof required a USB device, so I built one to prove it was exploitable.
Writeup and POC
The full technical writeup and proof of concept are in our GitHub repository: CVE-2026-50321 writeup and PoC.
The repository contains what is needed to reproduce and review the issue:
- README.md: root cause, affected code path, the race window, confirmed crash classes, and patch analysis.
- trigger.py: the primary race harness.
- double_free_poc.py: the cleanup and SelectAltSetting concurrent teardown case.
- firmware/: Teensy 3.2 firmware for the WinUSB test device.
Reproduction requires a WinUSB device exposing two or more alternate settings and Driver Verifier Special Pool enabled for winusb.sys. Microsoft shipped the fix on July 14, 2026. The PoCs are published for defensive validation and research, and should be run only on systems you own or are explicitly authorized to assess.
Which Windows builds are affected and which are fixed?
The July 2026 Microsoft advisory lists affected Windows client and server releases across supported Windows 10, Windows 11, and Windows Server versions. At the original publication, Microsoft marked public disclosure as No, exploitation in the wild as No, and exploitation assessment as Exploitation Unlikely.
The fixed build thresholds published through MSRC and NVD include:
Product family | Fixed build threshold |
Windows 10 1607 / Windows Server 2016 | 10.0.14393.9339 |
Windows 10 1809 / Windows Server 2019 | 10.0.17763.9020 |
Windows 10 21H2 | 10.0.19044.7548 |
Windows 10 22H2 | 10.0.19045.7548 |
Windows 11 24H2 | 10.0.26100.8875 |
Windows 11 25H2 | 10.0.26200.8875 |
Windows 11 26H1 | 10.0.28000.2525 |
Windows Server 2012 | 6.2.9200.26226 |
Windows Server 2012 R2 | 6.3.9600.23291 |
Windows Server 2022 | 10.0.20348.5386 |
Windows Server 2025 | 10.0.26100.33158 |
The vulnerable component is Microsoft's Windows USB driver stack, specifically the WinUSB kernel driver path analyzed here:
Component: winusb.sys
Tested version: 10.0.26100.1150
Tested OS: Windows 11 build 26200.7840, kernel 26100
Architecture: x64
Bug class: Use-after-free / concurrent pipe teardown
CVE: CVE-2026-50321
MSRC title: Windows USB Driver Elevation of Privilege Vulnerability
MSRC severity: Important
MSRC weakness: CWE-362, CWE-416The USB device used for testing was a Teensy 3.2 running custom firmware. It presents a vendor-specific interface that binds to WinUSB through a Microsoft OS 1.0 compatible ID descriptor:
VID: 0x1209
PID: 0x0001
Compatible ID: WINUSB
Interface 0: two alternate settings
Alt 0: bulk OUT EP1 and bulk IN EP2, max packet size 64
Alt 1: bulk OUT EP1 and bulk IN EP2, max packet size 8The descriptors are ordinary. They simply create the minimum interface shape needed for WinUSB to allocate pipe contexts for alternate settings and then free and reinitialize them during interface switching.
A normal WinUSB interface with two alternate settings is enough to reach the vulnerable teardown path.
Where does the pipe context get freed too early?
The bug is in the pipe-context lifecycle.
When WinUSB changes an interface's alternate setting, it tears down the old pipes and initializes the new ones. The teardown path calls WinUSB_FreePipe for each pipe context. Static analysis showed the critical ordering:
WinUSB_FreePipe:
...
call ExFreePoolWithTag ; pipe_ctx freed
; a small instruction window remains
mov [pipe_array + index], 0 ; pointer nulled laterBetween the free and the null write, the interface pipe array still contains a pointer to freed memory. That would already be dangerous in a serialized path. It becomes materially worse because WinUSB dispatches the relevant IOCTLs concurrently.
WinUSB_AddDevice creates the main device-control queue as a WDF parallel queue, and the device uses WdfSynchronizationScopeNone. In practice, multiple user-mode WinUSB calls can execute at the same time without WDF serializing access to the pipe array.
The race is straightforward:
Thread A: WinUsb_SetCurrentAlternateSetting()
-> WinUSB_SelectAltSetting
-> WinUSB_SetInterface
-> WinUSB_FreePipe
-> ExFreePoolWithTag(pipe_ctx)
-> pipe_array[index] = NULL
Thread B: WinUsb_GetPipePolicy() or WinUsb_SetPipePolicy()
-> read pipe_ctx from pipe_array[index]
-> check pipe_ctx != NULL
-> KeAcquireSpinLock(pipe_ctx + 0x28)
-> read or write policy fieldsIf Thread B reads the pointer after Thread A frees the object but before Thread A nulls the array slot, Thread B uses a freed pipe context. With Driver Verifier Special Pool enabled, the freed allocation's page is unmapped. GetPipePolicy and SetPipePolicy then call KeAcquireSpinLock on pipe_ctx + 0x28, causing a page fault at DISPATCH_LEVEL and an immediate 0x0A bugcheck.
The bug is not in the “free” itself. The bug is that the freed object remains reachable long enough for another path to trust it.
Why do policy IOCTLs make the race easy to hit?
Not every WinUSB operation is equally useful as a racing victim.
ReadPipe and WritePipe interact with WDF I/O queues. SelectAltSetting checks whether pipe queues are busy and can return STATUS_DEVICE_BUSY, which makes those operations poor race partners.
GetPipePolicy and SetPipePolicy are different. They run inline in the device-control path. They read the pipe context, acquire the pipe-context spinlock, and read or modify policy fields without queuing pipe I/O.
That means policy calls do not trip the busy check. The PoC can run policy threads at high throughput while other threads continuously switch alternate settings. In testing, that produced millions of pipe-context accesses while repeatedly freeing and reallocating the same class of object.
Policy IOCTLs are reliable race partners because they stay off the pipe queues that block alternate-setting teardowns.
What crash classes were confirmed?
The main PoC, trigger.py, launches 16 threads. Eight call WinUsb_GetPipePolicy() in tight loops. Four call WinUsb_SetPipePolicy() in tight loops. Four toggle WinUsb_SetCurrentAlternateSetting() between alternate settings 0 and 1.
With Special Pool enabled for winusb.sys, it produces reliable IRQL_NOT_LESS_OR_EQUAL crashes in WinUSB_GetPipePolicy and WinUSB_SetPipePolicy.
The supplementary PoCs explored additional trigger and victim combinations:
Trigger | Victim | Result |
SelectAltSetting | SetPipePolicy | Confirmed BSOD |
SelectAltSetting | GetPipePolicy | Confirmed BSOD |
ResetDefaults | SetPipePolicy | Confirmed BSOD |
ResetDefaults | GetPipePolicy | Confirmed BSOD |
Cleanup + SelectAltSetting | concurrent teardown | Confirmed BSOD |
The dumps showed a consistent fault pattern. For the UAF crashes, the faulting address was the freed pipe context plus 0x28, the location of the embedded spinlock. The crash occurs inside nt!KeAcquireSpinLockRaiseToDpc, after IRQL has already been raised to DISPATCH_LEVEL.
Representative bugcheck:
IRQL_NOT_LESS_OR_EQUAL (0x0A)
Arg1: <freed_pipe_ctx + 0x28>
Arg2: 0x2 ; DISPATCH_LEVEL
Arg3: 0x1 ; write operation
Arg4: nt!KeAcquireSpinLockRaiseToDpc+0x4eRepresentative stack:
nt!KeBugCheckEx
nt!KiBugCheckDispatch
nt!KiPageFault
nt!KeAcquireSpinLockRaiseToDpc
nt!VerifierKeAcquireSpinLockRaiseToDpc
WinUSB!WinUSB_SetPipePolicy
WinUSB!WinUSB_DeviceControl
Wdf01000!FxIoQueue::DispatchRequestToDriver
...
nt!NtDeviceIoControlFileThe extra double_free_poc.py demonstrates a second crash class using the same lifetime flaw. It races WinUsb_SetCurrentAlternateSetting(0) against CloseHandle(device_handle), which enters WinUSB_Cleanup, ResetDefaults, and then FreePipe on the same path. WinUSB_Cleanup takes a FastMutex, but SelectAltSetting does not take that same mutex, and cleanup does not purge the default parallel IOCTL queue before it tears the pipes down.
The result was a WDF verifier failure rather than the same 0x0A spinlock fault:
WDF_VIOLATION (0x10D)
Arg1: 0x0AIn that path, cleanup passed a stale WDF queue handle read from freed pipe-context memory into WdfIoQueuePurgeSynchronously.
This is not one crash with one trigger. It is the same lifetime flaw expressing itself through multiple teardown paths.
What does exploitability look like without Driver Verifier?
The submitted PoC demonstrates reliable denial of service under Driver Verifier. Microsoft classified the vulnerability as Elevation of Privilege because the more important kernel primitive appears when Verifier is not forcing an immediate crash.
The pipe context is allocated from NonPagedPool:
Size: 0xF0 bytes
Tag: WUSBWith Special Pool disabled, the freed memory remains mapped and can be reclaimed by another kernel allocation. If an attacker can reclaim that freed 0xF0-byte slot with controlled or useful data, the policy accessors become interesting. GetPipePolicy can read fields at fixed offsets from the stale or reused object and return values to user mode. SetPipePolicy can write user-controlled values to fixed offsets. Both paths still acquire a spinlock at +0x28 before touching policy fields.
Relevant offsets include:
Offset | Field / use | Access |
+0x14 | SHORT_PACKET_TERMINATE | read/write |
+0x18 | PIPE_TRANSFER_TIMEOUT | read/write |
+0x1F | RAW_IO | read/write |
+0x28 | KSPIN_LOCK | acquired by policy paths |
+0x70 | MAXIMUM_TRANSFER_SIZE | read |
I did not build a weaponized privilege-escalation exploit. The important point is simpler: this is not a verifier-only crash. Verifier makes the UAF obvious by unmapped freed memory. Without it, the same sequence becomes a stale-object access and a pool-corruption primitive.
Driver Verifier turns the bug into a crash. Without Verifier, the same bug becomes a quieter kernel memory corruption surface.
What changed in the July winusb.sys patch?
ReadPipeCompletion and WritePipeCompletion looked like plausible victims at first, because completion routines also use pipe context state. Static analysis and testing showed why they were not practical for this bug: SelectAltSetting stops and drains relevant I/O targets before FreePipe executes. Continuous read flooding mostly caused ERROR_BUSY, which prevented alternate-setting switches from reaching the teardown window.
That negative result mattered. It pushed the research toward policy IOCTLs, which do not interact with pipe queues and therefore do not block alternate-setting changes.
After Microsoft released the July 2026 fix, I pulled the patched x64 winusb.sys for Windows 11 build 10.0.26100.8875 from Microsoft's symbol server and compared it against the vulnerable 10.0.26100.1150 build.
Vulnerable x64 SHA256: 11a59886ac977ec4890ff984a20cb1cbf092b4e6f11e719d7cfd92c658472fcb
Patched x64 SHA256: 6776e4a2476fb2df59a4629888a6e24460e14fa927dc3e04a398a27561771cc2The patched x64 binary does not appear to change the core WinUSB_FreePipe ordering. WinUSB_FreePipe still reads the pipe pointer from the interface array, deletes associated WDF objects, calls ExFreePoolWithTag(pipe_ctx, 0), and only then clears the array slot.
The more meaningful change is around reachability. WinUSB_Cleanup now stops the default device-control queue under the device_ctx+0x150 fast mutex before walking pipe queues and calling ResetDefaults, then starts the queues again afterward. That directly addresses the cleanup race, where a close path could previously run alongside SelectAltSetting and operate on the same pipe context.
The policy accessor functions still have the same dangerous shape if they can overlap the teardown. They load pipe_ctx from the interface pipe array, check it for NULL, and acquire KeAcquireSpinLockRaiseToDpc(pipe_ctx+0x28) before reading or writing policy fields. I did not see a new refcount, per-pipe lifetime lock, or interlocked reference inside GetPipePolicy or SetPipePolicy themselves.
My read is that Microsoft fixed the reported issue primarily by queue gating, not by making FreePipe independently safe. That can be a valid fix if the queue stop fully drains the relevant in-flight callbacks before pipe teardown. It also means the safety of the remaining free-before-null sequence now depends on those dispatch guarantees.
The patch appears to reduce reachability of the stale pointer rather than eliminate the free-before-null pattern itself.
Timeline
The disclosure timeline is straightforward:
2026-05-06 Vulnerability discovered during winusb.sys reverse engineering
2026-05-07 Initial SelectAltSetting + SetPipePolicy crashes confirmed
2026-05-07 Original PoC package and MSRC submission prepared
2026-05-08 Additional ResetDefaults and GetPipePolicy crash paths confirmed
2026-05-09 Cleanup + SelectAltSetting concurrent teardown crash confirmed
2026-05-26 Microsoft confirmed the bug
2026-06-03 Microsoft stated they were working on a fix
2026-07-08 Microsoft assigned CVE-2026-50321
2026-07-14 Microsoft advisory and official fix released
2026-08-11 Patched x64 winusb.sys reviewed from Microsoft symbol serverWhat should defenders take from a bug this ordinary?
What matters most is the broader lesson. Generic OS drivers are attack surface, not plumbing. WinUSB exists to make device development easier, which means a large amount of hardware can reach the same kernel path. In this case, a small race in pipe teardown was enough to turn a custom USB device and a user-mode stress script into reliable kernel memory corruption.
This is fundamentally a software assurance problem. It is the one Delphos Labs works on every day.
A normal driver feature exercised with normal user-mode APIs was enough to expose kernel memory corruption. That is exactly why compiled code has to be treated as ground truth.
