CVE-2026-95343
Overview
Background
- `AudioWorkletHandler`
- an
AudioHandlersubclass that bridges the main-threadAudioWorkletNodeand the worklet-threadAudioWorkletProcessor, and whose teardown (~AudioHandler) and graph cleanup are designed to run on the main thread. - `AudioWorkletMessagingProxy`
- the object that marshals processor creation across threads by posting
CreateProcessorOnRenderingThreadto the worklet thread fromCreateProcessor. - `scoped_refptr<AudioWorkletHandler>`
- a reference-counted pointer whose destructor fires on whichever thread drops the final reference, meaning a ref released on the worklet thread destroys the handler there.
- `DeferredTaskHandler::GraphAutoLocker`
- a scoped lock over the audio graph that must be held while handlers are torn down and graph structure is mutated on the main thread.
Root Cause Analysis
AudioWorkletMessagingProxy::CreateProcessor posted CreateProcessorOnRenderingThread to the worklet thread while binding a scoped_refptr<AudioWorkletHandler> into the cross-thread task, but retained no owning reference on the main thread for the duration of that in-flight task. If the main-thread references held by AudioWorkletNode and AudioContext were released while the task was pending, the bound scoped_refptr on the worklet thread became the last reference; when the task finished the ref dropped there, so ~AudioHandler executed on the worklet thread. That violated the invariant that AudioHandler teardown and its associated graph cleanup run only on the main thread under the graph lock, producing a use-after-free of the handler and its graph structures.
The fix inserts the handler into pending_create_processor_handlers_ on the main thread before posting, so the authoritative reference always lives on the main thread; DidCreateProcessor (posted back after creation) and WorkerThreadTerminated (for aborted tasks) then release those handlers on the main thread under DeferredTaskHandler::GraphAutoLocker, guaranteeing orderly, correctly-threaded destruction.
scoped_refptr be the potential last owner of an AudioWorkletHandler, so its destructor could run off the main thread; the fix anchors an authoritative reference in pending_create_processor_handlers_ on the main thread and releases it there under the graph lock.Attack Path
- Instantiate a worklet node
Script constructs an
AudioWorkletNodeon anAudioContext, causingAudioWorkletMessagingProxy::CreateProcessorto postCreateProcessorOnRenderingThreadto the worklet thread with a boundscoped_refptr<AudioWorkletHandler>. - Drop main-thread references while in flight
Script releases the
AudioWorkletNodeandAudioContext(e.g. discards references and forces garbage collection) before the worklet-thread task completes. - Force last-ref release on the worklet thread
When
CreateProcessorOnRenderingThreadfinishes, the boundscoped_refptris the final reference, so~AudioHandlerruns on the worklet thread instead of the main thread. - Trigger the UAF Handler teardown and graph-structure cleanup run without the main-thread graph lock, freeing objects while the main thread may still reference them, yielding a use-after-free exploitable for memory corruption.
Impact Assessment
AudioWorkletHandler and its graph structures off the main thread, gaining a use-after-free in the Blink WebAudio subsystem of the renderer process. Preconditions are only the ability to create an AudioWorkletNode and release the owning AudioWorkletNode/AudioContext references while processor creation is still in flight on the worklet thread, so no special privileges are needed. This corruption is a foundation for further heap manipulation and potential code execution within the renderer sandbox.Changed Functions
| Function | Change | Notes |
|---|---|---|
AudioNodeInputthird_party/blink/renderer/modules/webaudio/audio_worklet_handler.h |
modified | |
AudioWorkletProcessorthird_party/blink/renderer/modules/webaudio/audio_worklet_handler.h |
modified | |
AudioWorkletHandlerthird_party/blink/renderer/modules/webaudio/audio_worklet_handler.h |
modified | |
MODULES_EXPORTthird_party/blink/renderer/modules/webaudio/audio_worklet_handler.h |
modified | |
forthird_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.cc |
modified | |
SingleThreadTaskRunnerthird_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.h |
modified | |
AudioWorkletNodethird_party/blink/renderer/modules/webaudio/audio_worklet_node.h |
modified | |
MODULES_EXPORTthird_party/blink/renderer/modules/webaudio/audio_worklet_node.h |
modified | |
TEST_Fthird_party/blink/renderer/modules/webaudio/audio_context_test.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webaudio/audio_worklet_handler.hthird_party/blink/renderer/modules/webaudio/audio_worklet_node.hthird_party/blink/renderer/modules/webaudio/audio_worklet.hthird_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.ccthird_party/blink/renderer/modules/webaudio/audio_context_test.ccthird_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.h
Audit Directions
- Cross-thread `scoped_refptr` bindingaudit every
PostCrossThreadTask/CrossThreadBindOncethat binds ascoped_refptrto a main-thread-owned object, checking whether the worklet or worker thread can hold the last reference and run the destructor off-thread. - Main-thread teardown invariantsreview
AudioHandlerand other graph objects whose destruction must occur on the main thread underDeferredTaskHandler::GraphAutoLocker, confirming an owning reference is anchored on that thread for the whole lifetime of in-flight tasks. - Worker-termination cleanupinspect proxies and object proxies that post creation tasks to worker/worklet threads for a
WorkerThreadTerminatedpath that releases pending references, since aborted tasks otherwise leak or free objects on the wrong thread.
Patch
From d17f41658bfacf5bb47f6de9445bd3ef9a28674a Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@google.com>
Date: Wed, 16 Sep 2026 14:58:35 -0700
Subject: [PATCH] Manage handler lifetime in AudioWorkletMessagingProxy
AudioWorkletMessagingProxy::CreateProcessor posts a task to the
AudioWorklet thread binding a scoped_refptr<AudioWorkletHandler>.
If the main-thread references to AudioWorkletNode and AudioContext are
released while this creation task is in-flight, the handler teardown
could be initiated from the worker thread when the task finishes.
AudioHandler teardown and associated graph structure cleanup are
designed to execute on the main thread.
This CL manages the handler lifetime in AudioWorkletMessagingProxy:
1. AudioWorkletMessagingProxy retains a reference in
pending_create_processor_handlers_ on the main thread while
CreateProcessor is pending on the worklet thread.
2. After processor creation completes on the worker thread, a completion
task DidCreateProcessor transfers the handler reference back to the
main thread.
3. In DidCreateProcessor and WorkerThreadTerminated, pending handlers
are released on the main thread under DeferredTaskHandler's graph lock
to ensure orderly destruction.
Bug: 559815527
Change-Id: I92b3bd433870f3e5a4b4d798098abb573a23b2f0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8393082
Reviewed-by: Mahesh Kannan <kmaheshb@google.com>
Commit-Queue: Hongchan Choi <hongchan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699285}
---
diff --git a/third_party/blink/renderer/modules/webaudio/audio_worklet.h b/third_party/blink/renderer/modules/webaudio/audio_worklet.h
index b989ef5..58178e9 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_worklet.h
+++ b/third_party/blink/renderer/modules/webaudio/audio_worklet.h
@@ -64,6 +64,8 @@
private:
FRIEND_TEST_ALL_PREFIXES(AudioContextTest, AudioWorkletTerminatedOnClose);
+ FRIEND_TEST_ALL_PREFIXES(AudioContextTest,
+ AudioWorkletCreateProcessorTeardownOnMainThread);
// Implements Worklet
bool NeedsToCreateGlobalScope() final;
diff --git a/third_party/blink/renderer/modules/webaudio/audio_worklet_handler.h b/third_party/blink/renderer/modules/webaudio/audio_worklet_handler.h
index 665fc6be..12b7fee 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_worklet_handler.h
+++ b/third_party/blink/renderer/modules/webaudio/audio_worklet_handler.h
@@ -9,6 +9,7 @@
#include "base/memory/weak_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_worklet_node_options.h"
+#include "third_party/blink/renderer/modules/modules_export.h"
#include "third_party/blink/renderer/modules/webaudio/audio_node.h"
#include "third_party/blink/renderer/modules/webaudio/audio_param_map.h"
#include "third_party/blink/renderer/modules/webaudio/audio_worklet_processor_error_details.h"
@@ -20,7 +21,7 @@
class AudioNodeInput;
class AudioWorkletProcessor;
-class AudioWorkletHandler final : public AudioHandler {
+class MODULES_EXPORT AudioWorkletHandler final : public AudioHandler {
public:
static scoped_refptr<AudioWorkletHandler> Create(
AudioNode&,
@@ -47,6 +48,7 @@
void SetProcessorOnRenderThread(AudioWorkletProcessor*);
bool IsProcessorActive() { return is_processor_active_; }
+ void MarkProcessorInactiveOnMainThread();
private:
AudioWorkletHandler(
@@ -64,8 +66,6 @@
void NotifyProcessorError(
const AudioWorkletProcessorErrorDetails& error_details);
- void MarkProcessorInactiveOnMainThread();
-
bool HasActiveInputs();
void FinishProcessorOnRenderThread();
diff --git a/third_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.cc b/third_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.cc
index cd5031f..e46bae1 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.cc
@@ -19,10 +19,12 @@
#include "third_party/blink/renderer/modules/webaudio/audio_worklet_object_proxy.h"
#include "third_party/blink/renderer/modules/webaudio/audio_worklet_processor.h"
#include "third_party/blink/renderer/modules/webaudio/cross_thread_audio_worklet_processor_info.h"
+#include "third_party/blink/renderer/modules/webaudio/deferred_task_handler.h"
#include "third_party/blink/renderer/modules/webaudio/offline_audio_worklet_thread.h"
#include "third_party/blink/renderer/modules/webaudio/realtime_audio_worklet_thread.h"
#include "third_party/blink/renderer/modules/webaudio/semi_realtime_audio_worklet_thread.h"
#include "third_party/blink/renderer/platform/audio/audio_utilities.h"
+#include "third_party/blink/renderer/platform/heap/cross_thread_handle.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_copier_base.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_copier_public.h"
@@ -33,20 +35,39 @@
AudioWorklet* worklet)
: ThreadedWorkletMessagingProxy(execution_context), worklet_(worklet) {}
+AudioWorkletMessagingProxy::~AudioWorkletMessagingProxy() {
+ DCHECK(pending_create_processor_handlers_.empty());
+}
+
void AudioWorkletMessagingProxy::CreateProcessor(
scoped_refptr<AudioWorkletHandler> handler,
MessagePortChannel message_port_channel,
scoped_refptr<SerializedScriptValue> node_options) {
DCHECK(IsMainThread());
+ if (!GetWorkerThread()) {
+ return;
+ }
+ // Retain a reference on the main thread while processor creation is in
+ // flight on the worklet thread. This ensures the handler lifetime extends
+ // until the completion task returns to the main thread.
+ pending_create_processor_handlers_.insert(handler);
+ scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner =
+ GetParentExecutionContextTaskRunners()->Get(TaskType::kInternalDefault);
+ String name = handler->Name();
PostCrossThreadTask(
*GetWorkerThread()->GetTaskRunner(TaskType::kMiscPlatformAPI), FROM_HERE,
CrossThreadBindOnce(
&AudioWorkletMessagingProxy::CreateProcessorOnRenderingThread,
- CrossThreadUnretained(GetWorkerThread()), handler, handler->Name(),
- std::move(message_port_channel), std::move(node_options)));
+ MakeCrossThreadWeakHandle(this),
+ std::move(main_thread_task_runner),
+ CrossThreadUnretained(GetWorkerThread()), std::move(handler),
+ std::move(name), std::move(message_port_channel),
+ std::move(node_options)));
}
void AudioWorkletMessagingProxy::CreateProcessorOnRenderingThread(
+ CrossThreadWeakHandle<AudioWorkletMessagingProxy> proxy_handle,
+ scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner,
WorkerThread* worker_thread,
scoped_refptr<AudioWorkletHandler> handler,
const String& name,
@@ -58,6 +79,57 @@
AudioWorkletProcessor* processor = global_scope->CreateProcessor(
name, message_port_channel, std::move(node_options));
handler->SetProcessorOnRenderThread(processor);
+
+ // Transfer the handler reference back to the main thread in the completion
+ // task. This guarantees that if this was the last reference, destruction
+ // happens on the main thread rather than on this worker thread.
+ //
+ // Note that this reference is not the authoritative one:
+ // `pending_create_processor_handlers_` on the main thread holds a reference
+ // for the entire duration of the creation task. If this task is discarded
+ // (e.g. the task runner shuts down), the set entry still keeps the handler
+ // alive until `WorkerThreadTerminated()` releases it under the graph lock.
+ PostCrossThreadTask(
+ *main_thread_task_runner, FROM_HERE,
+ CrossThreadBindOnce(&AudioWorkletMessagingProxy::DidCreateProcessor,
+ MakeUnwrappingCrossThreadWeakHandle(proxy_handle),
+ std::move(handler)));
+}
+
+void AudioWorkletMessagingProxy::DidCreateProcessor(
+ scoped_refptr<AudioWorkletHandler> handler) {
+ DCHECK(IsMainThread());
+ // AudioHandler teardown and associated graph modifications must occur
+ // on the main thread under the graph lock. Releasing both the set entry
+ // and the passed parameter here ensures safe destruction if references
+ // dropped.
+ DeferredTaskHandler::GraphAutoLocker locker(
+ handler->GetDeferredTaskHandler());
+ pending_create_processor_handlers_.erase(handler);
+ handler = nullptr;
+}
+
+void AudioWorkletMessagingProxy::WorkerThreadTerminated() {
+ DCHECK(IsMainThread());
+ // Release any handlers whose processor creation was aborted by worker
+ // termination, ensuring their destruction occurs under the graph lock.
+ //
+ // A messaging proxy serves exactly one BaseAudioContext, so all pending
+ // handlers share a single DeferredTaskHandler. That lets one lock cover the
+ // release of the whole set.
+ if (!pending_create_processor_handlers_.empty()) {
+ auto& any_handler = *pending_create_processor_handlers_.begin();
+ DeferredTaskHandler* deferred_task_handler =
+ &any_handler->GetDeferredTaskHandler();
+ DeferredTaskHandler::GraphAutoLocker locker(*deferred_task_handler);
+ for (const auto& pending_handler : pending_create_processor_handlers_) {
+ DCHECK_EQ(&pending_handler->GetDeferredTaskHandler(),
+ deferred_task_handler);
+ pending_handler->MarkProcessorInactiveOnMainThread();
+ }
+ pending_create_processor_handlers_.clear();
+ }
+ ThreadedWorkletMessagingProxy::WorkerThreadTerminated();
}
void AudioWorkletMessagingProxy::SynchronizeWorkletProcessorInfoList(
diff --git a/third_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.h b/third_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.h
index 1cd047e..cbb0439 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.h
+++ b/third_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.h
@@ -9,9 +9,16 @@
#include <optional>
#include "base/gtest_prod_util.h"
+#include "base/memory/scoped_refptr.h"
#include "base/time/time.h"
#include "third_party/blink/renderer/core/workers/threaded_worklet_messaging_proxy.h"
#include "third_party/blink/renderer/modules/modules_export.h"
+#include "third_party/blink/renderer/platform/heap/cross_thread_handle.h"
+#include "third_party/blink/renderer/platform/wtf/hash_set.h"
+
+namespace base {
+class SingleThreadTaskRunner;
+} // namespace base
namespace blink {
@@ -31,6 +38,7 @@
: public ThreadedWorkletMessagingProxy {
public:
AudioWorkletMessagingProxy(ExecutionContext*, AudioWorklet*);
+ ~AudioWorkletMessagingProxy() override;
// Since the creation of AudioWorkletProcessor needs to be done in the
// different thread, this method is a wrapper for cross-thread task posting.
@@ -41,12 +49,16 @@
// Invokes AudioWorkletGlobalScope to create an instance of
// AudioWorkletProcessor.
static void CreateProcessorOnRenderingThread(
+ CrossThreadWeakHandle<AudioWorkletMessagingProxy>,
+ scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner,
WorkerThread*,
scoped_refptr<AudioWorkletHandler>,
const String& name,
MessagePortChannel,
scoped_refptr<SerializedScriptValue> node_options);
+ void WorkerThreadTerminated() override;
+
// Invoked by AudioWorkletObjectProxy on AudioWorkletThread to fetch the
// information from AudioWorkletGlobalScope to AudioWorkletMessagingProxy
// after the script code evaluation. It copies the information about newly
@@ -80,6 +92,13 @@
private:
FRIEND_TEST_ALL_PREFIXES(AudioContextTest, AudioWorkletTerminatedOnClose);
+ FRIEND_TEST_ALL_PREFIXES(AudioContextTest,
+ AudioWorkletCreateProcessorTeardownOnMainThread);
+
+ // Invoked on the main thread after processor creation completes on the
+ // worklet thread. Releases the retained handler reference under the graph
+ // lock.
+ void DidCreateProcessor(scoped_refptr<AudioWorkletHandler>);
// Implements ThreadedWorkletMessagingProxy.
std::unique_ptr<ThreadedWorkletObjectProxy> CreateObjectProxy(
@@ -93,6 +112,13 @@
// Each entry consists of processor name and associated AudioParam list.
HashMap<String, Vector<CrossThreadAudioParamInfo>> processor_info_map_;
+ // Tracks handlers awaiting processor creation on the worklet thread.
+ // Retaining a reference here guarantees that handler teardown and graph
+ // cleanup always execute on the main thread, even if the node is collected
+ // while creation is in flight.
+ HashSet<scoped_refptr<AudioWorkletHandler>>
+ pending_create_processor_handlers_;
+
Member<AudioWorklet> worklet_;
};
diff --git a/third_party/blink/renderer/modules/webaudio/audio_worklet_node.h b/third_party/blink/renderer/modules/webaudio/audio_worklet_node.h
index d272746..63ab9b3 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_worklet_node.h
+++ b/third_party/blink/renderer/modules/webaudio/audio_worklet_node.h
@@ -7,6 +7,7 @@
#include "base/memory/scoped_refptr.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_worklet_node_options.h"
+#include "third_party/blink/renderer/modules/modules_export.h"
#include "third_party/blink/renderer/modules/webaudio/audio_node.h"
#include "third_party/blink/renderer/modules/webaudio/audio_param_map.h"
#include "third_party/blink/renderer/modules/webaudio/audio_worklet_handler.h"
@@ -30,8 +31,9 @@
// [Main Scope] | [AudioWorkletGlobalScope]
// AudioWorkletNode <-> AudioWorkletHandler <==|==> AudioWorkletProcessor
// (JS interface) (Renderer access) | (V8 audio processing)
-class AudioWorkletNode final : public AudioNode,
- public ActiveScriptWrappable<AudioWorkletNode> {
+class MODULES_EXPORT AudioWorkletNode final
+ : public AudioNode,
+ public ActiveScriptWrappable<AudioWorkletNode> {
DEFINE_WRAPPERTYPEINFO();
public:
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 2bc5ac1..f174043 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
@@ -29,9 +29,11 @@
#include "third_party/blink/public/platform/web_runtime_features.h"
#include "third_party/blink/public/web/web_heap.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
+#include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_worklet_options.h"
+#include "third_party/blink/renderer/bindings/core/v8/worker_or_worklet_script_controller.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_sink_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_audiocontextlatencycategory_double.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_audiocontextrendersizecategory_unsignedlong.h"
@@ -41,7 +43,9 @@
#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/messaging/message_channel.h"
#include "third_party/blink/renderer/core/messaging/message_port.h"
+#include "third_party/blink/renderer/core/script/classic_script.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/core/timing/performance.h"
#include "third_party/blink/renderer/core/workers/worker_thread.h"
@@ -49,7 +53,11 @@
#include "third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.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"
+#include "third_party/blink/renderer/modules/webaudio/audio_worklet_handler.h"
#include "third_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.h"
+#include "third_party/blink/renderer/modules/webaudio/audio_worklet_node.h"
+#include "third_party/blink/renderer/modules/webaudio/cross_thread_audio_worklet_processor_info.h"
#include "third_party/blink/renderer/modules/webaudio/delay_node.h"
#include "third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_node.h"
#include "third_party/blink/renderer/modules/webaudio/offline_audio_context.h"
@@ -1555,6 +1563,101 @@
proxy->GetBackingWorkerThread()->WaitForShutdownForTesting();
}
+// Test that releasing the last reference to an AudioWorkletHandler during
+// CreateProcessor teardown safely executes on the main thread instead of
+// running ~AudioHandler on the AudioWorklet thread (which triggers
+// DCHECK(IsMainThread())).
+TEST_F(AudioContextTest, AudioWorkletCreateProcessorTeardownOnMainThread) {
+ AudioContextOptions* options = AudioContextOptions::Create();
+ AudioContext* audio_context = AudioContext::Create(
+ GetFrame().DomWindow(), options, ASSERT_NO_EXCEPTION);
+
+ ScriptState* script_state = ToScriptStateForMainWorld(&GetFrame());
+ ScriptState::Scope scope(script_state);
+
+ auto* audio_worklet = audio_context->audioWorklet();
+ audio_worklet->proxies_.push_back(audio_worklet->CreateGlobalScope());
+
+ auto* proxy = audio_worklet->GetMessagingProxy();
+ ASSERT_TRUE(proxy);
+
+ WorkerThread* worker_thread = proxy->GetBackingWorkerThread();
+ ASSERT_TRUE(worker_thread);
+
+ // Register dummy-processor in the AudioWorkletGlobalScope.
+ base::WaitableEvent registration_event;
+ PostCrossThreadTask(
+ *worker_thread->GetTaskRunner(TaskType::kMiscPlatformAPI), FROM_HERE,
+ CrossThreadBindOnce(
+ [](WorkerThread* thread, base::WaitableEvent* event) {
+ auto* global_scope =
+ To<AudioWorkletGlobalScope>(thread->GlobalScope());
+ ScriptState* script_state =
+ global_scope->ScriptController()->GetScriptState();
+ ScriptState::Scope scope(script_state);
+ String source_code =
+ "class DummyProcessor extends AudioWorkletProcessor {"
+ " process() { return true; }"
+ "};"
+ "registerProcessor('dummy-processor', DummyProcessor);";
+ ClassicScript::CreateUnspecifiedScript(source_code)
+ ->RunScriptOnScriptState(script_state);
+ event->Signal();
+ },
+ CrossThreadUnretained(worker_thread),
+ CrossThreadUnretained(®istration_event)));
+ registration_event.Wait();
+
+ AudioWorkletNodeOptions* node_options = AudioWorkletNodeOptions::Create();
+ auto* node = MakeGarbageCollected<AudioWorkletNode>(
+ *audio_context, "dummy-processor", node_options,
+ Vector<CrossThreadAudioParamInfo>(), nullptr);
+ scoped_refptr<AudioWorkletHandler> handler =
+ WrapRefCounted(&static_cast<AudioWorkletHandler&>(node->Handler()));
+
+ auto* channel = MakeGarbageCollected<MessageChannel>(
+ audio_context->GetExecutionContext());
+ MessagePortChannel processor_port_channel = channel->port2()->Disentangle();
+
+ handler->MarkProcessorInactiveOnMainThread();
+
+ WeakPersistent<AudioWorkletNode> weak_node = node;
+
... (file diff truncated)
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 2bc5ac1..f174043 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
@@ -29,9 +29,11 @@
#include "third_party/blink/public/platform/web_runtime_features.h"
#include "third_party/blink/public/web/web_heap.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
+#include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_worklet_options.h"
+#include "third_party/blink/renderer/bindings/core/v8/worker_or_worklet_script_controller.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_sink_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_audiocontextlatencycategory_double.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_audiocontextrendersizecategory_unsignedlong.h"
@@ -41,7 +43,9 @@
#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/messaging/message_channel.h"
#include "third_party/blink/renderer/core/messaging/message_port.h"
+#include "third_party/blink/renderer/core/script/classic_script.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/core/timing/performance.h"
#include "third_party/blink/renderer/core/workers/worker_thread.h"
@@ -49,7 +53,11 @@
#include "third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.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"
+#include "third_party/blink/renderer/modules/webaudio/audio_worklet_handler.h"
#include "third_party/blink/renderer/modules/webaudio/audio_worklet_messaging_proxy.h"
+#include "third_party/blink/renderer/modules/webaudio/audio_worklet_node.h"
+#include "third_party/blink/renderer/modules/webaudio/cross_thread_audio_worklet_processor_info.h"
#include "third_party/blink/renderer/modules/webaudio/delay_node.h"
#include "third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_node.h"
#include "third_party/blink/renderer/modules/webaudio/offline_audio_context.h"
@@ -1555,6 +1563,101 @@
proxy->GetBackingWorkerThread()->WaitForShutdownForTesting();
}
+// Test that releasing the last reference to an AudioWorkletHandler during
+// CreateProcessor teardown safely executes on the main thread instead of
+// running ~AudioHandler on the AudioWorklet thread (which triggers
+// DCHECK(IsMainThread())).
+TEST_F(AudioContextTest, AudioWorkletCreateProcessorTeardownOnMainThread) {
+ AudioContextOptions* options = AudioContextOptions::Create();
+ AudioContext* audio_context = AudioContext::Create(
+ GetFrame().DomWindow(), options, ASSERT_NO_EXCEPTION);
+
+ ScriptState* script_state = ToScriptStateForMainWorld(&GetFrame());
+ ScriptState::Scope scope(script_state);
+
+ auto* audio_worklet = audio_context->audioWorklet();
+ audio_worklet->proxies_.push_back(audio_worklet->CreateGlobalScope());
+
+ auto* proxy = audio_worklet->GetMessagingProxy();
+ ASSERT_TRUE(proxy);
+
+ WorkerThread* worker_thread = proxy->GetBackingWorkerThread();
+ ASSERT_TRUE(worker_thread);
+
+ // Register dummy-processor in the AudioWorkletGlobalScope.
+ base::WaitableEvent registration_event;
+ PostCrossThreadTask(
+ *worker_thread->GetTaskRunner(TaskType::kMiscPlatformAPI), FROM_HERE,
+ CrossThreadBindOnce(
+ [](WorkerThread* thread, base::WaitableEvent* event) {
+ auto* global_scope =
+ To<AudioWorkletGlobalScope>(thread->GlobalScope());
+ ScriptState* script_state =
+ global_scope->ScriptController()->GetScriptState();
+ ScriptState::Scope scope(script_state);
+ String source_code =
+ "class DummyProcessor extends AudioWorkletProcessor {"
+ " process() { return true; }"
+ "};"
+ "registerProcessor('dummy-processor', DummyProcessor);";
+ ClassicScript::CreateUnspecifiedScript(source_code)
+ ->RunScriptOnScriptState(script_state);
+ event->Signal();
+ },
+ CrossThreadUnretained(worker_thread),
+ CrossThreadUnretained(®istration_event)));
+ registration_event.Wait();
+
+ AudioWorkletNodeOptions* node_options = AudioWorkletNodeOptions::Create();
+ auto* node = MakeGarbageCollected<AudioWorkletNode>(
+ *audio_context, "dummy-processor", node_options,
+ Vector<CrossThreadAudioParamInfo>(), nullptr);
+ scoped_refptr<AudioWorkletHandler> handler =
+ WrapRefCounted(&static_cast<AudioWorkletHandler&>(node->Handler()));
+
+ auto* channel = MakeGarbageCollected<MessageChannel>(
+ audio_context->GetExecutionContext());
+ MessagePortChannel processor_port_channel = channel->port2()->Disentangle();
+
+ handler->MarkProcessorInactiveOnMainThread();
+
+ WeakPersistent<AudioWorkletNode> weak_node = node;
+
+ // Block the worker thread until the main thread has dropped all references.
+ base::WaitableEvent worker_block_event;
+ PostCrossThreadTask(
+ *worker_thread->GetTaskRunner(TaskType::kMiscPlatformAPI), FROM_HERE,
+ CrossThreadBindOnce([](base::WaitableEvent* event) { event->Wait(); },
+ CrossThreadUnretained(&worker_block_event)));
+
+ // Dispatch CreateProcessor and drop all main thread references.
+ proxy->CreateProcessor(handler, std::move(processor_port_channel),
+ SerializedScriptValue::NullValue());
+ EXPECT_EQ(proxy->pending_create_processor_handlers_.size(), 1u);
+ handler = nullptr;
+ node = nullptr;
+ WebHeap::CollectAllGarbageForTesting();
+ EXPECT_EQ(weak_node.Get(), nullptr);
+ audio_context->GetDeferredTaskHandler().ClearHandlersToBeDeleted();
+
+ // Unblock worker thread so CreateProcessor runs.
+ worker_block_event.Signal();
+
+ // Flush the worker thread task queue and run main thread tasks.
+ base::WaitableEvent wait_event;
+ PostCrossThreadTask(
+ *worker_thread->GetTaskRunner(TaskType::kMiscPlatformAPI), FROM_HERE,
+ CrossThreadBindOnce(&base::WaitableEvent::Signal,
+ CrossThreadUnretained(&wait_event)));
+ wait_event.Wait();
+ test::RunPendingTasks();
+ EXPECT_TRUE(proxy->pending_create_processor_handlers_.empty());
+
+ DummyExceptionStateForTesting exception_state;
+ audio_context->closeContext(script_state, exception_state);
+ worker_thread->WaitForShutdownForTesting();
+}
+
// Test that an active AudioWorklet shared MessagePort keeps neither its
// associated AudioWorklet nor the BaseAudioContext alive, and that the port is
// collected after its peer disconnects without an explicit close() call.