CVE-2026-79210
Overview
Background
- `AAudioStreamWrapper`
- Chrome’s Android wrapper around an
AAudioStream; AAudio invokes data and error callbacks on its own real-time thread and passes auser_datapointer back to Chrome. - `AAudioDestructionHelper`
- the object bound as the callbacks’
user_data. During teardown it takes ownership of theAAudioStream(aaudio_stream_) so that callbacks still in flight hit a live object instead of the destroyed wrapper. - crbug.com/1183255 mitigation
- per the in-code comment, on Android releases before S (API 31) AAudio may keep invoking callbacks after
AAudioStream_close(). For that case Chrome keeps the helper alive past the wrapper’s lifetime with a delayed task. - `base::DoNothingWithBoundArgs`
- a posted task whose only job is to keep its bound arguments (here the
std::unique_ptrto the helper) alive until it runs, at which point they are destroyed.
Root Cause Analysis
Before the patch, wrapper teardown on API ≤ 30 posted a single delayed task, at 1 s, that held destruction_helper_. When that task ran, ~AAudioDestructionHelper() called AAudioStream_close(aaudio_stream_) and the helper was freed in the same step. The mitigation’s own comment says AAudio can keep invoking callbacks after AAudioStream_close(). Closing the stream and freeing the user_data object at the same instant therefore left no grace period: a callback arriving just after the close could dereference the already-freed helper, which is a use-after-free. The required invariant was that the user_data object outlives every callback, including those delivered after close.
The fix splits teardown into two tasks on the same sequence. CloseDeferredStream() runs at kCloseDelay (1 s), and the helper is freed at kDestructionDelay (1.25 s), so callbacks have 250 ms to drain after close. CloseDeferredStream() also swaps aaudio_stream_ to nullptr under lock_ before closing, which makes the close idempotent. The destructor keeps a fallback call in case the close task was dropped at shutdown. On API 31+, the stream is now closed explicitly through CloseDeferredStream() before normal teardown proceeds.
Attack Path
- Play audio
Web content causes the Android audio stack to open an AAudio output stream (for example, through media playback or Web Audio). Per the unit test, the wrapper is created with
AAUDIO_USAGE_MEDIA. - Tear the stream down
The page stops, pauses or destroys the audio sink, which destroys the
AAudioStreamWrapperon an affected device (API < 31) and schedules the deferred helper teardown. - Close and free together
At T+1 s the pre-patch task closed the stream inside
~AAudioDestructionHelper()and freed the helper immediately. - Late callback hits freed `user_data` An AAudio callback delivered after close dereferences the freed helper, a use-after-free in the audio-hosting process. Repeated stream create/destroy cycles to win the race are inferred.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/audio/android/aaudio_stream_wrapper.cc |
modified | |
MockDataCallbackmedia/audio/android/aaudio_stream_wrapper_unittest.cc |
modified | |
AAudioStreamWrapperTestmedia/audio/android/aaudio_stream_wrapper_unittest.cc |
modified | |
TEST_Fmedia/audio/android/aaudio_stream_wrapper_unittest.cc |
modified | |
ifmedia/audio/android/aaudio_stream_wrapper_unittest.cc |
modified | |
ifmedia/audio/BUILD.gn |
modified |
Files Changed
media/audio/BUILD.gnmedia/audio/android/aaudio_stream_wrapper.ccmedia/audio/android/aaudio_stream_wrapper_unittest.cc
Audit Directions
- Close-then-free on third-party callback threadsLook for other platform audio and media back ends (OpenSL ES, CoreAudio, WASAPI, PulseAudio, camera HALs) where a native close and the release of the callback’s
user_datahappen together; platform close calls do not always guarantee that callbacks have quiesced. - Delay-based lifetime workaroundsAudit
PostDelayedTask+DoNothingWithBoundArgskeep-alive patterns: check that the object is kept alive for the full window after the event that actually stops the callbacks, not merely after an earlier event. - `base::Unretained` into delayed tasksCheck that every
Unretainedpointer bound into a delayed task is guaranteed by sequence ordering, as here, and that shutdown-time task dropping cannot reorder or skip the guarantor.
Patch
diff --git a/media/audio/android/aaudio_stream_wrapper.cc b/media/audio/android/aaudio_stream_wrapper.cc
index 2d3ae4bb..bcf62f4 100644
--- a/media/audio/android/aaudio_stream_wrapper.cc
+++ b/media/audio/android/aaudio_stream_wrapper.cc
@@ -13,6 +13,7 @@
#include "base/android/device_info.h"
#include "base/feature_list.h"
#include "base/functional/callback_helpers.h"
+#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/aligned_memory.h"
#include "base/memory/raw_ptr.h"
@@ -34,6 +35,11 @@
namespace {
+constexpr base::TimeDelta kCloseDelay = base::Seconds(1);
+// Delay destruction of the helper slightly longer than the close delay, to
+// ensure callbacks have stopped after close.
+constexpr base::TimeDelta kDestructionDelay = kCloseDelay + base::Milliseconds(250);
+
constexpr char kAAudioBufferSizeInFramesMetricsPrefix[] =
"Media.Audio.Android.AAudioBufferSizeInFrames.";
constexpr char kAAudioFramesPerDataCallbackMetricsPrefix[] =
@@ -181,8 +187,19 @@
~AAudioDestructionHelper() {
CHECK(is_closing_);
- if (aaudio_stream_) {
- AAudioStream_close(aaudio_stream_);
+ // Fallback if the delayed close task was dropped during shutdown.
+ CloseDeferredStream();
+ }
+
+ void CloseDeferredStream() {
+ raw_ptr<AAudioStream> stream_to_close = nullptr;
+ {
+ base::AutoLock al(lock_);
+ stream_to_close = aaudio_stream_;
+ aaudio_stream_ = nullptr;
+ }
+ if (stream_to_close) {
+ AAudioStream_close(stream_to_close);
}
}
@@ -425,9 +442,10 @@
CHECK(!aaudio_stream_);
- // On Android S+, |destruction_helper_| can be destroyed as part of the
+ // On Android S+, `destruction_helper_` can be destroyed as part of the
// normal class teardown.
if (__builtin_available(android 31, *)) {
+ destruction_helper_->CloseDeferredStream();
return;
}
@@ -435,11 +453,26 @@
// after calling AAudioStream_close(). The code below is a mitigation to
// work around this issue. See crbug.com/1183255.
- // Keep |destruction_helper_| alive longer than |this|, so the |user_data|
+ // Keep `destruction_helper_` alive longer than `this`, so the `user_data`
// bound to the callback stays valid, until the callbacks stop.
+ //
+ // We post two tasks:
+ // 1. A task to close the stream (at T+1s).
+ // 2. A task to destroy the helper (at T+1.25s).
+ //
+ // The helper raw pointer is safe to use in the first task because the helper
+ // is guaranteed to be kept alive by the second task which has a longer delay
+ // on the same sequence.
+ auto* helper_raw = destruction_helper_.get();
+ base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce(&AAudioDestructionHelper::CloseDeferredStream,
+ base::Unretained(helper_raw)),
+ kCloseDelay);
+
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, base::DoNothingWithBoundArgs(std::move(destruction_helper_)),
- base::Seconds(1));
+ kDestructionDelay);
}
bool AAudioStreamWrapper::Open() {
diff --git a/media/audio/android/aaudio_stream_wrapper_unittest.cc b/media/audio/android/aaudio_stream_wrapper_unittest.cc
new file mode 100644
index 0000000..3c46416a
--- /dev/null
+++ b/media/audio/android/aaudio_stream_wrapper_unittest.cc
@@ -0,0 +1,86 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/audio/android/aaudio_stream_wrapper.h"
+
+#include <aaudio/AAudio.h>
+
+#include <memory>
+
+#include "base/android/android_info.h"
+#include "base/test/task_environment.h"
+#include "media/base/audio_parameters.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace media {
+
+namespace {
+
+class MockDataCallback : public AAudioStreamWrapper::DataCallback {
+ public:
+ MockDataCallback() = default;
+ ~MockDataCallback() override = default;
+ bool OnAudioDataRequested(base::span<float> audio_data) override {
+ return true;
+ }
+ void OnError() override {}
+ void OnDeviceChange() override {}
+};
+
+} // namespace
+
+class AAudioStreamWrapperTest : public testing::Test {
+ protected:
+ base::test::SingleThreadTaskEnvironment task_environment_{
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+ MockDataCallback callback_;
+};
+
+TEST_F(AAudioStreamWrapperTest, DeferredHelperOutlivesStreamClose) {
+ AudioParameters params(AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ ChannelLayoutConfig::Stereo(), 48000, 480);
+
+ auto wrapper = std::make_unique<AAudioStreamWrapper>(
+ &callback_, AAudioStreamWrapper::StreamType::kOutput, params,
+ android::AudioDevice::Default(), AAUDIO_USAGE_MEDIA);
+
+ // Destroy the wrapper. This should trigger the deferred teardown on API <= 30.
+ wrapper.reset();
+
+ auto sdk_int = base::android::android_info::sdk_int();
+ if (sdk_int < 31) {
+ // We expect two tasks to be posted:
+ // 1. Close task at T+1s.
+ // 2. Free task at T+1.25s.
+ EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 2u);
+
+ // Fast forward to T+1s (but not 1.25s).
+ task_environment_.FastForwardBy(base::Seconds(1));
+
+ // The close task should have run, but the free task should still be pending.
+ EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 1u);
+
+ // Fast forward the rest.
+ task_environment_.FastForwardBy(base::Milliseconds(250));
+ EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
+ } else {
+ // On API >= 31, destruction should be synchronous, no tasks posted.
+ EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
+ }
+}
+
+TEST_F(AAudioStreamWrapperTest, DestroyWithPendingDeferredTasks) {
+ // This test verifies that if the TaskEnvironment is destroyed (or sequence shut down)
+ // while tasks are pending, it doesn't crash.
+ AudioParameters params(AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ ChannelLayoutConfig::Stereo(), 48000, 480);
+ auto wrapper = std::make_unique<AAudioStreamWrapper>(
+ &callback_, AAudioStreamWrapper::StreamType::kOutput, params,
+ android::AudioDevice::Default(), AAUDIO_USAGE_MEDIA);
+ wrapper.reset();
+ // We leave the tasks pending. The fixture's `task_environment_` will be
+ // destroyed on teardown, verifying that destroying pending tasks doesn't crash.
+}
+
+} // namespace media
diff --git a/media/audio/BUILD.gn b/media/audio/BUILD.gn
index 1c0c28de..42f2bcc 100644
--- a/media/audio/BUILD.gn
+++ b/media/audio/BUILD.gn
@@ -482,6 +482,7 @@
if (is_android) {
sources += [
+ "android/aaudio_stream_wrapper_unittest.cc",
"android/audio_android_unittest.cc",
"android/audio_device_id_unittest.cc",
"android/audio_device_type_unittest.cc",
Regression Test / PoC
diff --git a/media/audio/android/aaudio_stream_wrapper_unittest.cc b/media/audio/android/aaudio_stream_wrapper_unittest.cc
new file mode 100644
index 0000000..3c46416a
--- /dev/null
+++ b/media/audio/android/aaudio_stream_wrapper_unittest.cc
@@ -0,0 +1,86 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/audio/android/aaudio_stream_wrapper.h"
+
+#include <aaudio/AAudio.h>
+
+#include <memory>
+
+#include "base/android/android_info.h"
+#include "base/test/task_environment.h"
+#include "media/base/audio_parameters.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace media {
+
+namespace {
+
+class MockDataCallback : public AAudioStreamWrapper::DataCallback {
+ public:
+ MockDataCallback() = default;
+ ~MockDataCallback() override = default;
+ bool OnAudioDataRequested(base::span<float> audio_data) override {
+ return true;
+ }
+ void OnError() override {}
+ void OnDeviceChange() override {}
+};
+
+} // namespace
+
+class AAudioStreamWrapperTest : public testing::Test {
+ protected:
+ base::test::SingleThreadTaskEnvironment task_environment_{
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+ MockDataCallback callback_;
+};
+
+TEST_F(AAudioStreamWrapperTest, DeferredHelperOutlivesStreamClose) {
+ AudioParameters params(AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ ChannelLayoutConfig::Stereo(), 48000, 480);
+
+ auto wrapper = std::make_unique<AAudioStreamWrapper>(
+ &callback_, AAudioStreamWrapper::StreamType::kOutput, params,
+ android::AudioDevice::Default(), AAUDIO_USAGE_MEDIA);
+
+ // Destroy the wrapper. This should trigger the deferred teardown on API <= 30.
+ wrapper.reset();
+
+ auto sdk_int = base::android::android_info::sdk_int();
+ if (sdk_int < 31) {
+ // We expect two tasks to be posted:
+ // 1. Close task at T+1s.
+ // 2. Free task at T+1.25s.
+ EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 2u);
+
+ // Fast forward to T+1s (but not 1.25s).
+ task_environment_.FastForwardBy(base::Seconds(1));
+
+ // The close task should have run, but the free task should still be pending.
+ EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 1u);
+
+ // Fast forward the rest.
+ task_environment_.FastForwardBy(base::Milliseconds(250));
+ EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
+ } else {
+ // On API >= 31, destruction should be synchronous, no tasks posted.
+ EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
+ }
+}
+
+TEST_F(AAudioStreamWrapperTest, DestroyWithPendingDeferredTasks) {
+ // This test verifies that if the TaskEnvironment is destroyed (or sequence shut down)
+ // while tasks are pending, it doesn't crash.
+ AudioParameters params(AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ ChannelLayoutConfig::Stereo(), 48000, 480);
+ auto wrapper = std::make_unique<AAudioStreamWrapper>(
+ &callback_, AAudioStreamWrapper::StreamType::kOutput, params,
+ android::AudioDevice::Default(), AAUDIO_USAGE_MEDIA);
+ wrapper.reset();
+ // We leave the tasks pending. The fixture's `task_environment_` will be
+ // destroyed on teardown, verifying that destroying pending tasks doesn't crash.
+}
+
+} // namespace media
Original Bug Report
Potential Browser-Process UAF in AAudioStreamWrapper on Android 10/11
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: A Use-After-Free (UAF) vulnerability exists in the AAudio implementation on Android 10 and 11 (API levels 29 and 30). The time-based mitigation for late audio callbacks is structurally flawed and frees a helper object immediately after closing the stream. An attacker can potentially trigger a late callback to access this freed memory, leading to arbitrary code execution in the browser process.
Affected files:
media/audio/android/aaudio_stream_wrapper.ccmedia/audio/android/aaudio_stream_wrapper.h
Estimated timestamp from git blame: 2024-01-22
Summary
A Use-After-Free (UAF) vulnerability exists in media/audio/android/aaudio_stream_wrapper.cc on Android API levels 29 and 30. The issue stems from a structurally flawed mitigation for a documented Android OS bug (crbug.com/1183255) where audio callbacks can continue to fire after AAudioStream_close() is called.
Because the mitigation’s safety mechanism is purely time-based and fails to properly synchronize the lifetime of the AAudioDestructionHelper with the actual stream closure, a late callback can dereference a freed pointer, allowing an attacker to potentially achieve code execution in the browser process.
Technical Details
On Android versions prior to API 31, AAudioStreamWrapper uses an AAudioDestructionHelper to defer the closure and destruction of an AAudioStream.
- Flawed Mitigation: During
AAudioStreamWrapper::~AAudioStreamWrapper(), ownership of theAAudioDestructionHelperis passed to a delayed task scheduled for 1 second later. - Immediate Free After Close: When the delayed task executes 1 second later, the
~AAudioDestructionHelper()destructor is invoked. This destructor callsAAudioStream_close()and then immediately finishes. The memory backing the helper object is synchronously freed back to PartitionAlloc. - The Race Condition: The 1-second delay successfully separates the stream stop request from the
close()call, but it provides zero margin betweenclose()returning and the helper being freed. If an Android OS audio callback fires afterclose()returns (the exact scenario the code attempts to mitigate), it will access freed memory. - MiraclePtr Bypass: The
AAudioDestructionHelperpointer is registered with the OSlibaaudiolibrary as a barevoid* user_data. Consequently, MiraclePtr (BackupRefPtr) does not track this allocation, and a crash is not guaranteed upon dereference.
Potential Exploitation Steps
Note: These are potential steps based on code analysis; a full Proof-of-Concept has not been run.
- A compromised renderer repeatedly creates and closes low-latency audio streams while artificially saturating the device’s CPU and audio subsystem to maximize callback latency.
- When an
AAudioStreamWrapperis closed, the 1-second delayed task is queued. - During that 1 second, the attacker performs a heap spray via IPCs to the browser process, aiming to reclaim the ~64-byte PartitionAlloc bucket that will hold the
AAudioDestructionHelperonce it is freed. - The delayed task executes, closing the stream and freeing the helper.
- The attacker reclaims the freed memory and writes a forged payload. Crucially, they zero out the memory representing the
base::Lock lock_. On Android,base::Lockwrapspthread_mutex_t, and the Bionic libc treats a zeroed structure as a valid, unlocked mutex. - A late
OnAudioDataRequestedCallbackexecutes, passing the dangling pointer toGetAndLockWrapper(). The lock succeeds, and the function returns an attacker-controlledwrapper_pointer. - The callback statically dispatches to
AAudioStreamWrapper::OnAudioDataRequested()using the fake wrapper. Inside, it callscallback_->OnAudioDataRequested(...). - Because
DataCallback::OnAudioDataRequestedis a virtual method, this results in a vtable hijack, redirecting execution to an attacker-controlled address. As this occurs in the browser process, it results in a full sandbox escape.
Suggested Fix
The 1-second delay mitigation is insufficient. To fix this:
- The
AAudioStream_close()call should occur before the 1-second delay, so the delay actually pads the time after closure while keeping the helper alive. - Alternatively, use a more robust synchronization primitive, such as a thread-safe reference count (
base::RefCountedThreadSafe) for the helper object, ensuring it is only destroyed once all callbacks have verifiably ceased.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.