Summary
Use-after-free on the ASIO SDK's global theAsioDriver pointer when a device
fails the capability check. On current main, a device that fails
CheckDeviceCapabilities() during ASIO re-init can leave a dangling global
pointer that a later re-init dereferences. Symptoms are inconsistent — hang,
crash, or silent corruption — which matches the reports in #872 and #305.
Context
Evidence (mcfnord, on Windows — issue #3779 comment 5224437188)
One honest caveat up front: this measurement was done with a synthetic test
driver that deliberately fails the capability check, not with ASIO4ALL on real
hardware. With that said:
- This is a use-after-free, not a leak:
ASIOExit() is effectively
removeCurrentDriver() + theAsioDriver = 0. The current failure branch
only does the first of those.
- Outcome is driver-dependent: ASIO4ALL's driver object is static in its
DLL and survives the COM release (→ hang / lockout); Focusrite USB
ASIO's object is heap-allocated, so the next call faults (0xC0000005).
- With the synthetic driver, the stock build (
4d142945) made seven ASIO
calls on the freed object; with ASIOExit(), zero.
How to reproduce
The reliable repro we have is with a synthetic driver that fails
CheckDeviceCapabilities() on demand. On real Windows hardware with ASIO4ALL
this is our best guess at hitting the same path — it is unverified:
- Select a device that fails the capability check (e.g. one that does not
support the required sample rate) while a valid device is also available.
- Trigger re-init (e.g. change the audio device in the client settings).
- Re-select the previous working device.
The failure branch is hit at CSound::LoadAndInitializeDriver()
(src/sound/asio/sound.cpp) — CheckDeviceCapabilities() fails, the driver is
released via asioDrivers->removeCurrentDriver(), and the global
theAsioDriver pointer is left dangling. A subsequent re-init dereferences it.
Proposed fix
In CSound::LoadAndInitializeDriver() (src/sound/asio/sound.cpp), the
CheckDeviceCapabilities() failure branch should call ASIOExit() instead
of asioDrivers->removeCurrentDriver(), so the global pointer is nulled. The
ASIOInit()-failed branch is left alone — from reading the ASIO SDK it resets
the global pointer on that path, though I haven't instrumented it to be sure.
A ready patch exists in fork PR ann0see/jamulus#286 (one-line change + comment),
reproducing the 7-vs-0 result. This issue is opened to track the defect so the
fix is not lost if the fork PR is never reopened upstream.
Requested actions
- Review whether the failure branch should use
ASIOExit().
- If agreed, apply the fix on
main (a small, low-risk change).
⚠️ AI-generated issue — please verify
- This text is AI generated, may be wrong, and may contain inaccuracies.
- The hardware verification described below was performed by a human with an AI agent
(mcfnord, on Windows).
Summary
Use-after-free on the ASIO SDK's global
theAsioDriverpointer when a devicefails the capability check. On current
main, a device that failsCheckDeviceCapabilities()during ASIO re-init can leave a dangling globalpointer that a later re-init dereferences. Symptoms are inconsistent — hang,
crash, or silent corruption — which matches the reports in #872 and #305.
Context
cross lockout" symptom; the root cause was not identified then.
in which Jamulus process doesn't get killed if you select a device with wrong sample rate (ASIO4ALL) #872's defect is one of several distinct problems.
Evidence (mcfnord, on Windows — issue #3779 comment 5224437188)
One honest caveat up front: this measurement was done with a synthetic test
driver that deliberately fails the capability check, not with ASIO4ALL on real
hardware. With that said:
ASIOExit()is effectivelyremoveCurrentDriver()+theAsioDriver = 0. The current failure branchonly does the first of those.
DLL and survives the COM release (→ hang / lockout); Focusrite USB
ASIO's object is heap-allocated, so the next call faults (
0xC0000005).4d142945) made seven ASIOcalls on the freed object; with
ASIOExit(), zero.How to reproduce
The reliable repro we have is with a synthetic driver that fails
CheckDeviceCapabilities()on demand. On real Windows hardware with ASIO4ALLthis is our best guess at hitting the same path — it is unverified:
support the required sample rate) while a valid device is also available.
The failure branch is hit at
CSound::LoadAndInitializeDriver()(src/sound/asio/sound.cpp) —
CheckDeviceCapabilities()fails, the driver isreleased via
asioDrivers->removeCurrentDriver(), and the globaltheAsioDriverpointer is left dangling. A subsequent re-init dereferences it.Proposed fix
In
CSound::LoadAndInitializeDriver()(src/sound/asio/sound.cpp), theCheckDeviceCapabilities()failure branch should callASIOExit()insteadof
asioDrivers->removeCurrentDriver(), so the global pointer is nulled. TheASIOInit()-failed branch is left alone — from reading the ASIO SDK it resetsthe global pointer on that path, though I haven't instrumented it to be sure.
A ready patch exists in fork PR
ann0see/jamulus#286(one-line change + comment),reproducing the 7-vs-0 result. This issue is opened to track the defect so the
fix is not lost if the fork PR is never reopened upstream.
Requested actions
ASIOExit().main(a small, low-risk change).(mcfnord, on Windows).