High watch ✔︎ verified CVSS 7.5 chrome Race 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace in WebAudio
DescriptionRace in WebAudio
ComponentWebAudio
Sub-componentWebaudio
Bug ClassRace
Action tierwatch — ub-class only — no demonstrated corruption path — factors set by independent review by AI
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.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker513750691
Fix commit1b860cdc44d0 (chromium/src) +12/-3
CWECWE-362 (Race condition)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-16 (per vendor advisory)
Fix landed2026-05-20 (public git)
Disclosed2026-05-27
Patch gap7 days — fix public in source before the advisory

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 an AudioWorklet is active (offline_audio_destination_handler.cc:338-372).
`PrepareTaskRunnerForWorklet`
an AudioDestinationHandler hook 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.

Key insight
The offline destination handler’s worklet-ready hook was a no-op, so it never switched rendering to the AudioWorklet thread; implementing it to call PrepareTaskRunnerForRendering restores the single-owner-thread invariant for the v8::Isolate.

Attack Path

  1. Suspend an offline context Script creates an OfflineAudioContext, schedules a suspension, and starts rendering so it pauses at a frame. (inferred trigger per reporter)
  2. Add a worklet during suspension While suspended, script loads an AudioWorklet module and connects an AudioWorkletNode, making the worklet ready; PrepareTaskRunnerForWorklet does nothing (offline_audio_destination_handler.h:50).
  3. Resume on the stale runner resume() leads to StartRendering, which posts rendering to the unchanged render_thread_task_runner_ (the offline render thread) (offline_audio_destination_handler.cc:110-123).
  4. Concurrent isolate access AudioWorkletProcessor::Process enters the worklet’s v8::Isolate on 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

An attacker-controlled page could cause Web Audio worklet processing to run on the wrong thread, producing concurrent access to a 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.cc
  • third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
  • third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.h

Audit Directions

  • Empty virtual overrides
    Treat 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 isolates
    Where code assumes single-thread isolate ownership without v8::Locker, enforce it with CHECK(IsContextThread()) rather than DCHECK.
  • Task-runner selection across state transitions
    Audit suspend/resume paths to ensure the active task runner is re-evaluated when the operating mode (e.g. worklet activation) changes.
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;
 
Loading diff…

Original Bug Report

reported by vm...@google.com

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.h
  • third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
  • third_party/blink/renderer/modules/webaudio/base_audio_context.cc
  • third_party/blink/renderer/modules/webaudio/offline_audio_context.cc
  • third_party/blink/renderer/modules/webaudio/audio_worklet_handler.cc
  • third_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)

  1. Create an OfflineAudioContext and call suspend(0) followed by startRendering(). This initializes the offline render thread and suspends rendering at the first frame.
  2. Use audioWorklet.addModule() to load a worklet script.
  3. Create an AudioWorkletNode and connect it to the destination.
  4. Call resume() on the context while simultaneously triggering activity on the worklet thread (e.g., via postMessage to the node’s port).
  5. 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.

View on issue tracker