CVE-2024-54551
Overview
Background
- PlatformMediaSessionManager
- WebCore singleton-like coordinator that tracks media sessions and schedules audio-session and now-playing state updates on the main thread.
- callOnMainThread
- A WTF helper that queues a lambda to run later on the main run loop; captured raw pointers are not lifetime-managed and can dangle if their target is freed first.
- TaskCancellationGroup / CancellableTask
- A WTF mechanism that associates queued tasks with a group so they can all be cancelled at once (e.g. in a destructor), turning pending tasks into no-ops.
- Use-after-free (UAF)
- A memory-safety error where memory is accessed after it has been freed, here via a stale captured ’this’ in a deferred task.
- Deferred-task lifetime race
- A bug pattern where an object schedules asynchronous work capturing itself and is destroyed before the work runs, leaving the callback holding a dangling reference.
Root Cause Analysis
PlatformMediaSessionManager (and its Cocoa subclass MediaSessionManagerCocoa) schedules deferred work on the main thread using callOnMainThread with lambdas that capture the raw ’this’ pointer (or a raw session pointer) — for example in sessionCanProduceAudioChanged, scheduleUpdateSessionState, MediaSessionManagerCocoa::scheduleSessionStatusUpdate, and the two callOnMainThread blocks in sessionWillEndPlayback. The invariant these captures assume is that the manager (or session) still exists when the queued task runs. That invariant can be violated: the task is posted, then the PlatformMediaSessionManager is destroyed before the run loop dispatches the task, so the lambda executes and dereferences a freed ’this’ — a classic use-after-free on a dangling captured pointer. Because these updates are driven by media/audio session state changes that web content can provoke and tear down, the lifetime race is web-reachable.
The fix introduces a TaskCancellationGroup member m_taskGroup and a helper enqueueTaskOnMainThread that wraps each task in a WTF CancellableTask bound to that group; all the raw-’this’-capturing callOnMainThread call sites are converted to enqueueTaskOnMainThread. A newly added ~PlatformMediaSessionManager destructor calls m_taskGroup.cancel(), which invalidates any still-pending CancellableTasks so they become no-ops instead of running against freed memory. The header changes add the CancellableTask include, declare the out-of-line destructor (replacing ‘= default’), declare enqueueTaskOnMainThread, and add the TaskCancellationGroup member. The net effect: tasks that outlive the manager are cancelled at destruction, restoring the ‘object alive when task runs’ invariant. Note the sessionWillEndPlayback conversion also wraps a task capturing a WeakPtr session; while WeakPtr already guards that particular deref, routing it through the cancellation group makes cancellation uniform. The precise object whose destruction triggers the freed access is not shown being deleted in the diff, so the exact teardown ordering is inferred from the fix shape (a cancellation-on-destruction pattern), which is the standard remedy for pending-task UAF.
Attack Path
- Start media producing state changes Web content creates media elements / audio sessions (e.g. WebAudio or <video>) that cause the PlatformMediaSessionManager to schedule session-state or now-playing updates via callOnMainThread capturing raw this.
- Queue a deferred task A state change (session can produce audio changed, session will end playback, status update) posts a main-thread task that holds a raw pointer to the manager or session.
- Destroy the manager before the task runs Tear down the associated page/document/media context so the PlatformMediaSessionManager (or session) is freed while the posted task is still pending in the run loop queue.
- Task dispatches against freed memory The run loop later executes the lambda, dereferencing the dangling this/session pointer (maybeActivateAudioSession/updateSessionState/updateNowPlayingInfo), a use-after-free.
- Observe the crash The stale access corrupts or reads freed heap, producing the unexpected process crash / denial-of-service described in the CVE.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
PlatformMediaSessionManager::~PlatformMediaSessionManagerSource/WebCore/platform/audio/PlatformMediaSessionManager.cpp |
added | New destructor that calls m_taskGroup.cancel(), neutralizing any pending main-thread tasks so they cannot run after the manager is freed. |
PlatformMediaSessionManager::enqueueTaskOnMainThreadSource/WebCore/platform/audio/PlatformMediaSessionManager.cpp |
added | Helper that wraps a task in CancellableTask(m_taskGroup, ...) before callOnMainThread, tying its lifetime to the cancellation group. |
PlatformMediaSessionManager::sessionCanProduceAudioChangedSource/WebCore/platform/audio/PlatformMediaSessionManager.cpp |
modified | Switches its callOnMainThread([this]{...}) to enqueueTaskOnMainThread so the deferred maybeActivateAudioSession/updateSessionState is cancellable. |
PlatformMediaSessionManager::scheduleUpdateSessionStateSource/WebCore/platform/audio/PlatformMediaSessionManager.cpp |
modified | Converts its raw-this callOnMainThread to enqueueTaskOnMainThread. |
MediaSessionManagerCocoa::scheduleSessionStatusUpdateSource/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm |
modified | Converts its raw-this callOnMainThread (setSupportsSeeking/updateNowPlayingInfo) to enqueueTaskOnMainThread. |
MediaSessionManagerCocoa::sessionWillEndPlaybackSource/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm |
modified | Converts both callOnMainThread blocks (weakSession updateMediaUsageIfChanged and raw-this updateNowPlayingInfo) to enqueueTaskOnMainThread for uniform cancellation. |
PlatformMediaSessionManager (class declaration)Source/WebCore/platform/audio/PlatformMediaSessionManager.h |
modified | Adds CancellableTask.h include, out-of-line ~ declaration, enqueueTaskOnMainThread declaration, and the TaskCancellationGroup m_taskGroup member. |
Files Changed
Source/WebCore/platform/audio/PlatformMediaSessionManager.cppSource/WebCore/platform/audio/PlatformMediaSessionManager.hSource/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm
Audit Directions
- Remaining raw-this callOnMainThread in these filesVerify no callOnMainThread capturing raw this or a raw session pointer survived the conversion; grep PlatformMediaSessionManager.cpp and MediaSessionManagerCocoa.mm for ‘callOnMainThread(’ and ‘[this’.
- Other PlatformMediaSessionManager subclassesCheck platform-specific managers (GLib/GStreamer, remote, Mac AV variants) for the same deferred-update pattern that now needs enqueueTaskOnMainThread; grep across platform/audio for subclasses overriding scheduleSessionStatusUpdate/updateNowPlayingInfo.
- Objects that schedule work capturing this without cancellationSearch WebCore media/audio classes for asynchronous dispatch capturing raw this without a WeakPtr or TaskCancellationGroup; grep for ‘callOnMainThread([this’ and ‘RunLoop::main().dispatch([this’.
- Destructors missing task cancellationFind classes owning a TaskCancellationGroup or posting cancellable/deferred tasks whose destructor does not cancel them; grep for ‘TaskCancellationGroup’ members and confirm each has a ‘~…{ m_taskGroup.cancel(); }’ equivalent.
Patch
diff --git a/Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp b/Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp
index 81dc8e026141..999b08a1e845 100644
--- a/Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp
+++ b/Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp
@@ -117,6 +117,11 @@ PlatformMediaSessionManager::PlatformMediaSessionManager()
{
}
+PlatformMediaSessionManager::~PlatformMediaSessionManager()
+{
+ m_taskGroup.cancel();
+}
+
static inline unsigned indexFromMediaType(PlatformMediaSession::MediaType type)
{
return static_cast<unsigned>(type);
@@ -492,7 +497,7 @@ void PlatformMediaSessionManager::sessionCanProduceAudioChanged()
return;
m_alreadyScheduledSessionStatedUpdate = true;
- callOnMainThread([this] {
+ enqueueTaskOnMainThread([this] {
m_alreadyScheduledSessionStatedUpdate = false;
maybeActivateAudioSession();
updateSessionState();
@@ -656,7 +661,7 @@ void PlatformMediaSessionManager::scheduleUpdateSessionState()
return;
m_hasScheduledSessionStateUpdate = true;
- callOnMainThread([this] {
+ enqueueTaskOnMainThread([this] {
updateSessionState();
m_hasScheduledSessionStateUpdate = false;
});
@@ -903,6 +908,13 @@ bool PlatformMediaSessionManager::hasActiveNowPlayingSessionInGroup(MediaSession
return hasActiveNowPlayingSession;
}
+void PlatformMediaSessionManager::enqueueTaskOnMainThread(Function<void()>&& task)
+{
+ callOnMainThread(CancellableTask(m_taskGroup, [task = WTFMove(task)] () mutable {
+ task();
+ }));
+}
+
#if !RELEASE_LOG_DISABLED
WTFLogChannel& PlatformMediaSessionManager::logChannel() const
{
diff --git a/Source/WebCore/platform/audio/PlatformMediaSessionManager.h b/Source/WebCore/platform/audio/PlatformMediaSessionManager.h
index 527d12fff117..e5dd5c0f9503 100644
--- a/Source/WebCore/platform/audio/PlatformMediaSessionManager.h
+++ b/Source/WebCore/platform/audio/PlatformMediaSessionManager.h
@@ -31,6 +31,7 @@
#include "RemoteCommandListener.h"
#include "Timer.h"
#include <wtf/AggregateLogger.h>
+#include <wtf/CancellableTask.h>
#include <wtf/Vector.h>
#include <wtf/WeakHashSet.h>
#include <wtf/WeakPtr.h>
@@ -83,7 +84,7 @@ class PlatformMediaSessionManager
WEBCORE_EXPORT static void setMediaCapabilityGrantsEnabled(bool);
#endif
- virtual ~PlatformMediaSessionManager() = default;
+ virtual ~PlatformMediaSessionManager();
virtual void scheduleSessionStatusUpdate() { }
@@ -227,6 +228,7 @@ class PlatformMediaSessionManager
std::optional<bool> supportsSpatialAudioPlayback() { return m_supportsSpatialAudioPlayback; }
void nowPlayingMetadataChanged(const NowPlayingMetadata&);
+ void enqueueTaskOnMainThread(Function<void()>&&);
private:
friend class Internals;
@@ -260,6 +262,7 @@ class PlatformMediaSessionManager
bool m_hasScheduledSessionStateUpdate { false };
WeakHashSet<NowPlayingMetadataObserver> m_nowPlayingMetadataObservers;
+ TaskCancellationGroup m_taskGroup;
#if ENABLE(WEBM_FORMAT_READER)
static bool m_webMFormatReaderEnabled;
diff --git a/Source/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm b/Source/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm
index 3c4000a571a1..d50c37334a7e 100644
--- a/Source/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm
+++ b/Source/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm
@@ -270,7 +270,7 @@
void MediaSessionManagerCocoa::scheduleSessionStatusUpdate()
{
- callOnMainThread([this] () mutable {
+ enqueueTaskOnMainThread([this] () mutable {
m_nowPlayingManager->setSupportsSeeking(computeSupportsSeeking());
updateNowPlayingInfo();
@@ -329,7 +329,7 @@
{