← Chrome — 2026-W34

27bf1601ade0f0799c2e43edefadb85abd1d7e5f  [M144-LTS] media/audio/cras: Fix Use-After-Free in CrasInputStream

severity high class UAF CVSS 7.0 signal score 13.96 media/ AI review CONFIRMED
Structural signals: smartptr_member_added (+4.87), null_guard (+2.59), msg_strong (+3.0), msg_medium (+2.0), area:media/ (+1.5)

The bug at a glance

The commit message states CrasInputStream had no synchronization between its stop/destruction on the audio thread and real-time libcras callbacks, and because a raw pointer to the stream was passed as the callback usr_arg, a late callback could run after the object was freed, yielding a use-after-free write in the browser process; a UAF-write in the privileged browser process is high severity, tempered by the ChromeOS-only platform and the need for an active capture stream.

On ChromeOS, a page (or component) that opens and then rapidly tears down an audio input/capture stream can race the real-time CRAS callback thread against stream destruction, turning a late callback into a use-after-free write in the browser process callable toward memory corruption.

Root cause

libcras invokes the static SamplesReady/StreamError callbacks on a high-priority CRAS thread with usr_arg set to the stream object; pre-patch usr_arg was the raw CrasInputStream* (passed as this). Stop()/Close() ran on the audio thread with no lock, and CrasInputStream::Close() ends by deleting this, so a callback already in flight (or arriving after stop) dereferenced and wrote through freed memory. The fix introduces CrasAudioInputStreamProxy, a base::Lock-guarded holder of a raw_ptr owned by CrasInputStream via unique_ptr. The proxy pointer is now passed as usr_arg; callbacks forward through the proxy, which takes the lock and checks the stream is non-null before calling OnSamplesReady/OnStreamError. Stop() calls proxy_->Detach() to acquire the lock and null the pointer, fencing off the CRAS thread before teardown and destruction.

Key code

Lock-guarded proxy nulls the stream pointer on detach, fencing the CRAS callback thread

int SamplesReady(struct libcras_stream_cb_data* data) {
  base::AutoLock auto_lock(lock_);
  if (stream_) {
    return stream_->OnSamplesReady(data);
  }
  ...
}
void Detach() {
  base::AutoLock auto_lock(lock_);
  stream_ = nullptr;
}

Patch walkthrough

  • media/audio/cras/cras_input.cc — Adds CrasAudioInputStreamProxy with a base::Lock and raw_ptr; passes proxy_.get() instead of this as the libcras usr_arg; static callbacks now forward through the proxy under the lock; Stop() calls proxy_->Detach() to null the pointer before teardown and Close() resets the proxy. This closes the race where a late callback dereferenced/wrote through a freed CrasInputStream.
  • media/audio/cras/cras_input.h — Declares CrasAudioInputStreamProxy as a friend, adds the OnSamplesReady/OnStreamError instance methods, and adds the std::unique_ptr proxy_ member that owns the proxy.

Background

CRAS (ChromeOS Audio Server) — The ChromeOS audio server; libcras delivers capture buffers via callbacks on a dedicated real-time thread.

usr_arg callback pattern — libcras passes an opaque user pointer back to the registered C callbacks; using a raw object pointer there makes lifetime a correctness/safety hazard across threads.

base::Lock-guarded proxy — A thread-safe indirection (as used by CatapAudioInputStream on Mac) that nulls its back-pointer under a lock so callbacks can safely no-op after detach.

Vulnerability window

  1. Reachability — ChromeOS only, in the browser process audio service; requires an active audio capture/input stream, typically via getUserMedia microphone permission or a system capture path (inferred).
  2. Trigger — The stream is stopped/closed on the audio thread while a libcras SamplesReady/StreamError callback is in flight or arrives late on the CRAS thread.
  3. Primitive — The late callback dereferences the freed CrasInputStream via the raw usr_arg and performs a write (e.g. through ReadAudio/NotifyStreamError), a use-after-free write in the browser process.
  4. Escalation — A controlled UAF write in the browser process could be shaped toward memory corruption and privileged code execution (inferred).

Proof of concept

Conceptually: on ChromeOS, start an audio capture stream so libcras begins issuing real-time SamplesReady callbacks, then repeatedly stop/close the stream to race destruction against an in-flight callback; pre-patch the callback can run after the CrasInputStream is freed. No exploit code is provided.

Exploitation

  1. Setup — Obtain microphone/capture access on ChromeOS and open a CRAS input stream so callbacks flow on the real-time thread (inferred).
  2. Race — Tear down the stream (Stop/Close) timed against an in-flight callback so the callback outlives the object (inferred).
  3. Corruption — The late callback writes through the freed stream, giving a UAF write primitive in the browser process (inferred).
  4. Leverage — Groom the freed slot and shape the write toward control-flow hijack in the browser (inferred).

Detection & hunting

For defenders and detection engineers:

  • grep for raw this or raw object pointers passed as callback usr_arg/user-data into libcras_stream_params_set and similar C callback registrations — Flag audio/IPC callback registrations where a raw object pointer crosses to another thread without a lock-guarded proxy or weak reference.
  • ASAN heap-use-after-free reports in media/audio/cras during stream stop/close — Hunt crashes where SamplesReady/StreamError appear on the stack after CrasInputStream destruction.

Audit directions

  • Other AudioInputStream/AudioOutputStream backends that register C callbacks with a raw stream pointer (ALSA, PulseAudio, OpenSLES, platform CRAS output) — Audit each for the same pattern and apply the lock-guarded proxy or ensure callbacks cannot outlive the stream.

Before / after

Loading diff…