[4] Unkeyed AudioHardwareListener memoization outlives its client
Every caller got the first caller's listener, and it never left.
High. A process-wide singleton caches a listener bound to whichever client happened to call first and then never releases it, so three renderer-reachable IPC handlers can dispatch virtually into that client long after it is gone. The escalation gate is reclaiming the freed client before the next hardware-change message arrives.
Callback registration in WebKit's media plumbing normally carries an implicit contract: the object receiving notifications must not outlive the object it notifies. AudioHardwareListener is WebCore's abstraction for "tell me when audio hardware becomes active, inactive, or changes output device," and concrete instances are produced through a process-global creation function that the UI process installs at startup. In the UI process that function routes into RemoteMediaSessionManagerProxy, a process-wide singleton that also serves RemoteMediaSessionManager* IPC messages from content processes.
The angle: a content process can send an audio-hardware-change message and have the UI process make a virtual call through a stale client reference held alive by a cache that no client owns.
Pre-fix, the factory ignored its own parameter — it built one listener on first call and returned that same instance to every caller thereafter:
Source/WebKit/UIProcess/Media/RemoteMediaSessionManagerProxy.cpp
// ensureAudioHardwareListenerProxy(Client& client) ignored `client`:
// first call constructed one RemoteMediaSessionManagerAudioHardwareListener,
// stored it in a strong RefPtr m_audioHardwareListenerProxy, and returned
// that same instance to every subsequent caller.
And the listener held its sink as a bare reference:
Source/WebCore/platform/audio/AudioHardwareListener.h
Client& m_client;
Patch Details
Caching now happens only when the requesting client is the singleton proxy itself, established by the new &client == static_cast<WebCore::AudioHardwareListener::Client*>(this) comparison; every other client receives a fresh listener the proxy does not retain. The cache handle is demoted from a strong RefPtr to a ThreadSafeWeakPtr. m_client becomes a WeakPtr<Client> that each dispatch point upgrades to a RefPtr before calling. Making Client an AbstractRefCountedAndCanMakeWeakPtr is what forces RemoteAudioHardwareListenerProxy to become RefCounted and the GPU-process map to hold Ref rather than unique_ptr.
One hunk is hardening rather than a bug fix: MediaSessionManagerCocoa::audioOutputDeviceChanged() already began with an early return on a null m_audioHardwareListener, so that path could not dereference null before the patch. The change hoists the member into a protecting local RefPtr so the listener cannot be released between the check and the later supportedBufferSizes() / updateSessionState() calls.
Memoized object that captures caller-specific state but is not keyed on the caller's identity, retained by an owner that outlives every caller.
Background
Where this lives. RemoteMediaSessionManagerProxy is a UI-process singleton subclassing WebCore's MediaSessionManagerCocoa, and it doubles as the receiver for RemoteMediaSessionManager* IPC from content processes. The source shows a private constructor with singleton() returning a NeverDestroyed<Ref<RemoteMediaSessionManagerProxy>>, plus singletonWeakPtr() and singletonIfCreated() accessors — the proxy is effectively immortal for the life of the process.
Process-global creation functions. WebCore abstractions like AudioHardwareListener are constructed through a function pointer that the embedding process installs, which lets the UI process substitute a remote-aware implementation without WebCore knowing about IPC. AudioHardwareListener::create(client) funnels through whatever the proxy's constructor installed.
WeakPtr upgrade versus raw reference. A WeakPtr<T> does not keep its referent alive but goes null when the referent dies; upgrading it to a RefPtr at the call site both tests liveness and pins the object for the call. A raw T& does neither — it is a lifetime assertion with no enforcement.
AbstractRefCountedAndCanMakeWeakPtr. This is the WebKit base that gives an abstract interface both refcounting and weak-pointer support, so an interface-typed member can be held weakly and upgraded. Requiring it of Client is what makes the WeakPtr<Client> storage possible at all, and it propagates ownership requirements to every implementer.
Analysis
Two invariants were missing at once: a memoized object that captures caller-specific state must be keyed on that caller's identity, and a listener must not outlive the client it dispatches into unless it holds that client weakly. The bug class is use-after-free via virtual dispatch through a dangling raw reference, in the UI process.
first client singleton proxy later client
──────────── ─────────────── ────────────
create(A) ──────────► build listener{m_client=A}
cache in strong RefPtr
return listener ──────────► create(B)
~A() cache still holds it ─────► same listener (m_client=A)
▲ A freed (immortal owner,
listener lives never released)
IPC: remoteAudioOutputDeviceChanged
└─► m_client.audioOutputDeviceChanged()
▲ virtual call into freed A
Two distinct defects fall out of that single cached instance. The aliasing defect is visible directly in the pre-fix function body: the first create() call in the process permanently fixed the listener's raw m_client to whichever Client called first, so a second client's create() returned a listener dispatching into the first client. The lifetime defect is worse — because m_audioHardwareListenerProxy was a strong RefPtr owned by the immortal singleton, neither the first client's destruction nor MediaSessionManagerCocoa releasing its own m_audioHardwareListener could destroy the cached listener.
Reachability is the part that elevates this. Three IPC handlers — remoteAudioHardwareDidBecomeActive, remoteAudioHardwareDidBecomeInactive, and remoteAudioOutputDeviceChanged — reach the surviving cached listener directly from a content-process message and execute m_client.audioOutputDeviceChanged(), a virtual call through a reference to freed memory once the bound client is gone. That is an attacker-timed trigger for a dangling dereference in the privileged UI process.
The identity comparison is itself evidence that non-singleton clients reach this factory — there would be nothing to distinguish otherwise — though which classes those are is not identified here, so the exact set of clients whose destruction opens the window stays open.
This vulnerability weakens UI-process memory safety across the content-to-UI IPC boundary, with the freed access landing on a virtual dispatch rather than a data read.
Audit directions
- Unkeyed memoization of caller-bound objects. A factory that takes a client parameter, caches its result, and returns the cache on subsequent calls is only correct if the cached object captures nothing about the caller. The tell in review is a function whose first statement is
if (m_cached) return m_cached;while its signature takes a parameter that the constructor consumes — the parameter is being silently discarded on every call after the first. Start with otherensure*Proxy(Client&)factories on the media session proxies. - Immortal singletons holding strong references to mortal collaborators. A
NeverDestroyedsingleton withRefPtrmembers turns those members into leaked-but-live objects. Audit singleton-held caches for handles that should be weak, especially where the cached object stores a back-reference to something short-lived; the demotion this commit makes (RefPtr→ThreadSafeWeakPtr) is the shape of the fix to look for elsewhere. - Raw
Client&members on listener/observer types. Any callback interface stored as a bare reference asserts a lifetime relationship the type system is not checking. Sweep WebCore platform abstractions forClient& m_clientand ask what guarantees the ordering; the review tell is a header declaring a reference member alongside acreate()that does not take ownership of the referent.