Two audio-endpoint bugs found chasing the Sound Recording-tab hang #381

Merged
enricobuehler merged 4 commits from worktree-audio-devnode-orphans into main 2026-08-23 08:05:10 +00:00
Owner

Chasing a twice-reported field bug: opening Control Panel → Sound → Recording hangs the Sound control panel, and disabling the punktfunk virtual-microphone endpoint plus its backing ROOT\MEDIA\0004 clears it.

Neither fix here is proven to be that hang. Both are real bugs found while investigating, both verified on .173. The hang itself is still unreproduced — see the bottom.

Fix 1 — a host that died mid-mint left an orphan devnode

Minting is two PnP steps: SetupDiRegisterDeviceInfo (makes it real and bindable) then the owner marker into Device Parameters. A host dying between them leaves a registered, driver-bound, endpoint-serving devnode with no marker. find_role_devnode matches only the marker, so the next pass mints a second devnode and abandons the first — and devnode_cleanup is marker-matched too, so uninstall never removes it.

Reporter #1's box shows exactly that in every wiring plan it logged: Punktfunk Speakers (3- Punktfunk) beside an unstamped Punktfunk Speakers (4- Steam Streaming Speakers). Their host supplied the mid-mint deaths — 49 relaunches, 5 aborts.

Reproduced on .173 against shipping 0.31.2 by clearing the marker:

ROOT\MEDIA\0004  marker=<NONE>                 <- orphan, still active
ROOT\MEDIA\0005  marker=PunktfunkAudioRole=2   <- freshly minted duplicate

After the fix, same procedure:

WARN adopted an abandoned audio devnode — one of ours whose owner marker never
     landed (a host that died mid-mint). Re-marked and reused instead of
     minting a duplicate  role="mic" devnode=ROOT\MEDIA\0004

No 0005; endpoint GUIDs unchanged, so recorded ids stay valid and no device-change broadcast is paid.

devnode_cleanup sweeps the same shape so orphans already on a box go at uninstall. The instance prefix keeps both off Valve's own devices: Steam's devnodes carry these hardware ids and are ROOT-enumerated too, but live under ROOT\SteamStreamingSpeakers\* / ROOT\SteamStreamingMicrophone\*. Only ROOT\MEDIA\* comes from our SetupDiCreateDeviceInfoW(… DICD_GENERATE_ID).

Fix 2 — the registry stamp route reached for the wrong hive

write_stamps falls back to a raw-registry write when the property store denies it. That fallback built its path from MMDEV_RENDER_PATH unconditionally, so stamping the minted microphone's capture endpoint reached for …\MMDevices\Audio\Render\{capture-guid}\Properties — a key that cannot exist. The open failed and stamp_identity degraded to "keeps the driver's default name".

Invisible to the pad program (render-only endpoints) and invisible wherever the property store route succeeds — both field logs show registry=[] on every stamp line, so neither reporter took this path. It only bites where the property store is denied, i.e. exactly the boxes the ACL repair exists for.

Tests

cargo test -p punktfunk-host audio:: on .173 — 111 passed, including four new ones covering the abandoned-devnode discriminator (against Steam's devnodes, VB-Cable's ROOT\MEDIA devnodes, and real HDAUDIO hardware) and the hive selection.

The hang is still unexplained

.173 matches both reporters on every dimension I can measure — same Steam drivers, same minted pair, punktfunk mic as default capture on all three roles, host running and holding the mic render open host-lifetime, VB-Cable and Realtek present, 21 vs 27 capture registry entries — and does not reproduce. Measured there and ruled out:

Hypothesis Result
Incoherent DeviceFormat makes the capture endpoint unopenable Initialize succeeds in 12 ms
The pre-Aug-7 mix-fmt-2 capture stamp (52cd42e9's poison, never healed) ✗ applied it by hand; still opens
Duplicate mic devnodes ✗ 0/30 hung pings with two live mics
Property-store Commit storm → OnPropertyValueChanged flood 102,197 commits during a 30 s ping window, 0/30 hung
registry_stamp DACL rewrite damaging the key ✗ ACLs byte-identical to Steam's, fully inherited; and registry=[] in both field logs
Slow enumeration / property reads / Activate ✗ every endpoint under 55 ms
Level-meter polling (what the Recording tab does per device) GetPeakValue 0 ms on all 21 capture endpoints
Host crash-loop leaving a wedged capture pin ✗ 5 hard kills, then 0/30 hung
Punktfunk mic being the default capture device ✗ it already is on .173

Reporter #1's isolation is confounded: they disabled two devices and rebooted. A reboot alone ends the 0.30.0 crash loop, so "disabling the mic fixed it" is not established.

Worth noting separately: their mic capture endpoint is internally inconsistent — DeviceFormat 2ch/16-bit PCM against OEM/Mix/Host all 2ch/32-bit float, where Steam's own capture endpoint is 1ch/32-float across all four. Nothing punktfunk ships ever opens that endpoint to catch it (micpins only calls IsFormatSupported, the call minted.rs's own comments record as lying here). It does not hang the endpoint, but it is a real coherence wart.

Also worth separate fixes

  • The five crashes are the TLS-destructor abort already fixed in 4cd35e15 (v0.31.0); reporter #1 is on 0.30.0.
  • PROVISIONED / LAST_ATTEMPT / UNLATCHED_ATTEMPTS are process-global, so MAX_UNLATCHED_ATTEMPTS — the guard that stops bind_driver broadcasting device changes at the whole box — is defeated by a crash-restart loop.
  • Four generations of every punktfunk driver sit in reporter #1's driver store; pnputil /delete-driver runs from Inno's [UninstallRun], which does not fire on upgrade.
Chasing a twice-reported field bug: **opening Control Panel → Sound → Recording hangs the Sound control panel**, and disabling the punktfunk virtual-microphone endpoint plus its backing `ROOT\MEDIA\0004` clears it. ⚠ **Neither fix here is proven to be that hang.** Both are real bugs found while investigating, both verified on .173. The hang itself is still unreproduced — see the bottom. ## Fix 1 — a host that died mid-mint left an orphan devnode Minting is two PnP steps: `SetupDiRegisterDeviceInfo` (makes it real and bindable) then the owner marker into `Device Parameters`. A host dying between them leaves a registered, driver-bound, endpoint-serving devnode with **no marker**. `find_role_devnode` matches only the marker, so the next pass mints a *second* devnode and abandons the first — and `devnode_cleanup` is marker-matched too, so uninstall never removes it. Reporter #1's box shows exactly that in **every** wiring plan it logged: `Punktfunk Speakers (3- Punktfunk)` beside an unstamped `Punktfunk Speakers (4- Steam Streaming Speakers)`. Their host supplied the mid-mint deaths — 49 relaunches, 5 aborts. **Reproduced on .173 against shipping 0.31.2** by clearing the marker: ``` ROOT\MEDIA\0004 marker=<NONE> <- orphan, still active ROOT\MEDIA\0005 marker=PunktfunkAudioRole=2 <- freshly minted duplicate ``` **After the fix, same procedure:** ``` WARN adopted an abandoned audio devnode — one of ours whose owner marker never landed (a host that died mid-mint). Re-marked and reused instead of minting a duplicate role="mic" devnode=ROOT\MEDIA\0004 ``` No `0005`; endpoint GUIDs unchanged, so recorded ids stay valid and no device-change broadcast is paid. `devnode_cleanup` sweeps the same shape so orphans already on a box go at uninstall. The instance prefix keeps both off **Valve's own devices**: Steam's devnodes carry these hardware ids and are ROOT-enumerated too, but live under `ROOT\SteamStreamingSpeakers\*` / `ROOT\SteamStreamingMicrophone\*`. Only `ROOT\MEDIA\*` comes from our `SetupDiCreateDeviceInfoW(… DICD_GENERATE_ID)`. ## Fix 2 — the registry stamp route reached for the wrong hive `write_stamps` falls back to a raw-registry write when the property store denies it. That fallback built its path from `MMDEV_RENDER_PATH` unconditionally, so stamping the minted microphone's **capture** endpoint reached for `…\MMDevices\Audio\Render\{capture-guid}\Properties` — a key that cannot exist. The open failed and `stamp_identity` degraded to "keeps the driver's default name". Invisible to the pad program (render-only endpoints) and invisible wherever the property store route succeeds — both field logs show `registry=[]` on every stamp line, so neither reporter took this path. It only bites where the property store is denied, i.e. exactly the boxes the ACL repair exists for. ## Tests `cargo test -p punktfunk-host audio::` on .173 — **111 passed**, including four new ones covering the abandoned-devnode discriminator (against Steam's devnodes, VB-Cable's `ROOT\MEDIA` devnodes, and real HDAUDIO hardware) and the hive selection. ## The hang is still unexplained .173 matches both reporters on every dimension I can measure — same Steam drivers, same minted pair, punktfunk mic as default capture on all three roles, host running and holding the mic render open host-lifetime, VB-Cable and Realtek present, 21 vs 27 capture registry entries — and **does not reproduce**. Measured there and ruled out: | Hypothesis | Result | |---|---| | Incoherent `DeviceFormat` makes the capture endpoint unopenable | ✗ `Initialize` succeeds in 12 ms | | The pre-Aug-7 `mix-fmt-2` capture stamp (52cd42e9's poison, never healed) | ✗ applied it by hand; still opens | | Duplicate mic devnodes | ✗ 0/30 hung pings with two live mics | | Property-store `Commit` storm → `OnPropertyValueChanged` flood | ✗ **102,197 commits** during a 30 s ping window, 0/30 hung | | `registry_stamp` DACL rewrite damaging the key | ✗ ACLs byte-identical to Steam's, fully inherited; and `registry=[]` in both field logs | | Slow enumeration / property reads / `Activate` | ✗ every endpoint under 55 ms | | Level-meter polling (what the Recording tab does per device) | ✗ `GetPeakValue` 0 ms on all 21 capture endpoints | | Host crash-loop leaving a wedged capture pin | ✗ 5 hard kills, then 0/30 hung | | Punktfunk mic being the default capture device | ✗ it already is on .173 | **Reporter #1's isolation is confounded**: they disabled two devices *and rebooted*. A reboot alone ends the 0.30.0 crash loop, so "disabling the mic fixed it" is not established. Worth noting separately: their mic capture endpoint is internally inconsistent — `DeviceFormat` 2ch/16-bit PCM against `OEM`/`Mix`/`Host` all 2ch/32-bit float, where Steam's own capture endpoint is 1ch/32-float across all four. Nothing punktfunk ships ever opens that endpoint to catch it (`micpins` only calls `IsFormatSupported`, the call `minted.rs`'s own comments record as lying here). It does not hang the endpoint, but it is a real coherence wart. ## Also worth separate fixes * The five crashes are the TLS-destructor abort already fixed in 4cd35e15 (v0.31.0); reporter #1 is on 0.30.0. * `PROVISIONED` / `LAST_ATTEMPT` / `UNLATCHED_ATTEMPTS` are process-global, so `MAX_UNLATCHED_ATTEMPTS` — the guard that stops `bind_driver` broadcasting device changes at the whole box — is defeated by a crash-restart loop. * Four generations of every punktfunk driver sit in reporter #1's driver store; `pnputil /delete-driver` runs from Inno's `[UninstallRun]`, which does not fire on upgrade.
enricobuehler added 2 commits 2026-08-23 07:13:32 +00:00
Minting an audio devnode is two PnP steps: SetupDiRegisterDeviceInfo makes it
real and bindable, then the owner marker goes into Device Parameters. A host
that dies between them - the 0.30.0 TLS-destructor abort did exactly this,
five times on one field box - leaves a registered, driver-bound, endpoint-
serving devnode carrying no marker.

Nothing resolved it afterwards. find_role_devnode matches on the marker, so
the next pass minted a SECOND devnode and the orphan stayed: a duplicate
'Punktfunk Speakers'/'Punktfunk Microphone' in the Sound zoo that no uninstall
removed, because devnode_cleanup is marker-matched too. A field box showed
exactly this shape - 'Punktfunk Speakers (3- Punktfunk)' beside an unstamped
'Punktfunk Speakers (4- Steam Streaming Speakers)' - in every wiring plan it
logged. Reproduced on .173 against the shipping 0.31.2 binary by clearing the
marker: ROOT\MEDIA\0005 was minted and 0004 was abandoned, still active and
still serving two live Punktfunk Microphone endpoints.

* minted.rs adopts before it mints. An unmarked ROOT\MEDIA\NNNN devnode
  carrying the role's Steam hardware id is re-marked and reused, so the
  endpoint GUID survives and no device-change broadcast is paid.
* devnode_cleanup sweeps the same shape, so orphans already on a box go at
  uninstall instead of outliving the product.

The instance prefix is what keeps both off Valve's own devices: Steam's
devnodes carry these hardware ids and are ROOT-enumerated too, but live under
ROOT\SteamStreamingSpeakers\* / ROOT\SteamStreamingMicrophone\*. Only
ROOT\MEDIA\* can come from our SetupDiCreateDeviceInfoW(DICD_GENERATE_ID).
is_abandoned_mint carries that rule with unit tests.
fix(host,audio): merge the abandoned-devnode tests into the existing module
ci / bun-nix (pull_request) Successful in 29s
ci / web (pull_request) Successful in 1m8s
ci / docs-drift (pull_request) Successful in 1m9s
ci / docs-site (pull_request) Successful in 1m35s
ci / rust (pull_request) Failing after 1m44s
ci / rust-arm64 (pull_request) Successful in 2m30s
android / android (pull_request) Successful in 5m48s
4beee17953
enricobuehler added 1 commit 2026-08-23 07:40:21 +00:00
fix(host,audio): the registry stamp route reached for the Render hive even for capture endpoints
ci / rust-arm64 (pull_request) Successful in 1m21s
ci / rust (pull_request) Failing after 2m50s
ci / web (pull_request) Successful in 1m6s
ci / bun-nix (pull_request) Successful in 22s
ci / docs-site (pull_request) Successful in 1m7s
ci / docs-drift (pull_request) Successful in 23s
android / android (pull_request) Canceled after 7m19s
4c5b97cfe4
write_stamps falls back to a raw-registry write when the property store
denies it. That fallback built its path from MMDEV_RENDER_PATH unconditionally,
so stamping the minted microphone's CAPTURE endpoint reached for
...\MMDevices\Audio\Render\{capture-guid}\Properties - a key that cannot
exist. RegOpenKeyExW then failed, write_stamps returned the error, and
stamp_identity degraded to 'keeps the driver's default name'.

Invisible to the pad program, whose endpoints are render-only, and invisible on
any box where the property store route succeeds (both field logs show
registry=[] on every stamp line, so neither reporter ever took this path). It
only bites where the property store is denied - exactly the boxes the ACL
repair exists for.

The hive now follows the direction the endpoint id encodes, with render as the
default for anything unrecognised. Unit-tested.
enricobuehler changed title from WIP: A host that died mid-mint left an orphan devnode, and the next start minted a duplicate to Two audio-endpoint bugs found chasing the Sound Recording-tab hang 2026-08-23 07:42:39 +00:00
enricobuehler added 1 commit 2026-08-23 07:49:55 +00:00
style: rustfmt the abandoned-devnode sweep
ci / rust-arm64 (pull_request) Successful in 1m20s
ci / bun-nix (pull_request) Successful in 20s
ci / docs-drift (pull_request) Successful in 21s
ci / docs-site (pull_request) Successful in 1m3s
ci / web (pull_request) Successful in 3m33s
android / android (pull_request) Successful in 10m5s
ci / rust (pull_request) Successful in 9m45s
96f75f4e52
enricobuehler merged commit c920204184 into main 2026-08-23 08:05:10 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#381