← WebKit Silent-Fix Report — 2026-W25

a7ea563d0f  StabilityTracer: Crash in WebKit::RemoteAudioVideoRendererProxyManager::ref

severity medium class UAF confidence 0.70 WebKit GPU Process RemoteAudioVideoRendererProxyManager exploitable-grade
Rupin Mittal Tue Jun 16 08:58:59 2026 -0700 full: a7ea563d0fb8b73b316aefd556610de241ab8d62 bug report ↗ view on GitHub ↗
Primitive: cross-thread WeakPtr use of freed manager
Triage note: Replaces non-threadsafe WeakPtr captured into callbacks that run on other threads, fixing a lifetime/race crash in ref().
Contents

The bug at a glance

This is a cross-thread use-after-free / lifetime race in the GPU process: renderer callbacks captured a non-thread-safe WeakPtr to RemoteAudioVideoRendererProxyManager and ran on other threads, dereferencing it in the window between the owning GPUConnectionToWebProcess reaching refcount zero and its main-thread destruction. A UAF in the GPU process is a serious sandbox-relevant corruption primitive, though the reported artifact is a null-deref crash in ref() surfaced by StabilityTracer, keeping the observed impact at medium. Reachability depends on media-renderer teardown timing rather than a direct scripted trigger.

RemoteAudioVideoRendererProxyManager forwards ref/deref to its owning GPUConnectionToWebProcess, so its lifetime is that connection’s. Lambdas passed to the renderer (setTimeObserver, notifyWhenErrorOccurs, whenSettled, etc.) captured a plain WeakPtr{*this} and reconstructed RefPtr protectedThis on arbitrary threads; when the connection’s refcount hit zero on a non-main thread, the non-thread-safe WeakPtr did not observe it, so protectedThis called ref() on a manager whose backing connection was already dead. The fix routes lifetime through the connection’s ThreadSafeWeakPtrControlBlock so weakThis.get() returns null once refcount is zero.

Root cause

RemoteAudioVideoRendererProxyManager is a unique_ptr member of GPUConnectionToWebProcess and implements ref()/deref() by forwarding to m_gpuConnectionToWebProcess.get()->ref()/deref(). Its effective lifetime is therefore the connection’s. The manager installs many asynchronous callbacks on the underlying WebCore::AudioVideoRenderer – error/first-frame/flush/rendering-mode/size/rate notifications in create(), plus addTrack, requestMediaDataWhenReady, performTaskAtTime, installTimeObserver, prepareToSeek, finishSeek, and notifyWhenHasAvailableVideoFrame – each capturing weakThis = WeakPtr{*this} and, when invoked, building RefPtr protectedThis = weakThis.get().

GPUConnectionToWebProcess derives from ThreadSafeRefCountedAndCanMakeThreadSafeWeakPtr. Its refcount can drop to zero on any thread, but its destructor must run on the main thread; when the count hits zero off-main-thread, destruction is deferred by hopping to the main thread. That deferral opens a window: the object’s refcount is zero and its control block has already nulled its pointer, but ~GPUConnectionToWebProcess() has not yet run, so the manager’s memory is still present.

A plain WTF::WeakPtr keyed to the manager does not participate in the connection’s thread-safe refcount. During that window weakThis.get() still returns a non-null manager pointer, so RefPtr protectedThis = weakThis.get() proceeds to call RemoteAudioVideoRendererProxyManager::ref(), which dereferences m_gpuConnectionToWebProcess.get(). Per the commit, that get() returned null (the control block had already dropped the connection), so ref() crashed on a null dereference – the StabilityTracer signature in ref() reached from the installTimeObserver setTimeObserver lambda. More generally, dereferencing a manager whose owning connection is being torn down is a use-after-free hazard, not merely a null-deref.

The fix ties the manager’s weak references to the connection’s own thread-safe control block. A controlBlock() function is added to RemoteAudioVideoRendererProxyManager returning m_gpuConnectionToWebProcess.get()->controlBlock(), and GPUConnectionToWebProcess exposes controlBlock via using ThreadSafeRefCountedAndCanMakeThreadSafeWeakPtr::controlBlock. Every capturing lambda changes weakThis = WeakPtr{*this} to weakThis = ThreadSafeWeakPtr{*this}. Now weakThis.get() consults the connection’s control block: once the connection’s refcount reaches zero (even before destruction), m_object is null and get() returns nullptr, so protectedThis is null and ref() is never called. Additionally m_gpuConnectionToWebProcess is strengthened from ThreadSafeWeakPtr to ThreadSafeWeakRef, and sharedPreferencesForWebProcess() is simplified to dereference it directly, reflecting the invariant that it should never be null while the manager is alive.

Key code

controlBlock() ties the manager’s weak refs to the connection’s thread-safe control block (RemoteAudioVideoRendererProxyManager.cpp / .h)

ThreadSafeWeakPtrControlBlock& RemoteAudioVideoRendererProxyManager::controlBlock() const
{
    return m_gpuConnectionToWebProcess.get()->controlBlock();
}

// header:
//   ThreadSafeWeakPtrControlBlock& controlBlock() const;
//   ThreadSafeWeakRef<GPUConnectionToWebProcess> m_gpuConnectionToWebProcess;

// example converted capture:
context.renderer->setTimeObserver(interval, [weakThis = ThreadSafeWeakPtr { *this }, identifier](const MediaTime&) {
    RefPtr protectedThis = weakThis.get();
    if (!protectedThis)
        return;

Patch walkthrough

  • Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.cpp — Adds controlBlock() returning m_gpuConnectionToWebProcess.get()->controlBlock(). Rewrites every callback capture from WeakPtr{*this} to ThreadSafeWeakPtr{*this} across create() (error, first-frame, flush-to-resume, rendering-mode, size, rate notifications), addTrack, requestMediaDataWhenReady, performTaskAtTime, installTimeObserver, prepareToSeek, finishSeek, and notifyWhenHasAvailableVideoFrame, so weakThis.get() returns null once the connection’s refcount is zero. Also simplifies sharedPreferencesForWebProcess() to dereference the (now strong-typed) connection directly.
  • Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.h — Declares ThreadSafeWeakPtrControlBlock& controlBlock() const, includes <wtf/ThreadSafeWeakPtr.h>, and changes the member m_gpuConnectionToWebProcess from ThreadSafeWeakPtr<GPUConnectionToWebProcess> to ThreadSafeWeakRef<GPUConnectionToWebProcess>, encoding that the connection is expected to remain non-null for the manager’s lifetime.
  • Source/WebKit/GPUProcess/GPUConnectionToWebProcess.h — Exposes the base control block via using ThreadSafeRefCountedAndCanMakeThreadSafeWeakPtr::controlBlock, so the manager can hand out ThreadSafeWeakPtr{*this} references backed by the connection’s own thread-safe refcount/control block.

Background

ThreadSafeWeakPtr vs WeakPtr — WTF::WeakPtr is a single-thread weak reference whose validity is not synchronized with thread-safe refcounting. ThreadSafeWeakPtr consults a ThreadSafeWeakPtrControlBlock whose object pointer is atomically nulled the instant the refcount reaches zero, so get() from any thread correctly returns null even before the destructor runs – the property this fix relies on.

ThreadSafeRefCountedAndCanMakeThreadSafeWeakPtr — The base of GPUConnectionToWebProcess providing atomic refcounting plus a control block for thread-safe weak pointers. Its refcount may hit zero on any thread while destruction is deferred to the main thread, creating the zero-refcount-but-alive window the callbacks fell into.

Deferred main-thread destruction — Because ~GPUConnectionToWebProcess must run on the main thread, an off-main-thread final deref schedules destruction asynchronously. Code executing in that gap sees an object that is refcount-zero yet not yet freed; a thread-safe weak pointer treats it as gone, a plain WeakPtr does not.

RemoteAudioVideoRendererProxyManager lifetime forwarding — The manager is a unique_ptr member of the connection and forwards ref()/deref() to it, so it has no independent refcount. Its weak references must therefore key off the connection’s control block, which the previous plain-WeakPtr capture failed to do.

ThreadSafeWeakRef — A non-null-by-contract strong-typed thread-safe weak reference. Changing m_gpuConnectionToWebProcess to ThreadSafeWeakRef documents and enforces the expectation that the owning connection is present for the manager’s lifetime, and lets sharedPreferencesForWebProcess() dereference it without a null branch.

Vulnerability window

  1. Original design — Manager callbacks captured plain WeakPtr{*this} and forwarded ref/deref to the owning GPUConnectionToWebProcess, ignoring that the connection’s refcount is thread-safe and can reach zero off the main thread.
  2. Race window — When the connection’s last deref happened off-main-thread, destruction was deferred; during that gap a renderer callback could fire and reconstruct RefPtr from the non-thread-safe WeakPtr, calling ref() on a manager whose connection was already gone.
  3. Crash observed — StabilityTracer reported crashes in RemoteAudioVideoRendererProxyManager::ref() (null m_gpuConnectionToWebProcess.get()), traced to the setTimeObserver lambda in installTimeObserver – bug 317038 / rdar://167378009.
  4. Analysis — Investigation established the zero-refcount-but-not-yet-destroyed window and that a ThreadSafeWeakPtr keyed to the connection’s control block would return null in it.
  5. Fix — Committed as 315283@main: add controlBlock(), convert all captures to ThreadSafeWeakPtr, expose the base control block, and strengthen the member to ThreadSafeWeakRef.

Triggering

No test or PoC is included; the change is a lifetime-safety refactor driven by field crash telemetry (StabilityTracer). Trigger (inferred): drive GPU-process audio/video rendering so RemoteAudioVideoRendererProxyManager installs asynchronous renderer callbacks (time observer, media-data requests, seeks), then cause the owning GPUConnectionToWebProcess to reach refcount zero on a non-main thread (web process/connection teardown) at the moment such a callback fires, so the callback reconstructs a RefPtr through the stale WeakPtr and ref() dereferences the already-released connection. Observed outcome is a GPU-process crash in ref(); it is crash-only in the public record.

Exploitation

  1. Set up renderers — From the web process, drive media playback that causes the GPU process to create renderers and install the manager’s asynchronous callbacks (time observers, seek/flush notifications).
  2. Race teardown — Tear down the GPUConnectionToWebProcess so its refcount hits zero on a non-main thread while a renderer callback is in flight, hitting the deferred-destruction window.
  3. Trigger the stale dereference — The callback reconstructs RefPtr protectedThis from the non-thread-safe WeakPtr and calls ref(), dereferencing the released connection. Observed result is a null-deref/UAF crash in the GPU process.
  4. Note — This is a timing-dependent race with no demonstrated control primitive; realistic impact is a GPU-process crash, and any exploitation would require winning the narrow window with heap grooming – inferred, not shown.

Detection & hunting

For defenders and SOC / detection engineers:

  • Crash in RemoteAudioVideoRendererProxyManager::ref
  • Off-main-thread final deref of GPUConnectionToWebProcess
  • Plain WeakPtr captured into cross-thread callbacks

Audit directions

  • GPU-process proxy managers forwarding ref/deref
  • Cross-thread lambda captures
  • controlBlock() forwarding correctness
  • ThreadSafeWeakRef non-null invariant

Before / after

Loading diff…