High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in WebAudio
DescriptionMemory corruption in WebAudio
ComponentWebAudio
Sub-componentWebAudio
Bug ClassUAF
Tracker572191610
Fix commitc26b38a137c5 (chromium/src) +85/-5
CISA KEVNot listed
CreditedXinyang Ge (Anthropic), assisted by Claude
Disclosed2026-10-27

Background

`RealtimeAudioDestinationHandler`
the Blink WebAudio node that bridges the audio graph to the platform audio device (AudioDestination), driving Render() on the real-time audio thread.
`AudioDestination` states
the platform destination has kRunning, kPaused, and kStopped. Freezing a frame pauses it (kPaused, IsPlaying() == false); thawing resumes it.
`StopPlatformDestination()`
tears down platform audio when the context is closed; it is supposed to transition the destination to kStopped so a later lifecycle Resume() cannot restart it.
`ProcessAutomaticPullNodes()`
pulls “automatic” nodes (e.g. AnalyserNode) that are not connected to the destination. It iterates rendering_automatic_pull_handlers_, a list that can retain upstream handlers.
Oilpan / disposed nodes
WebAudio nodes are garbage-collected (Member<>/GarbageCollected). When an upstream node (e.g. DelayNode) is collected, its handler is “disposed”; touching it afterward is a use-after-free of the disposed object’s resources.
`DisablePullingAudioGraph()`
marks the graph as not-to-be-pulled; IsPullingAudioGraph() then returns false.

Root Cause Analysis

The advisory says “Memory corruption in WebAudio” (MemoryCorruption); the diff shows the specific flavor is a use-after-free of disposed Oilpan audio handlers, driven by two cooperating defects in realtime_audio_destination_handler.cc. First, StopPlatformDestination() guarded the Stop() call with if (platform_destination_->IsPlaying()). When an AudioContext is closed while its frame is frozen, the platform destination is already kPaused (not playing), so Stop() was skipped and the destination stayed in kPaused rather than moving to kStopped. Second, Render() unconditionally called context->GetDeferredTaskHandler().ProcessAutomaticPullNodes(number_of_frames) after the graph-pulling branch, i.e. even when pulling the audio graph was disabled.

The combination is fatal: closing the frozen context calls DisablePullingAudioGraph() but leaves the destination merely paused; when the frame later thaws, the lifecycle Resume() restarts the audio thread on the now-closed context because the destination was never stopped. Render() then runs with pulling disabled and still invokes ProcessAutomaticPullNodes(), which walks rendering_automatic_pull_handlers_. As the new test demonstrates (it connects a DelayNode into an AnalyserNode, flushes the automatic-pull list, drops the DelayNode, and forces WebHeap::CollectAllGarbageForTesting()), that list can still reference an upstream handler whose backing node has been garbage-collected and disposed. Pulling the disposed node is the use-after-free. The fix makes StopPlatformDestination() call platform_destination_->Stop() whenever platform_destination_ is present (so kPaused → kStopped, and a later Resume() cannot restart a stopped destination), and moves ProcessAutomaticPullNodes() inside the active graph-pulling branch of Render() so it never runs once pulling is disabled.

Key insight
Closing a frozen AudioContext left the platform destination kPaused instead of kStopped, so a thaw-driven Resume() restarted rendering on the closed context, and Render() processed automatic pull nodes even with graph-pulling disabled — pulling a disposed, garbage-collected upstream handler: a use-after-free.

Attack Path

  1. Web attacker script creates an AudioContext and wires an automatic-pull node (e.g. AnalyserNode) fed by an upstream node such as DelayNode (inferred: standard WebAudio API, web-reachable).
  2. Freeze the frame the page is frozen (bfcache/lifecycle freeze), putting the platform destination into kPaused.
  3. Close while frozen audioContext.close() runs StopPlatformDestination(), which (pre-patch) skips Stop() because the destination is not playing, and disables graph pulling.
  4. Drop the upstream node release references to the upstream node so Oilpan GC disposes its handler while it remains in rendering_automatic_pull_handlers_.
  5. Thaw the frame resumes; lifecycle Resume() restarts the audio thread on the closed context because the destination was never stopped.
  6. Primitive Render() calls ProcessAutomaticPullNodes() with pulling disabled and dereferences the disposed handler: a use-after-free on the real-time audio thread (freed-memory access; escalation to controlled corruption is (inferred)).

Impact Assessment

The bug is in the renderer process, reachable by any page via the standard WebAudio API with no special privilege. The primitive is a use-after-free of a disposed Oilpan audio handler occurring on the audio thread after context close + frame thaw. The diff proves the lifetime violation (the test explicitly collects garbage to free the upstream DelayNode and then runs Render() under the graph lock); it does not demonstrate a concrete read/write-what-where, so weaponization to RCE is (inferred). Preconditions (freeze → close → GC → thaw) are all script- and lifecycle-reachable, making this a credible renderer UAF rather than a mere crash.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/webaudio/realtime_audio_destination_handler.cc
modified
TEST_F
third_party/blink/renderer/modules/webaudio/audio_context_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/webaudio/realtime_audio_destination_handler.cc
  • third_party/blink/renderer/modules/webaudio/audio_context_test.cc

Audit Directions

  • State-gated teardown guards
    look for other if (IsPlaying())/if (running) guards around Stop()/cleanup that skip teardown for paused/frozen states, leaving objects restartable.
  • Unconditional post-render work
    review other handlers that run graph/post-processing (ProcessAutomaticPullNodes, HandlePostRenderTasks) outside the “is the graph active” check.
  • Automatic pull node lifetime
    audit rendering_automatic_pull_handlers_ / UpdateAutomaticPullNodes() for windows where the list retains handlers whose nodes can be collected (graph lock held, context closed).
  • Freeze/close/thaw interplay
    examine other media and worklet subsystems for lifecycle Resume() paths that can restart activity on an already-closed object.
From c26b38a137c5f813e0bc01d3781be5a16cb25638 Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@google.com>
Date: Tue, 13 Oct 2026 15:16:57 -0700
Subject: [PATCH] [WebAudio] Stop paused destination and guard automatic pull on close

When an AudioContext is closed while its frame is frozen, the platform
audio destination is in the kPaused state (`IsPlaying()` is false), so
`StopPlatformDestination()` previously skipped calling `Stop()`. When
the frame later thawed, `Resume()` restarted the audio thread on the
closed context, and `Render()` invoked `ProcessAutomaticPullNodes()`
even when pulling the audio graph was disabled, causing an object
lifetime issue with disposed upstream nodes.

Changes:
- Call `platform_destination_->Stop()` in `StopPlatformDestination()`
  whenever `platform_destination_` is present so `kPaused` destinations
  transition to `kStopped`.
- Move `ProcessAutomaticPullNodes()` inside the active graph-pulling
  branch of `RealtimeAudioDestinationHandler::Render()`.
- Add `AudioContextTest.CloseWhileFrozenStopsDestination` to verify
  closing a frozen context stops the destination and guards automatic
  pull nodes.

Bug: 572191610
Test: blink_unittests --gtest_filter=AudioContextTest.CloseWhileFrozen*
Change-Id: I3126957a0a43ffdce2f541908ad90359e8f0fd6e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8561977
Commit-Queue: Hongchan Choi <hongchan@google.com>
Reviewed-by: Mahesh Kannan <kmaheshb@google.com>
Cr-Commit-Position: refs/heads/main@{#1717977}
---
diff --git a/third_party/blink/renderer/modules/webaudio/realtime_audio_destination_handler.cc b/third_party/blink/renderer/modules/webaudio/realtime_audio_destination_handler.cc
index 532926c..d83f42950 100644
--- a/third_party/blink/renderer/modules/webaudio/realtime_audio_destination_handler.cc
+++ b/third_party/blink/renderer/modules/webaudio/realtime_audio_destination_handler.cc
@@ -279,14 +279,15 @@
       // given `destination_bus` buffer.
       destination_bus->CopyFrom(*rendered_bus);
     }
+
+    // Processes "automatic" nodes that are not connected to anything. This can
+    // be done after copying because it does not affect the rendered result.
+    context->GetDeferredTaskHandler().ProcessAutomaticPullNodes(
+        number_of_frames);
   } else {
     destination_bus->Zero();
   }
 
-  // Processes "automatic" nodes that are not connected to anything. This can
-  // be done after copying because it does not affect the rendered result.
-  context->GetDeferredTaskHandler().ProcessAutomaticPullNodes(number_of_frames);
-
   context->HandlePostRenderTasks();
 
   // Handle audibility before handling the volume multiplier since the volume
@@ -478,7 +479,10 @@
   // for a while. (It may take a bit of time for the destination to stop.)
   DisablePullingAudioGraph();
 
-  if (platform_destination_->IsPlaying()) {
+  // `AudioDestination::Stop()` is a no-op when already stopped, and transitions
+  // both `kRunning` and `kPaused` destinations to `kStopped` so a subsequent
+  // lifecycle `Resume()` cannot restart a stopped destination.
+  if (platform_destination_) {
     platform_destination_->Stop();
   }
 }
diff --git a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
index e429d5c..3cd6230 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
@@ -51,6 +51,7 @@
 #include "third_party/blink/renderer/core/workers/worker_thread.h"
 #include "third_party/blink/renderer/modules/mediastream/sub_capture_target.h"
 #include "third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.h"
+#include "third_party/blink/renderer/modules/webaudio/analyser_node.h"
 #include "third_party/blink/renderer/modules/webaudio/audio_playback_stats.h"
 #include "third_party/blink/renderer/modules/webaudio/audio_worklet.h"
 #include "third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope.h"
@@ -389,6 +390,7 @@
   }
 
   void SetUp() override {
+    web_audio_device_paused_ = false;
     PageTestBase::SetUp(gfx::Size());
     CoreInitializer::GetInstance().ProvideModulesToPage(GetPage(),
                                                         std::string());
@@ -657,6 +659,80 @@
   EXPECT_FALSE(web_audio_device_paused_);
 }
 
+TEST_F(AudioContextTest, CloseWhileFrozenStopsDestination) {
+  AudioContextOptions* options = AudioContextOptions::Create();
+  AudioContext* audio_context = AudioContext::Create(
+      GetFrame().DomWindow(), options, ASSERT_NO_EXCEPTION);
+  ExpectContextBecomesRunningAsync(audio_context);
+  ExpectContextRunning(audio_context);
+
+  RealtimeAudioDestinationHandler* dest_handler =
+      &audio_context->destinationNode()->GetAudioDestinationHandler();
+
+  GetFrame().DomWindow()->SetLifecycleState(
+      mojom::FrameLifecycleState::kFrozen);
+  EXPECT_TRUE(web_audio_device_paused_);
+  EXPECT_FALSE(dest_handler->get_platform_destination_is_playing_for_testing());
+
+  {
+    V8TestingScope scope;
+    audio_context->closeContext(scope.GetScriptState(),
+                                scope.GetExceptionState());
+  }
+  EXPECT_EQ(audio_context->ContextState(), V8AudioContextState::Enum::kClosed);
+  EXPECT_FALSE(audio_context->IsPullingAudioGraph());
+
+  // Thawing the frame after closing must not resume the stopped destination.
+  GetFrame().DomWindow()->SetLifecycleState(
+      mojom::FrameLifecycleState::kRunning);
+  EXPECT_FALSE(dest_handler->get_platform_destination_is_playing_for_testing());
+  EXPECT_TRUE(web_audio_device_paused_);
+
+  // Verify that if Render() runs while pulling the audio graph is disabled,
+  // automatic pull nodes do not pull disposed upstream nodes.
+  FakeAudioThread audio_thread(ThreadType::kRealtimeAudioWorkletThread);
+  Persistent<AnalyserNode> analyser =
+      audio_context->createAnalyser(ASSERT_NO_EXCEPTION);
+  {
+    DelayNode* node = audio_context->createDelay(ASSERT_NO_EXCEPTION);
+    node->connect(analyser.Get(), 0, 0, ASSERT_NO_EXCEPTION);
+
+    // Flush the automatic pull handler list to
+    // `rendering_automatic_pull_handlers_` while the upstream `DelayNode` is
+    // still alive.
+    audio_thread.RunOnAudioThreadWithContext(
+        audio_context, CrossThreadBindOnce(
+                           [](RealtimeAudioDestinationHandler* handler) {
+                             const AudioIOPosition pos{0, 0, 0};
+                             const AudioCallbackMetric metric;
+                             scoped_refptr<AudioBus> bus =
+                                 AudioBus::Create(2, 128, /*allocate=*/true);
+                             handler->Render(bus.get(), 128, pos, metric,
+                                             base::TimeDelta(), {});
+                           },
+                           CrossThreadUnretained(dest_handler)));
+  }
+
+  WebHeap::CollectAllGarbageForTesting();
+
+  {
+    // Hold the graph lock so `UpdateAutomaticPullNodes()` is skipped and
+    // `rendering_automatic_pull_handlers_` retains `AnalyserHandler` after its
+    // upstream `DelayNode` has been collected and disposed.
+    DeferredTaskHandler::GraphAutoLocker locker(
+        audio_context->GetDeferredTaskHandler());
+    audio_thread.RunOnAudioThread(CrossThreadBindOnce(
+        [](RealtimeAudioDestinationHandler* handler) {
+          const AudioIOPosition pos{0, 0, 0};
+          const AudioCallbackMetric metric;
+          scoped_refptr<AudioBus> bus =
+              AudioBus::Create(2, 128, /*allocate=*/true);
+          handler->Render(bus.get(), 128, pos, metric, base::TimeDelta(), {});
+        },
+        CrossThreadUnretained(dest_handler)));
+  }
+}
+
 // Test that the AudioContext correctly handles audibility state transitions
 // and implements the 2-second silence hysteresis.
 TEST_F(AudioContextTest, AudioContextAudibilityHysteresis) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
index e429d5c..3cd6230 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
@@ -51,6 +51,7 @@
 #include "third_party/blink/renderer/core/workers/worker_thread.h"
 #include "third_party/blink/renderer/modules/mediastream/sub_capture_target.h"
 #include "third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.h"
+#include "third_party/blink/renderer/modules/webaudio/analyser_node.h"
 #include "third_party/blink/renderer/modules/webaudio/audio_playback_stats.h"
 #include "third_party/blink/renderer/modules/webaudio/audio_worklet.h"
 #include "third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope.h"
@@ -389,6 +390,7 @@
   }
 
   void SetUp() override {
+    web_audio_device_paused_ = false;
     PageTestBase::SetUp(gfx::Size());
     CoreInitializer::GetInstance().ProvideModulesToPage(GetPage(),
                                                         std::string());
@@ -657,6 +659,80 @@
   EXPECT_FALSE(web_audio_device_paused_);
 }
 
+TEST_F(AudioContextTest, CloseWhileFrozenStopsDestination) {
+  AudioContextOptions* options = AudioContextOptions::Create();
+  AudioContext* audio_context = AudioContext::Create(
+      GetFrame().DomWindow(), options, ASSERT_NO_EXCEPTION);
+  ExpectContextBecomesRunningAsync(audio_context);
+  ExpectContextRunning(audio_context);
+
+  RealtimeAudioDestinationHandler* dest_handler =
+      &audio_context->destinationNode()->GetAudioDestinationHandler();
+
+  GetFrame().DomWindow()->SetLifecycleState(
+      mojom::FrameLifecycleState::kFrozen);
+  EXPECT_TRUE(web_audio_device_paused_);
+  EXPECT_FALSE(dest_handler->get_platform_destination_is_playing_for_testing());
+
+  {
+    V8TestingScope scope;
+    audio_context->closeContext(scope.GetScriptState(),
+                                scope.GetExceptionState());
+  }
+  EXPECT_EQ(audio_context->ContextState(), V8AudioContextState::Enum::kClosed);
+  EXPECT_FALSE(audio_context->IsPullingAudioGraph());
+
+  // Thawing the frame after closing must not resume the stopped destination.
+  GetFrame().DomWindow()->SetLifecycleState(
+      mojom::FrameLifecycleState::kRunning);
+  EXPECT_FALSE(dest_handler->get_platform_destination_is_playing_for_testing());
+  EXPECT_TRUE(web_audio_device_paused_);
+
+  // Verify that if Render() runs while pulling the audio graph is disabled,
+  // automatic pull nodes do not pull disposed upstream nodes.
+  FakeAudioThread audio_thread(ThreadType::kRealtimeAudioWorkletThread);
+  Persistent<AnalyserNode> analyser =
+      audio_context->createAnalyser(ASSERT_NO_EXCEPTION);
+  {
+    DelayNode* node = audio_context->createDelay(ASSERT_NO_EXCEPTION);
+    node->connect(analyser.Get(), 0, 0, ASSERT_NO_EXCEPTION);
+
+    // Flush the automatic pull handler list to
+    // `rendering_automatic_pull_handlers_` while the upstream `DelayNode` is
+    // still alive.
+    audio_thread.RunOnAudioThreadWithContext(
+        audio_context, CrossThreadBindOnce(
+                           [](RealtimeAudioDestinationHandler* handler) {
+                             const AudioIOPosition pos{0, 0, 0};
+                             const AudioCallbackMetric metric;
+                             scoped_refptr<AudioBus> bus =
+                                 AudioBus::Create(2, 128, /*allocate=*/true);
+                             handler->Render(bus.get(), 128, pos, metric,
+                                             base::TimeDelta(), {});
+                           },
+                           CrossThreadUnretained(dest_handler)));
+  }
+
+  WebHeap::CollectAllGarbageForTesting();
+
+  {
+    // Hold the graph lock so `UpdateAutomaticPullNodes()` is skipped and
+    // `rendering_automatic_pull_handlers_` retains `AnalyserHandler` after its
+    // upstream `DelayNode` has been collected and disposed.
+    DeferredTaskHandler::GraphAutoLocker locker(
+        audio_context->GetDeferredTaskHandler());
+    audio_thread.RunOnAudioThread(CrossThreadBindOnce(
+        [](RealtimeAudioDestinationHandler* handler) {
+          const AudioIOPosition pos{0, 0, 0};
+          const AudioCallbackMetric metric;
+          scoped_refptr<AudioBus> bus =
+              AudioBus::Create(2, 128, /*allocate=*/true);
+          handler->Render(bus.get(), 128, pos, metric, base::TimeDelta(), {});
+        },
+        CrossThreadUnretained(dest_handler)));
+  }
+}
+
 // Test that the AudioContext correctly handles audibility state transitions
 // and implements the 2-second silence hysteresis.
 TEST_F(AudioContextTest, AudioContextAudibilityHysteresis) {
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.