CVE-2026-7980
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/modules/webaudio/audio_context_test.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webaudio/audio_context_test.ccthird_party/blink/renderer/modules/webaudio/audio_node.cc
Patch
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_));
}
Regression Test / PoC
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(
Original Bug Report
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.ccthird_party/blink/renderer/modules/webaudio/audio_summing_junction.hthird_party/blink/renderer/modules/webaudio/audio_node_input.ccthird_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.)
- Setup: An attacker creates an
AudioContextand connects anAudioNode(e.g., aGainNode) to the destination. - Trigger Interruption: The attacker hides the iframe containing the context, triggering
StartContextInterruption(). The state becomeskInterruptedandallow_pulling_audio_graph_becomesfalse. - Garbage Collection: The attacker removes JavaScript references to the
GainNodeand forces a garbage collection.AudioNode::Dispose()runs but fails to orphan the handler. TheAudioNodeOutputis freed. - Heap Spray: The attacker sprays the PartitionAlloc heap to reclaim the freed
AudioNodeOutputmemory. They overwrite thehandler_field (araw_ref<AudioHandler>) to point to a fake, attacker-controlledAudioHandlerobject with a fake vtable. (MiraclePtr/BRP checks pass because theraw_refpointer value itself was overwritten to point to a valid, attacker-controlled allocation). - 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. - Execution: The audio thread’s
TryLock()inHandlePreRenderTasks()fails due to the lock contention. It skipsHandleDeferredTasks(), leavingrendering_outputs_un-updated. The audio thread then dereferences the dangling pointer inrendering_outputs_and callsoutput->Pull(). This invokesHandler().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.