CVE-2026-10006
Overview
Background
- `OfflineAudioDestinationHandler`
- the Blink handler that drives rendering for an
OfflineAudioContext, owning the render thread and its task runner (offline_audio_destination_handler.h:24). - `render_thread_task_runner_`
- the task runner to which rendering work is posted; it targets either a dedicated offline render thread or the AudioWorklet backing thread (
offline_audio_destination_handler.h:139). - `PrepareTaskRunnerForRendering`
- the method that selects/updates
render_thread_task_runner_based on whether anAudioWorkletis active (offline_audio_destination_handler.cc:338-372). - `PrepareTaskRunnerForWorklet`
- an
AudioDestinationHandlerhook invoked when a worklet becomes ready; empty in the offline handler pre-fix (offline_audio_destination_handler.h:50).
Root Cause Analysis
Pre-fix, the offline handler overrode PrepareTaskRunnerForWorklet() override {} as a no-op in the header (offline_audio_destination_handler.h:50), so when a worklet became ready while the context was suspended, nothing updated render_thread_task_runner_. PrepareTaskRunnerForRendering is the method that actually (re)selects the task runner: when an AudioWorklet is ready it assigns the runner from audio_worklet->GetMessagingProxy()->GetBackingWorkerThread()->GetTaskRunner(...) (offline_audio_destination_handler.cc:342-360), otherwise it creates the offline render thread and uses its runner (:362-367). StartRendering posts the rendering task to whatever render_thread_task_runner_ currently holds (offline_audio_destination_handler.cc:110-123). The invariant required for memory safety is that once an AudioWorklet is active, rendering (and thus AudioWorkletProcessor::Process) must run on the AudioWorklet thread that owns the v8::Isolate; but because PrepareTaskRunnerForWorklet was empty, a worklet added during suspension left render_thread_task_runner_ pointing at the offline render thread, so on resume() rendering ran on the wrong thread and accessed the isolate concurrently with the worklet thread.
The fix implements OfflineAudioDestinationHandler::PrepareTaskRunnerForWorklet to CHECK(IsMainThread()) and call PrepareTaskRunnerForRendering() (offline_audio_destination_handler.cc diff), updating the runner to the worklet thread so rendering resumes on the correct thread. The commit additionally hardens AudioWorkletProcessor::Create (audio_worklet_processor.cc:429) and AudioWorkletProcessor::Process (audio_worklet_processor.cc:469) by upgrading DCHECK(global_scope->IsContextThread())/DCHECK(global_scope_->IsContextThread()) to CHECK, so wrong-thread execution aborts in release builds rather than proceeding into concurrent isolate use.
PrepareTaskRunnerForRendering restores the single-owner-thread invariant for the v8::Isolate.Attack Path
- Suspend an offline context
Script creates an
OfflineAudioContext, schedules a suspension, and starts rendering so it pauses at a frame. (inferred trigger per reporter) - Add a worklet during suspension
While suspended, script loads an AudioWorklet module and connects an
AudioWorkletNode, making the worklet ready;PrepareTaskRunnerForWorkletdoes nothing (offline_audio_destination_handler.h:50). - Resume on the stale runner
resume()leads toStartRendering, which posts rendering to the unchangedrender_thread_task_runner_(the offline render thread) (offline_audio_destination_handler.cc:110-123). - Concurrent isolate access
AudioWorkletProcessor::Processenters the worklet’sv8::Isolateon the offline render thread while the AudioWorklet thread is also active, racing on isolate state (audio_worklet_processor.cc:462-474). (inferred that the worklet thread is concurrently busy)
Impact Assessment
v8::Isolate and potential heap corruption in the renderer process. The advisory rates this a high-severity race with no in-the-wild exploitation. Escalation from the race to code execution is (inferred); the shown code demonstrates the missing task-runner update and the thread-identity checks.Files Changed
third_party/blink/renderer/modules/webaudio/audio_worklet_processor.ccthird_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.ccthird_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.h
Audit Directions
- Empty virtual overridesTreat a
{}override of a base hook as suspect; verify the derived class does not need the base behavior (here updating the task runner). - Thread-affinity invariants for V8 isolatesWhere code assumes single-thread isolate ownership without
v8::Locker, enforce it withCHECK(IsContextThread())rather thanDCHECK. - Task-runner selection across state transitionsAudit suspend/resume paths to ensure the active task runner is re-evaluated when the operating mode (e.g. worklet activation) changes.
Patch
From 1b860cdc44d0160f4c3eb0d3f4ebcde981803877 Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@google.com>
Date: Wed, 20 May 2026 07:05:48 -0700
Subject: [PATCH] [webaudio] Fix race condition in OfflineAudioDestinationHandler
OfflineAudioDestinationHandler fails to update the render task runner
when an AudioWorklet is added during context suspension, causing
rendering to execute on the incorrect thread (the OfflineAudioRender
thread instead of the AudioWorkletThread). This leads to concurrent
V8 isolate access and potential heap corruption.
This CL:
1. Declares and implements PrepareTaskRunnerForWorklet in
OfflineAudioDestinationHandler to update the task runner.
2. Hardens thread checks in AudioWorkletProcessor by upgrading
DCHECK to CHECK.
Bug: 513750691
Test: blink_unittests --gtest_filter="*OfflineAudioContext*:*AudioWorkletGlobalScope*"
Change-Id: I0c1e081325e80c942e574b8c0dd6cce69101615b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7856956
Commit-Queue: Hongchan Choi <hongchan@chromium.org>
Reviewed-by: Michael Wilson <mjwilson@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1633568}
---
diff --git a/third_party/blink/renderer/modules/webaudio/audio_worklet_processor.cc b/third_party/blink/renderer/modules/webaudio/audio_worklet_processor.cc
index 9d4533f..4b35cc4 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_worklet_processor.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_worklet_processor.cc
@@ -426,7 +426,7 @@
ExceptionState& exception_state) {
AudioWorkletGlobalScope* global_scope = To<AudioWorkletGlobalScope>(context);
DCHECK(global_scope);
- DCHECK(global_scope->IsContextThread());
+ CHECK(global_scope->IsContextThread());
// Get the stored initialization parameter from the global scope.
std::unique_ptr<ProcessorCreationParams> params =
@@ -466,7 +466,7 @@
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("audio-worklet"),
"AudioWorkletProcessor::Process");
- DCHECK(global_scope_->IsContextThread());
+ CHECK(global_scope_->IsContextThread());
DCHECK(!hasErrorOccurred());
ScriptState* script_state =
diff --git a/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc b/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
index 8331e44..13bbd23 100644
--- a/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
+++ b/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
@@ -371,6 +371,15 @@
DCHECK(render_thread_task_runner_);
}
+void OfflineAudioDestinationHandler::PrepareTaskRunnerForWorklet() {
+ CHECK(IsMainThread());
+
+ // Updates the render task runner to the AudioWorklet backing thread when a
+ // worklet becomes ready while the context is suspended, ensuring that
+ // rendering resumes on the correct thread.
+ PrepareTaskRunnerForRendering();
+}
+
void OfflineAudioDestinationHandler::RestartRendering() {
DCHECK(IsMainThread());
diff --git a/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.h b/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.h
index 461facf..b3c9dd5ba 100644
--- a/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.h
+++ b/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.h
@@ -47,7 +47,7 @@
void Pause() override;
void Resume() override;
uint32_t MaxChannelCount() const override;
- void PrepareTaskRunnerForWorklet() override {}
+ void PrepareTaskRunnerForWorklet() override;
void RestartRendering() override;
Original Bug Report
Concurrent V8 Isolate Access in OfflineAudioDestinationHandler
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential race condition in OfflineAudioDestinationHandler fails to update the render task runner when an AudioWorklet is added during context suspension. This can cause worklet processing to execute on the incorrect thread, leading to concurrent V8 isolate access and heap corruption. This flaw could potentially be exploited for remote code execution in the renderer process.
Affected files:
third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.hthird_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.ccthird_party/blink/renderer/modules/webaudio/base_audio_context.ccthird_party/blink/renderer/modules/webaudio/offline_audio_context.ccthird_party/blink/renderer/modules/webaudio/audio_worklet_handler.ccthird_party/blink/renderer/modules/webaudio/audio_worklet_processor.cc
Estimated timestamp from git blame: 2023-01-25
Summary
A potential vulnerability exists in the OfflineAudioDestinationHandler within Blink’s Web Audio implementation. When an AudioWorklet module is loaded into an OfflineAudioContext while it is in a suspended state, the destination handler fails to update its internal rendering task runner. Consequently, upon resuming rendering, the AudioWorkletProcessor::Process method may execute on the incorrect thread (the OfflineAudioRender thread instead of the designated AudioWorkletThread), leading to concurrent access to a v8::Isolate from multiple threads.
Potential Root Cause
The issue originates in third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.h, where the PrepareTaskRunnerForWorklet() method is overridden with an empty implementation:
// line 50
void PrepareTaskRunnerForWorklet() override {}
When an AudioWorklet becomes ready while the context is suspended, BaseAudioContext::NotifyWorkletIsReady() invokes this method. In the real-time destination handler, this method updates the render task runner. However, in the offline handler, this method does nothing, leaving render_thread_task_runner_ pointing to the initial OfflineAudioRender thread.
When resume() is subsequently called on the context, OfflineAudioContext::resumeContext() calls StartRendering(), which posts the rendering task (DoOfflineRendering) to the stale render_thread_task_runner_ without re-evaluating the active task runner.
Vulnerability Mechanism
As rendering proceeds on the incorrect thread, AudioWorkletHandler::Process() eventually calls AudioWorkletProcessor::Process(). This method enters the V8 isolate using ScriptState::Scope, which constructs a v8::HandleScope and enters the V8 context.
Since Blink does not utilize v8::Locker for AudioWorklets, V8 assumes the isolate is only accessed by its owner thread (the AudioWorkletThread). If the AudioWorkletThread is simultaneously active (e.g., handling MessagePort messages), the offline render thread will concurrently mutate isolate-specific data, such as HandleScope data and heap allocation pointers, leading to memory corruption.
Potential Impact
Successful exploitation could result in severe memory corruption within the V8 heap, potentially enabling an attacker to achieve arbitrary code execution within the sandboxed renderer process.
Suggested Steps to Reproduce (Potential)
- Create an
OfflineAudioContextand callsuspend(0)followed bystartRendering(). This initializes the offline render thread and suspends rendering at the first frame. - Use
audioWorklet.addModule()to load a worklet script. - Create an
AudioWorkletNodeand connect it to the destination. - Call
resume()on the context while simultaneously triggering activity on the worklet thread (e.g., viapostMessageto the node’s port). - Observe if the worklet’s processing logic executes concurrently with the worklet thread, potentially causing a crash or V8 corruption.
Suggested Fix
Implement OfflineAudioDestinationHandler::PrepareTaskRunnerForWorklet() to call PrepareTaskRunnerForRendering(), ensuring the render_thread_task_runner_ is correctly updated when a worklet becomes ready during suspension.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.