WASAPI: cached IMMDeviceEnumerator is created in one thread's STA and used after that thread exits
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 25/100
Hướng nghiên cứu
Start with the ENUMERATOR initialization in src/host/wasapi/device.rs and COM lifetime handling in src/host/com.rs; then trace how default_device_monitor() stores the enumerator and how DefaultDeviceMonitor::drop unregisters its callback. Reproduce the failure with Windows tests run using --test-threads=1, then determine a lifetime strategy that covers both paths. Done means device enumeration and monitor cleanup remain safe after the creating thread exits.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Calling cpal's safe API on Windows can crash the process with STATUS_ACCESS_VIOLATION rather than returning an error.
Symptom
In a test binary run with --test-threads=1, a test that enumerates devices passes when run alone, but the process faults when other tests run in the same binary. The fault lands on an arbitrary later test and moves between runs. Two stacks from cdb, both faulting at +0x19d — the vtable read:
INVALID_POINTER_READ_c0000005
windows::Win32::Media::Audio::IMMDeviceEnumerator::GetDefaultAudioEndpoint
cpal::host::wasapi::device::default_device
cpal::host::wasapi::device::default_input_device
cpal::traits::HostTrait::default_input_device
INVALID_POINTER_READ_c0000005
windows::Win32::Media::Audio::IMMDeviceEnumerator::EnumAudioEndpoints
cpal::host::wasapi::device::Devices::new
cpal::host::wasapi::impl$1::devices
cpal::traits::HostTrait::input_devices
Observed on windows-latest GitHub Actions runners, cpal 0.17.3, and the same code is present on main.
Diagnosis
ENUMERATOR in src/host/wasapi/device.rs is a process-wide OnceLock<Enumerator>, created on whichever thread calls first, with unsafe impl Send + Sync. COM is initialised as COINIT_APARTMENTTHREADED through a thread-local in src/host/com.rs whose Drop calls CoUninitialize.
An STA belongs to the thread that created it. So once the first thread to touch cpal exits, the cached interface pointer is dangling, and every later caller reads a dead vtable. The comment at the initialisation site states the assumption that does not hold:
// COM initialization is thread local, but we only need to have COM initialized in the
// thread we create the objects in
com::com_initialized();
This is most visible where audio work happens on short-lived threads: whichever one touches cpal first owns the enumerator, and its exit breaks device access for the rest of the process.
Ruled out
CoCreateInstancefailing is not a fit — that would panic at the.unwrap(), not fault at a vtable read.- Absent audio endpoints is not a fit either — that surfaces as an HRESULT from
GetDefaultAudioEndpoint/EnumAudioEndpoints, not an invalid dereference.
Two affected paths
Making ENUMERATOR a thread_local fixes enumeration, but is not sufficient. default_device_monitor() clones the enumerator into DefaultDeviceMonitor, which is unsafe impl Send + Sync and stored in Stream. Its Drop calls UnregisterEndpointNotificationCallback, and carries this comment:
// Ensure COM is initialised on this thread before making COM calls. Drop can run on
// any thread (e.g. the audio run thread), which may not have called CoInitialize.
Initialising COM on the dropping thread does not help if the apartment that owns the object has gone. That path shares an apartment-bound pointer regardless of what the enumeration path does.
Possibly related earlier reports
#517, #539 and #596 all report this exit code and were closed as ASIO issues. #517 in particular describes "enumerate devices in another thread, after the thread returns, I get access violation", and its workaround was to spawn a thread and join it — which on the WASAPI path is precisely what destroys the apartment. It may be worth re-reading those with this in mind.
History
The cached enumerator dates to e8a0537 (Jun 2019). DefaultDeviceMonitor arrived with #1183 (May 2026), which widened the exposure to a second path.
Possible fixes
Listing these with tradeoffs rather than proposing one, since the ASIO and winit constraints are yours:
- Per-thread enumerator (
thread_local) — simple; fixes enumeration; leaves the monitor path. - No caching, create per call — same coverage as above; costs a
CoCreateInstanceper call. - A dedicated long-lived COM thread owning the enumerator and servicing calls — covers both paths, keeps STA, costs one parked thread.
- Marshal via the Global Interface Table — canonical COM answer; the most machinery.
- MTA — would solve it outright, but conflicts with the deliberate
COINIT_APARTMENTTHREADEDchoice for ASIO and winit.
I have a branch with the per-thread change if it is useful as a starting point, but since it only closes the first path I have not opened it as a PR.
Open question
Does UnregisterEndpointNotificationCallback require the same IMMDeviceEnumerator instance that registered? The documentation reads that way. If so, it rules out "create a fresh enumerator at drop time" as a fix for the monitor path.
Minor, separate
CoCreateInstance(...).unwrap() in the enumerator initialiser lets a safe API abort the process on failure.
- Ngôn ngữ chính
- Rust
- Star
- 4k
- Fork
- 554
- Merge trung bình
- 2 ngày 11 giờ
- Pull request đã merge (30 ngày)
- 3
Chuẩn bị môi trường
- Có Dockerfile hoặc tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của RustAudio/cpal
-
aaudio: dynamic buffer tuning never grows the buffer (set_buffer_size_in_frames reports Err on success)Có thể đã có người làm @dannyward630 đã nhận 2 ngày trước. Đang mởhost - aaudio platform - android
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 79/100
Maintainer thường phản hồi trong vòng 1 ngày
-
WebAudio wasm build fails with wasm-bindgen panic=unwind closuresCó thể đã có người làm @bogusyogi đã nhận 11 ngày trước. Đang mởhost - webaudio platform - web
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
RustAudio/cpal#1394 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
New WASAPI implementationĐang mởcode quality dependencies host - wasapi platform - windows
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
RustAudio/cpal#1416 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
host - pipewire platform - linux
Độ khó 4/5 1-2 ngày Mức phù hợp với người mới 52/100
RustAudio/cpal#1405 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
WASAPI: default-device activation fails with RPC_E_CHANGED_MODE after endpoint-notification callbacks put thread-pool threads into an STACó thể đã có người làm @dannyward630 đã nhận 3 ngày trước. Đang mởcorrectness host - wasapi platform - windows
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 20/100
RustAudio/cpal#1402 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của RustAudio/cpal
Issue tương tự
-
desktop documentation
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 90/100
vfarcic/dot-agent-deck#2008 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
needs triage
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 85/100
pbakaus/impeccable#1210 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
area: ci priority: P4 ready-for-agent
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
shep-pm/shep#1249 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
L: javascript
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
dependabot/dependabot-core#16744 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
nautechsystems/nautilus_trader#5486 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày