[BUG]: A stalled driver wedges WinMM MIDI open/close machine-wide — unbounded RPC and global locks give it unlimited blast radius
@Psychlist1972 is already working on this.
Since Sep 11, 2026.
- 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:
- that call never returns,
- every subsequent WinMM MIDI open or close in the same process blocks behind the
process-wideCMidiPorts::m_Lock, - every subsequent open or close in every other process blocks behind the service's
exclusivem_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_MidiPortsis a per-process singleton —winmmdrv.cpp:55. Onewil::critical_section m_Lock(MidiSrvPorts.h:67) covers every port in the process.CMidiPorts::Opentakesm_LockatMidiSrvPorts.cpp:1162, then calls
MakeAndInitialize<CMidiPort>→CMidiPort::RuntimeClassInitialize→midiSrv->Initialize
(MidiSrvPort.cpp:90) →MidiSrvCreateClient. The lock is held for the whole chain.CMidiPorts::Closetakesm_LockatMidiSrvPorts.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) andMidiSrvDestroyClient(:253) are
plain blocking RPC calls insideRpcTryExcept. There is noRPC_C_OPT_CALL_TIMEOUTand no
RpcBindingSetOptioncall anywhere inMidiSrvTransportLib.RpcTryExceptcatches faults,
not hangs.
Service — one exclusive lock gates all clients:
CreateMidiClienttakesm_ClientManagerLock.lock_exclusive()atMidiClientManager.cpp:1133
and holds it acrossGetEndpointAlias,GetMidiClientand device/transform pipe creation.DestroyMidiClient(:1382) andDestroyMidiClientDeferredPipeShutdown(:1529) take the
same lock exclusive.Feature_Servicing_MIDI2VirtualDeviceRemovalDeadlockalready moves pipeShutdown()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.cfgrecords the port Live was
opening when it died:SSL 12 MIDI→MPC Studio mk2 Public→MPC 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, andASIO: 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.cfgnames, 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
- While an application is wedged, take
procdump -ma midisrv.exeand a dump of the hung
application, before killing anything. Check whether a thread is inDestroyMidiClientor
CreateMidiClientholdingm_ClientManagerLock, and what it is blocked on underneath. - Check for more than one instance of the application — the original zombie may still be running
and holding the ports. - Does
net stop midisrv/net start midisrvclear it without a reboot? If yes, the wedge is
service state. Note that #1037 then applies to any surviving client. - 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. - ETW using the recipe in #1169 — providers
Microsoft.Windows.Midi2.WdmAud2
{e6443bc1-e9c5-5a3f-cfb6-abcd62e52e41}andMicrosoft.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.
- Containment first — put a timeout on the client RPC.
RpcBindingSetOptionwith
RPC_C_OPT_CALL_TIMEOUTon the binding used byMidiSrvCreateClient/MidiSrvDestroyClient.
This converts an unbounded hang into anMMSYSERR_*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. - Do not hold
CMidiPorts::m_Lockacross the RPC inOpenandClose. 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 intom_OpenPortswithout a window where two
opens for the same port race. This stops one stuck call taking down the rest of the process. - Service side: narrow
m_ClientManagerLock. Extend the deferred pattern already used by
DestroyMidiClientDeferredPipeShutdownsoCreateMidiClientdoes 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. - 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 ofm_Lockcallers lists
Open / Closewith 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 allCMidiPortslock 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 sharedm_ClientManagerLockfix 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.