Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

WASAPI: cached IMMDeviceEnumerator is created in one thread's STA and used after that thread exits

未关闭
#1,302 18 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
25/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
冷清
技术栈
rust
领域
backend

调研方向

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.

由索引模型根据 Issue 内容生成。

描述

correctness host - wasapi platform - windows

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

  • CoCreateInstance failing 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 CoCreateInstance per 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_APARTMENTTHREADED choice 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.

主要语言
Rust
星标
4k
派生
554
平均合并
3 天 3 小时
30 天内合并 PR
4

环境准备

  • 提供 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

RustAudio/cpal 的其他 Issue

查看 RustAudio/cpal 的全部 Issue

相似的 Issue

更多 Rust Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。