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

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebAudio
DescriptionUse after free in WebAudio
ComponentWebAudio
Sub-componentWebAudio
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.
Tracker497859275
Fix commit469cda6c11c2 (chromium/src) +32/-1
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-04-01 (public git)
Disclosed2026-05-05
Patch gap34 days — fix public in source before the advisory

Background

`AudioNode` / `AudioHandler`
the garbage-collected (Oilpan) JavaScript-facing WebAudio node object and its separately ref-counted (scoped_refptr) processing handler that runs on the real-time audio rendering thread.
Orphan handler list
a list owned by DeferredTaskHandler (AddRenderingOrphanHandler) that keeps an AudioHandler alive while the audio graph is still pulling data, so the rendering thread never touches a freed handler.
Context interruption (`kInterrupted`)
an AudioContext state (entered via StartContextInterruption) distinct from kSuspended, used when audio is temporarily halted but may be resumed.
Disposal path
when an AudioNode is garbage-collected, it hands its handler_ off; the handler must be orphaned (retained) if the graph could still reference it.

Root Cause Analysis

When an AudioNode is disposed, the code in audio_node.cc decides whether to move its handler_ into the DeferredTaskHandler’s rendering-orphan list, which keeps the handler alive for the audio rendering thread. The pre-patch condition only retained the handler when context()->IsPullingAudioGraph() or the context state was V8AudioContextState::Enum::kSuspended; it did not account for the kInterrupted state. If a node was disposed (and then garbage-collected) while the context was interrupted, the handler was not added to the orphan list, so its last scoped_refptr dropped and the AudioHandler was freed even though the audio graph could still reference it upon resumption.

The fix adds || context()->ContextState() == V8AudioContextState::Enum::kInterrupted to the condition so an interrupted context also orphans (retains) the handler. The regression test DisposeOrphansHandlerWhenInterrupted confirms this by interrupting the context, creating and dropping a DelayNode, forcing GC, and asserting the handler is still referenced (EXPECT_FALSE(handler->HasOneRef())).

Key insight
The disposal guard enumerated specific “keep the handler alive” context states but omitted kInterrupted, so a handler could be destroyed while still reachable by the rendering thread; the fix closes the gap by treating kInterrupted the same as kSuspended/pulling.

Attack Path

  1. Create and interrupt a context Script creates an AudioContext and drives it into the kInterrupted state.
  2. Spawn and drop a node Script creates an AudioNode (e.g. a DelayNode) and removes all JavaScript references so it becomes eligible for garbage collection.
  3. Dispose without orphaning During GC the node is disposed; because the state is kInterrupted, the pre-patch condition skips AddRenderingOrphanHandler, and the AudioHandler’s final reference is released and the object freed.
  4. Dangling handler use The audio rendering path (or context resumption) subsequently accesses the freed AudioHandler, producing a use-after-free (inferred — the diff shows the missing retention, not the specific downstream dereference).

Impact Assessment

An attacker-controlled page using WebAudio can free an AudioHandler that the audio rendering thread may still reference, yielding a use-after-free in the Blink renderer process. The precondition is driving an AudioContext into the kInterrupted state and disposing a node during that window. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; any escalation to a read/write primitive or code execution is (inferred) and not demonstrated by the diff.

Changed Functions

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

Files Changed

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

Audit Directions

  • State-enumerating lifetime guards
    Audit any retain/release decision that switches on a subset of V8AudioContextState or similar enums; newly added states (like kInterrupted) are easy to omit from older conditions.
  • GC object vs. ref-counted companion
    Review Oilpan objects that own scoped_refptr members handed to another thread; confirm every disposal path transfers ownership to a keep-alive list before the GC object dies.
  • Rendering-thread reachability
    Trace every AudioHandler destruction and verify the real-time audio thread cannot still hold or dereference it across suspension, interruption, and resume transitions.
From 469cda6c11c283a6b101ddacbd0351172a0bfbb9 Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@chromium.org>
Date: Wed, 01 Apr 2026 11:11:33 -0700
Subject: [PATCH] [WebAudio] Correct AudioNode disposal state check

Ensure AudioHandler objects are correctly orphaned when disposing an
AudioNode during an interrupted state. This prevents potential memory
lifetime issues.

Bug: 497859275
Change-Id: I229c39171a6112ae4cd3aa9d34d311d90e53ab91
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718446
Reviewed-by: Michael Wilson <mjwilson@chromium.org>
Commit-Queue: Hongchan Choi <hongchan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1608668}
---

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 24c2a6f..cc4bdc69 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 
 #include "third_party/blink/renderer/modules/webaudio/audio_context.h"
+#include "third_party/blink/public/web/web_heap.h"
+#include "third_party/blink/renderer/modules/webaudio/delay_node.h"
 
 #include <array>
 #include <memory>
@@ -397,6 +399,34 @@
       mock_media_devices_dispatcher_host_;
 };
 
+TEST_F(AudioContextTest, DisposeOrphansHandlerWhenInterrupted) {
+  AudioContextOptions* options = AudioContextOptions::Create();
+  AudioContext* context = AudioContext::Create(
+      GetFrame().DomWindow(), options, ASSERT_NO_EXCEPTION);
+
+  context->StartContextInterruption();
+
+  scoped_refptr<AudioHandler> handler;
+  {
+    DelayNode* node = context->createDelay(ASSERT_NO_EXCEPTION);
+    handler = &node->Handler();
+  }
+
+  // Before GC, handler should have 1 ref (held by us, as the node itself is
+  // still alive in Oilpan but we have no Persistent handle to it).
+  // In Oilpan tests, a garbage collection cycle must be forced to destroy
+  // the node.
+  WebHeap::CollectAllGarbageForTesting();
+
+  // If the node was orphaned, its handler will be added to the
+  // DeferredTaskHandler's orphan list, meaning it will have at least one ref
+  // there, plus our 'handler' ref. (2 refs)
+  //
+  // If it was NOT orphaned, it would have been destroyed if we didn't hold it,
+  // OR it has only our ref. (HasOneRef() == true) Thus this will fail.
+  EXPECT_FALSE(handler->HasOneRef());
+}
+
 TEST_F(AudioContextTest, AudioContextOptions_WebAudioLatencyHint) {
   AudioContextOptions* interactive_options = AudioContextOptions::Create();
   interactive_options->setLatencyHint(
diff --git a/third_party/blink/renderer/modules/webaudio/audio_node.cc b/third_party/blink/renderer/modules/webaudio/audio_node.cc
index 98fb23b4..92fec78 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_node.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_node.cc
@@ -79,7 +79,8 @@
   // the handler still needs to be added in case the context is resumed.
   DCHECK(context());
   if (context()->IsPullingAudioGraph() ||
-      context()->ContextState() == V8AudioContextState::Enum::kSuspended) {
+      context()->ContextState() == V8AudioContextState::Enum::kSuspended ||
+      context()->ContextState() == V8AudioContextState::Enum::kInterrupted) {
     context()->GetDeferredTaskHandler().AddRenderingOrphanHandler(
         std::move(handler_));
   }
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 24c2a6f..cc4bdc69 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 
 #include "third_party/blink/renderer/modules/webaudio/audio_context.h"
+#include "third_party/blink/public/web/web_heap.h"
+#include "third_party/blink/renderer/modules/webaudio/delay_node.h"
 
 #include <array>
 #include <memory>
@@ -397,6 +399,34 @@
       mock_media_devices_dispatcher_host_;
 };
 
+TEST_F(AudioContextTest, DisposeOrphansHandlerWhenInterrupted) {
+  AudioContextOptions* options = AudioContextOptions::Create();
+  AudioContext* context = AudioContext::Create(
+      GetFrame().DomWindow(), options, ASSERT_NO_EXCEPTION);
+
+  context->StartContextInterruption();
+
+  scoped_refptr<AudioHandler> handler;
+  {
+    DelayNode* node = context->createDelay(ASSERT_NO_EXCEPTION);
+    handler = &node->Handler();
+  }
+
+  // Before GC, handler should have 1 ref (held by us, as the node itself is
+  // still alive in Oilpan but we have no Persistent handle to it).
+  // In Oilpan tests, a garbage collection cycle must be forced to destroy
+  // the node.
+  WebHeap::CollectAllGarbageForTesting();
+
+  // If the node was orphaned, its handler will be added to the
+  // DeferredTaskHandler's orphan list, meaning it will have at least one ref
+  // there, plus our 'handler' ref. (2 refs)
+  //
+  // If it was NOT orphaned, it would have been destroyed if we didn't hold it,
+  // OR it has only our ref. (HasOneRef() == true) Thus this will fail.
+  EXPECT_FALSE(handler->HasOneRef());
+}
+
 TEST_F(AudioContextTest, AudioContextOptions_WebAudioLatencyHint) {
   AudioContextOptions* interactive_options = AudioContextOptions::Create();
   interactive_options->setLatencyHint(
Loading diff…

Original Bug Report

reported by vm...@google.com

Use-After-Free in WebAudio AudioNode::Dispose() during kInterrupted state

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A potential Use-After-Free (UAF) vulnerability exists in Blink’s WebAudio module when an AudioContext enters the kInterrupted state. During this state, garbage collection of an AudioNode can lead to the premature destruction of its AudioHandler and AudioNodeOutput, leaving a dangling pointer in the audio thread’s rendering graph that can be exploited for RCE.

Affected files:

  • third_party/blink/renderer/modules/webaudio/audio_node.cc
  • third_party/blink/renderer/modules/webaudio/audio_summing_junction.h
  • third_party/blink/renderer/modules/webaudio/audio_node_input.cc
  • third_party/blink/renderer/modules/webaudio/audio_context.cc

Estimated timestamp from git blame: 2024-10-10

Description

A potential Use-After-Free (UAF) vulnerability has been identified in the Blink WebAudio module. The issue arises because AudioNode::Dispose() fails to account for the kInterrupted state when deciding whether to keep an AudioHandler alive for the audio rendering thread.

When an AudioContext is interrupted (e.g., by hiding an iframe with a restrictive media-playback-while-not-visible policy), the context stops pulling the audio graph and its state changes to kInterrupted. If an AudioNode is garbage collected while the context is in this state, its underlying AudioHandler and associated AudioNodeOutput objects are destroyed immediately. However, downstream AudioNodeInput objects maintain a rendering_outputs_ vector containing raw pointers to these outputs. By contesting the audio graph lock from the main thread, an attacker can prevent the audio thread from updating this vector, causing it to dereference the freed AudioNodeOutput upon resuming the context.

Root Cause Analysis

In third_party/blink/renderer/modules/webaudio/audio_node.cc, the Dispose() method manages the lifetime of the underlying AudioHandler:

if (context()->IsPullingAudioGraph() ||
    context()->ContextState() == V8AudioContextState::Enum::kSuspended) {
    context()->GetDeferredTaskHandler().AddRenderingOrphanHandler(
        std::move(handler_));
}

When the context is in the kInterrupted state, IsPullingAudioGraph() returns false and the state is not kSuspended. Consequently, handler_ is not added to the rendering orphan list. When the AudioNode’s destructor runs, handler_ is set to nullptr, dropping its refcount to 0.

This destroys the AudioHandler and its AudioNodeOutput (allocated via USING_FAST_MALLOC). Crucially, the audio thread’s AudioSummingJunction::rendering_outputs_ (a Vector<AudioNodeOutput*>) still contains a raw pointer to this freed object. This vector is only updated when DeferredTaskHandler::HandleDeferredTasks() is called, which requires acquiring the graph lock.

Potential Exploitation Steps

(Note: These are suggested steps based on code analysis; a working Proof of Concept has not yet been executed by our tooling.)

  1. Setup: An attacker creates an AudioContext and connects an AudioNode (e.g., a GainNode) to the destination.
  2. Trigger Interruption: The attacker hides the iframe containing the context, triggering StartContextInterruption(). The state becomes kInterrupted and allow_pulling_audio_graph_ becomes false.
  3. Garbage Collection: The attacker removes JavaScript references to the GainNode and forces a garbage collection. AudioNode::Dispose() runs but fails to orphan the handler. The AudioNodeOutput is freed.
  4. Heap Spray: The attacker sprays the PartitionAlloc heap to reclaim the freed AudioNodeOutput memory. They overwrite the handler_ field (a raw_ref<AudioHandler>) to point to a fake, attacker-controlled AudioHandler object with a fake vtable. (MiraclePtr/BRP checks pass because the raw_ref pointer value itself was overwritten to point to a valid, attacker-controlled allocation).
  5. Resumption and Race: The attacker makes the iframe visible again, which resumes the context and allows the audio thread to pull the graph. Concurrently, the attacker runs a tight loop on the main thread calling connect()/disconnect() on dummy nodes to heavily contest the graph lock.
  6. Execution: The audio thread’s TryLock() in HandlePreRenderTasks() fails due to the lock contention. It skips HandleDeferredTasks(), leaving rendering_outputs_ un-updated. The audio thread then dereferences the dangling pointer in rendering_outputs_ and calls output->Pull(). This invokes Handler().ProcessIfNecessary(), which performs a virtual method call using the attacker’s fake vtable, leading to Remote Code Execution (RCE) in the Renderer process.

Suggested Fix

Update the condition in AudioNode::Dispose() to explicitly include the kInterrupted state, ensuring the handler is properly orphaned if the context might be resumed later.

if (context()->IsPullingAudioGraph() ||
    context()->ContextState() == V8AudioContextState::Enum::kSuspended ||
    context()->ContextState() == V8AudioContextState::Enum::kInterrupted) {
    context()->GetDeferredTaskHandler().AddRenderingOrphanHandler(
        std::move(handler_));
}

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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