27bf1601ade0f0799c2e43edefadb85abd1d7e5f [M144-LTS] media/audio/cras: Fix Use-After-Free in CrasInputStream
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 ofthisas 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
- 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).
- 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.
- 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.
- 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
- Setup — Obtain microphone/capture access on ChromeOS and open a CRAS input stream so callbacks flow on the real-time thread (inferred).
- Race — Tear down the stream (Stop/Close) timed against an in-flight callback so the callback outlives the object (inferred).
- Corruption — The late callback writes through the freed stream, giving a UAF write primitive in the browser process (inferred).
- 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
thisor 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.