microsoft / microsoft/MIDI

[BUG]: A stalled driver wedges WinMM MIDI open/close machine-wide — unbounded RPC and global locks give it unlimited blast radius

Open
#1,195 0 comments 0 reactions 1 assignee View on GitHub

@Psychlist1972 is already working on this.

Since Sep 11, 2026.

needs-investigation :mag:
Dominant language
C++
Stars
679
Forks
72
Avg merge
4h 51m
Merged PRs (30d)
59

Description

This is not a proven bug yet. It's from some static code analysis and very limited information from a customer app crash dump. However, I did not want to lose the info, so logging it here. Needs investigation. Any other agents looking at this need to understand the proper status of this and not treat it as a proven cause/effect.

Everything after the break via GitHub Copilot (Claude Opus 5), including the possible directions it recommended (not really part of a bug report, but included here so they don't get lost)

Pete
Microsoft


Component: wdmaud2.drv (MidiSrvPorts.cpp, MidiSrvPort.cpp), MidiSrvTransportLib
(MidiSrvTransport.cpp), midisrv (MidiClientManager.cpp)
Found: 2026-09-11, from four Ableton Live 11.3.43 crash report packs sent by a user.

Status: mechanism identified by code inspection; the field failure is consistent with it but
not proven to be caused by it.

Summary

midiInOpen / midiOutOpen / midiInClose / midiOutClose have no upper bound. Every one of
them takes a process-wide lock and then makes an RPC to midisrv with no call timeout, and the
service handles both requests under a single exclusive lock. If any one open or close stops
making progress, three things follow in order:

  1. that call never returns,
  2. every subsequent WinMM MIDI open or close in the same process blocks behind the
    process-wide CMidiPorts::m_Lock,
  3. every subsequent open or close in every other process blocks behind the service's
    exclusive m_ClientManagerLock.

There is no timeout at any layer, so the failure is permanent rather than slow. The application
cannot be closed, because it is blocked inside its own shutdown path.

What is established by code inspection

Line numbers are from the current working tree and drift (#1169 cites 1122/1204 for the same
two functions).

Client — the lock is held across the RPC:

  • g_MidiPorts is a per-process singleton — winmmdrv.cpp:55. One wil::critical_section m_Lock (MidiSrvPorts.h:67) covers every port in the process.
  • CMidiPorts::Open takes m_Lock at MidiSrvPorts.cpp:1162, then calls
    MakeAndInitialize<CMidiPort>CMidiPort::RuntimeClassInitializemidiSrv->Initialize
    (MidiSrvPort.cpp:90) → MidiSrvCreateClient. The lock is held for the whole chain.
  • CMidiPorts::Close takes m_Lock at MidiSrvPorts.cpp:1244, then calls
    port->second->Shutdown()CMidiPort::Shutdown (MidiSrvPort.cpp:98) →
    m_MidisrvTransport->Shutdown() (:132) → MidiSrvDestroyClient. Same.
  • By contrast CMidiPorts::GetPort (MidiSrvPorts.cpp:1133) deliberately copies the port
    reference out "so we don't have to hold up all other messages while processing this one".
    The message path was given that treatment; open and close were not.

No timeout anywhere:

  • MidiSrvCreateClient (MidiSrvTransport.cpp:153) and MidiSrvDestroyClient (:253) are
    plain blocking RPC calls inside RpcTryExcept. There is no RPC_C_OPT_CALL_TIMEOUT and no
    RpcBindingSetOption call anywhere in MidiSrvTransportLib. RpcTryExcept catches faults,
    not hangs.

Service — one exclusive lock gates all clients:

  • CreateMidiClient takes m_ClientManagerLock.lock_exclusive() at MidiClientManager.cpp:1133
    and holds it across GetEndpointAlias, GetMidiClient and device/transform pipe creation.
  • DestroyMidiClient (:1382) and DestroyMidiClientDeferredPipeShutdown (:1529) take the
    same lock exclusive.
  • Feature_Servicing_MIDI2VirtualDeviceRemovalDeadlock already moves pipe Shutdown() outside
    the lock on the destroy path, so the precedent for this shape of fix exists. CreateMidiClient
    still holds it exclusive across work that reaches into transports.

This is the amplifier that makes the failure cross-process: a create and a destroy for two
completely unrelated endpoints, from two unrelated applications, serialize against each other.

Field evidence (circumstantial)

Four crash packs from one machine, Windows 11 Pro 25H2, Live 11.3.43 (64-bit). Live is a WinMM
client (MidiIO: NMMESubsystem::...) and wdmaud2.drv is loaded in the process, so the new
API mode was in use. All times local, from Preferences\Log.txt.

Time Event
12:25:50 Live starts. MIDI EnumerateDevices takes 3.15 s (6–8 ms on every later run). Open succeeds in 837 ms. Runs normally for 85 minutes.
13:51:50 User quits → MidiDeviceManager: Start CloseAllActivatedDevices
13:51:51 MidiIO: NMMESubsystem::SCloseInputPort(): Unknown error (Arturia KeyStep 37) after 1032 ms
No End CloseAllActivatedDevices is ever logged. That process never exits.
13:56:56 New instance: Start OpenAllActivatedDevices, no End. Killed after ~12 min.
14:09:09 Same. Killed after ~10 min.
14:19:23 Same. Killed after ~3 min.
14:23:02 Same. Killed after ~1 min.

Two details worth keeping:

  • The first hung process never died. Every later instance logs
    copying crash file "..._2026_09_11__12_25_51.dmp" failed: used by another process — the same
    file, named by the 12:25:50 process's start time, still locked 33 minutes later. Live was stuck
    inside its own MIDI shutdown and could not be closed.
  • The blamed device changes every time. Preferences\MidiCheck.cfg records the port Live was
    opening when it died: SSL 12 MIDIMPC Studio mk2 PublicMPC Studio mk2 MIDI Port
    SSL 12 MIDI. Live disables the blamed port and hangs on the next one. A single bad device
    does not produce that pattern.

Live also re-enumerated MIDI twice, ten seconds apart, immediately before the final hang, so
device-change notifications were firing — the same trigger as #1169.

The trigger is a known class of problem; this issue is the blast radius

inMusic drivers (Akai MPC and family) are known to hang on surprise removal and account for a
large share of observed service hangs. A workaround is scheduled for November that abandons the
stuck thread and its resources and lets them leak.

That workaround addresses one trigger. It does not address what this issue is about: any
stall below the RPC — a different vendor's driver, a transport, a wedged USB device — produces
the identical machine-wide wedge, because nothing between the application and the driver imposes
a bound. This issue should not be closed as fixed by the surprise-removal workaround.

Two vendors are implicated in this single incident, which is why the scope is not device-specific:

Wedge Port being opened/closed (MidiCheck.cfg) Vendor
13:51:50 close set contained Arturia KeyStep 37, SSL 12 MIDI, SSL V-MIDI Ports 1–10 SSL — MPC not present
13:56:56 open SSL 12 MIDI SSL
14:09:09 open MPC Studio mk2 Public inMusic
14:19:23 open MPC Studio mk2 MIDI Port inMusic
14:23:02 open SSL 12 MIDI SSL

Supporting detail:

  • The first hang predates the MPC entirely. That session's port set was Arturia KeyStep 37,
    SSL 12 MIDI and ten SSL V-MIDI ports. All SSL MIDI ports had disappeared from Live's list by
    14:22:41, and ASIO: SSL ASIO Driver 1: Open failed (ASIOInit) 'ASE_NotPresent' confirms the
    SSL 12 was gone or unresponsive while its endpoints still enumerated.
  • The MPC Studio mk2 ports appear in Live's list at 14:22:52. The final wedge follows at
    14:23:02 — a close/enumerate/open cycle 9.8 seconds later, consistent with the device
    departing again.
Still not proven
  • No dump of midisrv or of a hung application was captured, so no thread is known to have been
    parked on either lock. The mechanism is inferred from the shape, not traced.
  • Which specific removal initiated each wedge is inferred from timing and from which port
    MidiCheck.cfg names, not observed directly.

Scope of this report

Devices involved 2 vendors across one incident: Solid State Logic SSL 12 (SSL USB audio driver, plus SSL V-MIDI virtual ports) and Akai MPC Studio mk2 (inMusic driver). Arturia KeyStep 37 also present
Devices that reproduce Both the SSL 12 and the MPC Studio mk2 were being opened when the WinMM path wedged, on separate occasions in the same incident
Devices that do NOT reproduce Not established. The Arturia KeyStep 37 returned an error from close rather than hanging
Believed scope The containment defect is code-path-wide — it is in wdmaud2.drv and midisrv and is independent of device, driver and transport. The trigger is driver-scoped and known to affect at least two vendors

API path matrix

Path Tool Result
Native UMP midi endpoint monitor Not tested
WinRT MIDI 1.0 Not tested
WinMM (classic) Ableton Live 11 (not a clean repro tool) Hang observed in the field, not reproduced locally

The matrix cannot localize the trigger yet. The lock and timeout defects are in the WinMM client
and the service regardless of what the matrix eventually says.

API mode

Mode during the field report Not captured. wdmaud2.drv was loaded in the process, which indicates Full (0) rather than Legacy
Result in Legacy API mode Not tested
Conclusion Cannot be drawn yet. Legacy mode removes midisrv and wdmaud2.drv from the path entirely, so a Legacy result would be decisive for the trigger

Suggested confirmation steps

  1. While an application is wedged, take procdump -ma midisrv.exe and a dump of the hung
    application, before killing anything. Check whether a thread is in DestroyMidiClient or
    CreateMidiClient holding m_ClientManagerLock, and what it is blocked on underneath.
  2. Check for more than one instance of the application — the original zombie may still be running
    and holding the ports.
  3. Does net stop midisrv / net start midisrv clear it without a reboot? If yes, the wedge is
    service state. Note that #1037 then applies to any surviving client.
  4. Synthetic repro attempt: open a WinMM port on a USB device, then remove the device abruptly
    (unplug, or disable through Device Manager) while the port is open, and close it. Repeat with a
    second, differently plumbed device to establish scope.
  5. ETW using the recipe in #1169 — providers Microsoft.Windows.Midi2.WdmAud2
    {e6443bc1-e9c5-5a3f-cfb6-abcd62e52e41} and Microsoft.Windows.Midi2.MidiSrv
    {f42d2441-aac3-5216-0150-3c0f50006b64}. A gap with nothing logged by any thread is the
    signature to look for, as in that issue's Item 3.

Possible directions

All three are in shipping code and need the usual KIR gate with // Start add / // End add.

  1. Containment first — put a timeout on the client RPC. RpcBindingSetOption with
    RPC_C_OPT_CALL_TIMEOUT on the binding used by MidiSrvCreateClient / MidiSrvDestroyClient.
    This converts an unbounded hang into an MMSYSERR_* that applications already handle, and it
    contains any cause, including ones not yet understood. The value has to be generous enough
    not to break a legitimately slow open — endpoint creation was measured at ~435 ms in #1169 —
    so something on the order of seconds, not milliseconds. This is the highest-value change
    because it does not require knowing what stalled.
  2. Do not hold CMidiPorts::m_Lock across the RPC in Open and Close. Same shape as
    #1169 Item 2: do the map work under the lock, release, then make the cross-process call.
    Open needs care — the port has to be published into m_OpenPorts without a window where two
    opens for the same port race. This stops one stuck call taking down the rest of the process.
  3. Service side: narrow m_ClientManagerLock. Extend the deferred pattern already used by
    DestroyMidiClientDeferredPipeShutdown so CreateMidiClient does not hold the lock exclusive
    across transport-reaching work. This is what stops one client wedging every other client, and
    it is the part that matters for the cross-process symptom.
  4. Not a fix, but worth stating in the issue: per-driver surprise-removal workarounds are
    necessary and are proceeding separately. They reduce how often this is triggered. They cannot
    bound it, because the driver set is open-ended. Items 1–3 are what make a stalled driver cost
    one port instead of every MIDI application on the machine.

Fixing 1 and 3 would also give #1016 and #1174 a cleaner story, since all three are versions of
"one client's state gates everyone else's".

Relationship to existing issues

  • #1169 — same file, same class of defect, but that issue covers millisecond stalls on the
    send and enumeration paths during device changes. Its own table of m_Lock callers lists
    Open / Close with the "held across what" column blank; this issue fills that row in, and the
    consequence is a hang rather than a stall. Could reasonably be folded in as Item 5 there if you
    would rather keep all CMidiPorts lock work in one place.
  • #1174 and #1016 — the mirror image: the service blocked by clients, rather than clients
    blocked by the service. #1174 notes its failure is fast and nothing is permanently hung, which
    is the opposite of this one. A shared m_ClientManagerLock fix may touch all three.
  • #1037 — opens fail after a midisrv restart rather than hanging. Relevant because
    restarting midisrv is the obvious workaround for this wedge and lands the user there next.

AI Generated Content

The code analysis, the timeline reconstruction from the Ableton logs, the duplicate check against
#1169 / #1174 / #1037 / #1016, and the suggested directions were produced by an agent
(GitHub Copilot) from four crash report packs plus the repository source. No part of the field
failure was reproduced locally, and no dump of midisrv or of the hung application exists. The
causal link between the code paths described and the observed hang is inferred, not traced.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.