← WebKit Silent-Fix Report — 2026-W21

4193d2c83f  REGRESSION (301011@main): Crash in MediaPlayerPrivateMediaSourceAVFObjC::setNetworkState during destruction

severity high class UAF confidence 0.82 WebCore Media (MSE AVFObjC) exploitable-grade
David Kilzer Wed May 20 21:41:03 2026 -0700 full: 4193d2c83f3aec995d9b8d77ee81297727bc5b1a bug report ↗ view on GitHub ↗
Primitive: re-entrant WeakPtr callback into partially-destroyed player
Triage note: Adds weakPtrFactory().revokeAll() in the destructor to prevent setNetworkState() re-entrancy touching already-destroyed members — a classic use-after-free during teardown.
Contents

The bug at a glance

This is a use-after-free during destruction of MediaPlayerPrivateMediaSourceAVFObjC, a WebCore media backend used by Media Source Extensions on macOS/iOS. During teardown the destruction of m_mediaSourcePrivate can fire synchronous KVO notifications that re-enter the object through a WeakPtr-guarded callback (the AVFoundation error path calling setNetworkState()), which touches members such as m_logger that C++ has already destroyed because they precede m_mediaSourcePrivate in reverse declaration order. Severity is high because MSE is reachable from ordinary web content and UAF during teardown is a classic exploitable primitive; it is a REGRESSION (introduced at 301011@main) and the window is a narrow, timing-dependent teardown re-entrancy, which somewhat caps reliability.

Media Source Extensions let any web page create a MediaSource, attach it to a <video> element, and feed media segments; under the hood Safari uses MediaPlayerPrivateMediaSourceAVFObjC wrapping AVFoundation objects that emit KVO (key-value observing) notifications. When the page tears the player down (navigation, element removal, source ending), AVFoundation can synchronously deliver an error/state KVO callback mid-destruction, and because the object still answers WeakPtr requests, that callback runs against a half-destroyed C++ object.

Root cause

The broken invariant is that a WeakPtr must not resolve to an object that is being destroyed. In WTF, a CanMakeWeakPtr object holds a WeakPtrFactory whose control block is only cleared when the CanMakeWeakPtr base subobject is destroyed — and base-class destructors run AFTER all derived-class member destructors. So during the body of ~MediaPlayerPrivateMediaSourceAVFObjC and throughout member destruction, WeakPtr::get() on this object still returns a valid-looking pointer even though members have already been freed.

C++ destroys non-static data members in reverse declaration order. Here m_logger is declared before m_mediaSourcePrivate, so m_logger is destroyed first, then m_mediaSourcePrivate. The destructor of m_mediaSourcePrivate tears down AVFoundation-backed state, and that teardown can trigger synchronous KVO notifications. Those notifications are dispatched to WeakPtr-guarded callbacks on the player; the AVFoundation error path in particular invokes setNetworkState(), which reads m_logger (via the logging macros / ALWAYS_LOG). Because the WeakPtr still resolves (the CanMakeWeakPtr base is not yet destroyed) but m_logger has already been destructed, setNetworkState() dereferences a destroyed member — a use-after-free.

The fix adds weakPtrFactory().revokeAll() as the first statement of the destructor. revokeAll() invalidates the factory’s control block immediately, so every outstanding WeakPtr to the object becomes null from that point on. Any KVO notification fired later during member destruction now finds its WeakPtr guard resolving to null and the callback is skipped rather than re-entering the dying object. This closes the window between entering the destructor and the eventual CanMakeWeakPtr base destructor. The commit is explicitly a regression fix for 301011@main, meaning that change introduced (or exposed) the re-entrant KVO teardown path.

Key code

The fix: revoke all weak pointers at the start of the destructor so KVO re-entrancy can’t resolve a WeakPtr into the half-destroyed player (MediaPlayerPrivateMediaSourceAVFObjC.mm).

    ALWAYS_LOG(LOGIDENTIFIER);

+    // Prevent re-entrant callbacks during member destruction. Without this,
+    // KVO notifications fired during m_mediaSourcePrivate's destruction can
+    // call back into this partially-destroyed object via WeakPtr-guarded
+    // callbacks (such as the error callback calling setNetworkState(),
+    // which accesses the already-destroyed m_logger).
+    weakPtrFactory().revokeAll();
+
    cancelPendingSeek();

Patch walkthrough

  • Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaSourceAVFObjC.mm — Inserts weakPtrFactory().revokeAll() at the very top of ~MediaPlayerPrivateMediaSourceAVFObjC (immediately after the ALWAYS_LOG entry line, before cancelPendingSeek() and the rest of teardown). This nulls all outstanding WeakPtrs to the player before any member is destroyed, so a synchronous KVO notification fired during m_mediaSourcePrivate’s destruction cannot re-enter setNetworkState() and touch already-destroyed members like m_logger. Without it, the CanMakeWeakPtr base destructor (which normally invalidates weak refs) runs only after all members are gone, leaving a UAF window.

Background

WeakPtr / CanMakeWeakPtr / WeakPtrFactory — WTF’s weak-pointer facility: a class inherits CanMakeWeakPtr, which embeds a WeakPtrFactory holding a shared control block. WeakPtr::get() returns the object pointer while the control block is live and nullptr once it is invalidated. The control block is normally invalidated by the CanMakeWeakPtr destructor. revokeAll() invalidates it on demand, which is the standard way to defensively sever weak references at the start of teardown.

C++ member destruction order vs base destruction — When an object is destroyed, its own destructor body runs first, then its non-static data members are destroyed in reverse declaration order, then base-class subobjects are destroyed. Because the CanMakeWeakPtr base is destroyed last, its automatic weak-ref invalidation happens after every derived member is already gone — so any callback that runs during member destruction and consults a WeakPtr sees a live weak ref pointing at freed member storage.

KVO (Key-Value Observing) re-entrancy — AVFoundation objects notify observers of property changes synchronously via Cocoa KVO. Destroying an AVFoundation-backed object (here reached through m_mediaSourcePrivate’s destructor) can push a final state/error change that is delivered synchronously on the same thread, re-entering WebKit code before the destructor has finished. This turns teardown into a re-entrant control flow that must be hardened against touching partially-destroyed state.

MediaPlayerPrivateMediaSourceAVFObjC / setNetworkState() — This is the WebCore MediaPlayer backend for Media Source Extensions on Apple platforms, wrapping AVSampleBufferRenderSynchronizer/AVSampleBufferDisplayLayer and related AVFoundation objects. setNetworkState() updates the player’s network state and logs via the shared logger; when invoked from an error KVO callback during destruction it reads m_logger, which is why a destroyed m_logger is the concrete UAF site named in the commit.

MSE attack surface — Media Source Extensions is a web API: pages create a MediaSource, get a SourceBuffer, and append encoded media, driving these native player backends. Because lifecycle transitions (end-of-stream, detach, navigation) are all script-triggerable, web content can steer when the player is destroyed and thereby race the KVO teardown callback, making the destruction UAF web-reachable rather than an internal-only edge case.

REGRESSION 301011@main — The commit is tagged a regression from 301011@main, meaning a prior change altered the teardown or error-callback path so that the KVO notification now re-enters during destruction. Regression provenance is useful for defenders scoping which shipped versions are affected and for auditors looking at what that earlier change reordered.

Vulnerability window

  1. Playback — A page uses MSE: a MediaSource is attached to a media element and a MediaPlayerPrivateMediaSourceAVFObjC with AVFoundation-backed members and a WeakPtrFactory is created.
  2. Teardown begins — Script or navigation causes the player to be destroyed; ~MediaPlayerPrivateMediaSourceAVFObjC begins running.
  3. Member destruction — Members destruct in reverse declaration order: m_logger is destroyed, then m_mediaSourcePrivate begins its destructor.
  4. KVO re-entry — m_mediaSourcePrivate’s destruction fires a synchronous AVFoundation KVO error notification that dispatches to a WeakPtr-guarded callback on the player.
  5. WeakPtr still resolves (bug) — Because the CanMakeWeakPtr base isn’t destroyed yet, the WeakPtr resolves; the callback calls setNetworkState(), which reads the already-destroyed m_logger — use-after-free.
  6. Post-fix — weakPtrFactory().revokeAll() at the top of the destructor nulls all weak refs first, so the KVO callback’s WeakPtr guard resolves to null and the re-entrant call is skipped.

Triggering

The patch adds no regression test. To trigger, drive MSE playback (attach a MediaSource to a <video>, append segments) to instantiate MediaPlayerPrivateMediaSourceAVFObjC, then force destruction (detach the source / navigate / remove the element) at a moment when AVFoundation will emit a synchronous error KVO notification during m_mediaSourcePrivate teardown — e.g. by ending the stream in an error state so the render synchronizer’s error property changes during shutdown. On a pre-patch build the KVO callback re-enters setNetworkState() and dereferences the freed m_logger (crash/UAF under ASAN); post-patch revokeAll() suppresses the callback. Reliable reproduction is timing-dependent because it races the destructor against the synchronous KVO delivery.

Exploitation

  1. Realize the object — Use MSE from web content to create and run a MediaPlayerPrivateMediaSourceAVFObjC, controlling media state so an error path is armed on the AVFoundation objects.
  2. Force teardown re-entry — Trigger player destruction (navigation, element removal, source end-of-stream in error) so m_mediaSourcePrivate’s destructor fires a synchronous KVO notification mid-teardown.
  3. Obtain the UAF — The WeakPtr-guarded error callback re-enters setNetworkState() and accesses m_logger after it was destroyed, yielding a use-after-free on freed member storage.
  4. Toward control — Weaponizing requires spraying to reoccupy the freed m_logger (or other freed member) storage with attacker-controlled data before the re-entrant access, converting the stale read/use into a controlled dereference; the tight, timing-dependent window makes this a hard, likely crash-first primitive.

Detection & hunting

For defenders and SOC / detection engineers:

  • ASAN UAF in destructor
  • Crash telemetry
  • Version exposure

Audit directions

  • Narrow grep
  • Re-entrant teardown class
  • Declaration-order hazards
  • Regression provenance

Before / after

Loading diff…