Medium watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Audio
DescriptionUse after free in Audio
ComponentAudio
Sub-componentGardener
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker501331457
Fix commit79ae7859fd23 (chromium/src) +125/-5
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-10 (per vendor advisory)
Fix landed2026-07-22 (public git)
Disclosed2026-08-25
Patch gapNot computed: independent review: commit does not match.

Background

`AAudioStreamWrapper`
Chrome’s Android wrapper around an AAudioStream; AAudio invokes data and error callbacks on its own real-time thread and passes a user_data pointer back to Chrome.
`AAudioDestructionHelper`
the object bound as the callbacks’ user_data. During teardown it takes ownership of the AAudioStream (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_ptr to 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.

Key insight
The deferred-teardown workaround delayed the close but then closed the stream and freed the callback context in the same step, which defeated its own stated purpose: AAudio may still call back after close. The fix separates “close stream” from “free callback context” with a strictly later destruction delay on the same sequence.

Attack Path

  1. 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.
  2. Tear the stream down The page stops, pauses or destroys the audio sink, which destroys the AAudioStreamWrapper on an affected device (API < 31) and schedules the deferred helper teardown.
  3. Close and free together At T+1 s the pre-patch task closed the stream inside ~AAudioDestructionHelper() and freed the helper immediately.
  4. 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

The advisory rates this a medium-severity use-after-free in Audio. It affects only Android devices below API 31 that use the AAudio path, and it depends on a timing race with the platform’s callback thread, which the attacker controls only loosely by repeatedly creating and destroying streams. Memory corruption in the process hosting the audio stack is the demonstrated consequence; any further escalation is (inferred), and the advisory does not report in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
if
media/audio/android/aaudio_stream_wrapper.cc
modified
MockDataCallback
media/audio/android/aaudio_stream_wrapper_unittest.cc
modified
AAudioStreamWrapperTest
media/audio/android/aaudio_stream_wrapper_unittest.cc
modified
TEST_F
media/audio/android/aaudio_stream_wrapper_unittest.cc
modified
if
media/audio/android/aaudio_stream_wrapper_unittest.cc
modified
if
media/audio/BUILD.gn
modified

Files Changed

  • media/audio/BUILD.gn
  • media/audio/android/aaudio_stream_wrapper.cc
  • media/audio/android/aaudio_stream_wrapper_unittest.cc

Audit Directions

  • Close-then-free on third-party callback threads
    Look 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_data happen together; platform close calls do not always guarantee that callbacks have quiesced.
  • Delay-based lifetime workarounds
    Audit PostDelayedTask + DoNothingWithBoundArgs keep-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 tasks
    Check that every Unretained pointer bound into a delayed task is guaranteed by sequence ordering, as here, and that shutdown-time task dropping cannot reorder or skip the guarantor.
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",
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • media/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.

  1. Flawed Mitigation: During AAudioStreamWrapper::~AAudioStreamWrapper(), ownership of the AAudioDestructionHelper is passed to a delayed task scheduled for 1 second later.
  2. Immediate Free After Close: When the delayed task executes 1 second later, the ~AAudioDestructionHelper() destructor is invoked. This destructor calls AAudioStream_close() and then immediately finishes. The memory backing the helper object is synchronously freed back to PartitionAlloc.
  3. The Race Condition: The 1-second delay successfully separates the stream stop request from the close() call, but it provides zero margin between close() returning and the helper being freed. If an Android OS audio callback fires after close() returns (the exact scenario the code attempts to mitigate), it will access freed memory.
  4. MiraclePtr Bypass: The AAudioDestructionHelper pointer is registered with the OS libaaudio library as a bare void* 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.

  1. 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.
  2. When an AAudioStreamWrapper is closed, the 1-second delayed task is queued.
  3. 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 AAudioDestructionHelper once it is freed.
  4. The delayed task executes, closing the stream and freeing the helper.
  5. 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::Lock wraps pthread_mutex_t, and the Bionic libc treats a zeroed structure as a valid, unlocked mutex.
  6. A late OnAudioDataRequestedCallback executes, passing the dangling pointer to GetAndLockWrapper(). The lock succeeds, and the function returns an attacker-controlled wrapper_ pointer.
  7. The callback statically dispatches to AAudioStreamWrapper::OnAudioDataRequested() using the fake wrapper. Inside, it calls callback_->OnAudioDataRequested(...).
  8. Because DataCallback::OnAudioDataRequested is 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:

  1. 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.
  2. 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.

View on issue tracker
Links in the report