Skip to navigation Skip to main content Skip to footer

Technical Advisory: Microsoft Windows – usbmidi2.sys MIDI-1.0 wMaxPacketSize pool overflow

By Alex Plaskett

09 October 2026

Vendor: Microsoft    
Vendor URL: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-69720
Versions affected: See MSRC advisory
Systems Affected: Microsoft Windows
Author: Alex Plaskett 
Risk: Medium – Local Elevation of Privilege
Methodology: Agentic Vulnerability Identification and Exploitation

 

1. Summary

USBMIDI2DriverIoWrite() accumulates outbound MIDI data into a per-device kernel buffer DeviceWriteBuffer before flushing it to the USB OUT pipe.

In the USB-MIDI-1.0 conversion path, the "buffer full → flush and reset" decision uses an exact-equality comparison against the OUT endpoint's maximum packet size as follows:

if ((pDeviceContext->DeviceWriteBufferIndex * sizeof(UINT32)) == pDeviceContext->MidiOutMaxSize)   // Device.cpp:2983

MidiOutMaxSize is copied verbatim from the device-supplied OUT-endpoint wMaxPacketSize with no validation. If that value is not a multiple of 4, the running write index (`index * 4`) steps over MidiOutMaxSize and the equality never holds, so the buffer is never flushed, and the index is never reset. 

The store then walks linearly past the end of the fixed-size NonPagedPoolNx allocation with attacker-controlled MIDI content. The sibling USB-MIDI-2.0 branch in the same function uses the correct `<=` bound check and resets the index on (re)allocation; the 1.0 path does neither.

((PUINT32)pDeviceContext->DeviceWriteBuffer)[pDeviceContext->DeviceWriteBufferIndex++] = umpWritePacket.umpData.umpWords[count];  // Device.cpp:2980


2. Vulnerability Details

Code for this driver is available at:

github.com/microsoft/MIDI

The vulnerabilities were identified at src/api/Drivers/USBMIDI2/Driver/ (reviewed at commit 766b7e581a8d832adca83648a36a8cda8058c02b):

Unvalidated packet size (root input) — `Device.cpp:1333-1338`

else if (WdfUsbTargetPipeIsOutEndpoint(pipe))
{
    pDeviceContext->MidiOutPipe     = pipe;
    pDeviceContext->MidiOutPipeType = pipeInfo.PipeType;
    pDeviceContext->MidiOutMaxSize  = pipeInfo.MaximumPacketSize;   // device-controlled, never validated
}

MidiOutMaxSize is ULONG. Nothing rounds it, masks it, or constrains the OUT pipe's transfer type.

A USB interrupt OUT endpoint may legally declare any wMaxPacketSize 1–64 at full speed, including non-multiples of 4 such as 9, 13 or 62. (A *bulk* endpoint cannot — full-speed bulk wMaxPacketSize ∈ {8,16,32,64} — which is why the interrupt transfer type is the delivery vector.)

The defective flush/reset logic — `Device.cpp:2937-3001`

if (umpWritePacket.wordCount && (pDeviceContext->UsbOutMask & 0x0001 << cbl_num))
{
    for (int count = 0; count < umpWritePacket.wordCount; count++)
    {
        if (!pDeviceContext->DeviceWriteBuffer)
        {
            // ... WdfMemoryCreate(NonPagedPoolNx, USBMIDI_POOLTAG, MidiOutMaxSize, ...)   // :2966
            pDeviceContext->DeviceWriteBuffer = (PUCHAR)WdfMemoryGetBuffer(...);
            // NOTE: DeviceWriteBufferIndex is NOT reset here (the MIDI-2.0 path does — :3041)
        }

        // OOB write once the index passes the allocation:
        ((PUINT32)pDeviceContext->DeviceWriteBuffer)[pDeviceContext->DeviceWriteBufferIndex++]
            = umpWritePacket.umpData.umpWords[count];                                        // :2980

        // BUG: exact equality; never true when MidiOutMaxSize % 4 != 0
        if ((pDeviceContext->DeviceWriteBufferIndex * sizeof(UINT32)) == pDeviceContext->MidiOutMaxSize)  // :2983
        {
            USBMIDI2DriverSendToUSB(...);  // flush
            pDeviceContext->DeviceWriteBuffer = NULL;
            pDeviceContext->DeviceWriteBufferIndex = 0;
        }
    }
}

DeviceWriteBufferIndex is size_t in the device context, so it persists across packets and calls.

Why accumulation is unbounded — Device.cpp:3007` /`StreamEngine.cpp:243

The only other flush is the end-of-call leftover flush, gated on !isMoreData:

if (pDeviceContext->DeviceWriteBufferIndex && !isMoreData) { /* flush + reset */ }   // :3007

The MidiOut worker drives the function once per UMP packet drained from the user-mapped cyclic buffer:

USBMIDI2DriverIoWrite(..., header->ByteCount, (finalReadPosition != midiOutWritePosition) ? TRUE : FALSE);  // StreamEngine.cpp:243

isMoreData is TRUE whenever the consumer has not caught up to the producer's write position. A local process that keeps the cyclic buffer non-empty therefore keeps isMoreData == TRUE for every packet but the last, so neither the == flush (2983) nor the leftover flush (3007) ever fires within the drain, and DeviceWriteBufferIndex increments monotonically.

Contrast — the correct MIDI-2.0 branch (Device.cpp:3028-3116)

pDeviceContext->DeviceWriteBufferIndex = 0;                                                // :3041 (reset on alloc)
...
if ((thisTransferSize + pDeviceContext->DeviceWriteBufferIndex*sizeof(UINT32)) <= pDeviceContext->MidiOutMaxSize)  // :3083 (<=)

 

3. Attacker Control and Overflow

  • Allocation: MidiOutMaxSize bytes; NonPagedPoolNx rounds a 13-byte request up to a 16-byte block (or to an LFH bucket). The first store fully outside a 16-byte block is at DeviceWriteBufferIndex == 4 (byte offsets 16–19) — i.e. after ~5 contributing UMP packets.
  • Granularity: 4-byte-aligned, monotonically increasing offset; a clean contiguous linear overwrite.
  • Content (per overwritten word, MIDI-1.0 channel-voice case, `Device.cpp:2778-2784`): bytes 1–3 are copied verbatim from the attacker's UMP word; byte 0 is (group<<4) | CIN (group fully chosen, CIN constrained to the channel-voice range). ~24 of 32 bits are fully attacker-controlled
  • Extent: unbounded — the index never resets, so a single sustained drain writes well past the allocation (the reproduction below sprayed ≈3,275 words).

 
4. Exploit Information

A fully working exploit was developed, however, the details are not included in this advisory.


5. Vendor Communication

19th June 2026 – Issue reported to Microsoft
8th September 2026 – Microsoft releases update

 

6. About NCC Group


NCC Group is a global expert in cybersecurity and risk mitigation, working with businesses to protect their brand, value and reputation against the ever-evolving threat landscape. With our knowledge, experience and global footprint, we are best placed to help businesses identify, assess, mitigate & respond to the risks they face. We are passionate about making the Internet safer and revolutionizing the way in which organizations think about cybersecurity.