← All issues

[CoreIPC] [GPUP] off-main-thread ~RemoteAudioMediaStreamTrackRendererInternalUnitManagerUnit mutates unlocked AudioSession interruption-observer WeakHashSet

a9c37a1

LayoutTests/fast/mediastream/delete-audio-unit.html

promise_test(async () => {
const context = new AudioContext();
const oscillator = context.createOscillator();
const streamDestination = context.createMediaStreamDestination();
oscillator.connect(streamDestination);
oscillator.start();
 
video.srcObject = streamDestination.stream;
await video.play();
await new Promise(resolve => setTimeout(resolve, 100));
 
internals?.deleteAudioUnit(); // triggers IPC delete while unit is running
internals?.setPageMediaVolume(0);
await new Promise(resolve => setTimeout(resolve, 100));
}, "Delete a running audio unit without stopping it");

Source/WebCore/platform/mediastream/cocoa/AudioMediaStreamTrackRendererUnit.cpp

+void AudioMediaStreamTrackRendererUnit::Unit::deleteUnitForTesting()
+{
+ assertIsMainThread();
+ m_internalUnit->deleteUnitForTesting();
+}

WebKit's GPU process hosts RemoteAudioMediaStreamTrackRendererInternalUnitManager, which manages audio rendering units on behalf of the WebProcess. Each unit runs an audio rendering callback on a real-time rendering thread while lifecycle operations (construction, destruction, observer registration) are expected to occur only on the main thread. AudioSession tracks interruption observers in a WeakHashSet — a non-thread-safe structure. This commit fixes a thread-safety race where deleting a running audio unit via IPC could cause its destructor to execute on the rendering thread, triggering an unguarded mutation of that WeakHashSet. The IPC-triggered deletion path did not guarantee which thread would drop the last reference, so if the rendering thread was active the destructor (and its AudioSession::removeObserver() call) could fire there, racing concurrent main-thread access to the same set. The fix ensures the unit is stopped before being released, preventing the rendering thread from holding the last reference; a new internals API deleteAudioUnit() reproduces the race in tests.

BEFORE (racy):
WebProcess         GPUProcess main thread       Rendering thread
    │                      │                           │
    ├──IPC: deleteUnit────►│                           │  ← unit still running
    │              remove unit from map                │
    │              (main drops its ref)                │
    │                                        unit dtor fires here
    │                                   ──► AudioSession::removeObserver()
    │                                        [unlocked WeakHashSet write]

AFTER (fixed):
WebProcess         GPUProcess main thread       Rendering thread
    │                      │                           │
    ├──IPC: deleteUnit────►│                           │
    │                 unit->stop()                     │
    │                 (rendering thread releases ref)  │
    │              remove unit from map                │
    │              (last ref drops on main thread)     │
    │              unit dtor fires on main thread      │
    │         ──► AudioSession::removeObserver() [safe]│

This is a web-reachable race condition in the GPU process: an attacker-controlled MediaStream can trigger the vulnerable code path via IPC timing, potentially corrupting the AudioSession observer map from the wrong thread.

The repair relies on ref-counting discipline: stopping the unit must cause the rendering thread to release its reference before the IPC-side release fires. Investigate whether stop() is guaranteed synchronous with respect to the rendering thread's ref-drop, or whether a window remains. The same ownership pattern applies to other observer registrations and IPC-triggered lifecycle changes in the audio stack — auditing AudioSession observer registration/deregistration callsites for similar off-thread destructor paths is valuable. The new deleteAudioUnit() internals hook also makes this race more reliably reproducible for fuzzing.