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
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 @@ public:
WEBCORE_EXPORT static void setMediaCapabilityGrantsEnabled(bool);
#endif
- virtual ~PlatformMediaSessionManager() = default;
+ virtual ~PlatformMediaSessionManager();
virtual void scheduleSessionStatusUpdate() { }
@@ -227,6 +228,7 @@ protected:
std::optional<bool> supportsSpatialAudioPlayback() { return m_supportsSpatialAudioPlayback; }
void nowPlayingMetadataChanged(const NowPlayingMetadata&);
+ void enqueueTaskOnMainThread(Function<void()>&&);
private:
friend class Internals;
@@ -260,6 +262,7 @@ private:
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 @@ String MediaSessionManagerCocoa::audioTimePitchAlgorithmForMediaPlayerPitchCorre
void MediaSessionManagerCocoa::scheduleSessionStatusUpdate()
{
- callOnMainThread([this] () mutable {
+ enqueueTaskOnMainThread([this] () mutable {
m_nowPlayingManager->setSupportsSeeking(computeSupportsSeeking());
updateNowPlayingInfo();
@@ -329,7 +329,7 @@ void MediaSessionManagerCocoa::sessionWillEndPlayback(PlatformMediaSession& sess
{
PlatformMediaSessionManager::sessionWillEndPlayback(session, delayCallingUpdateNowPlaying);
- callOnMainThread([weakSession = WeakPtr { session }] {
+ enqueueTaskOnMainThread([weakSession = WeakPtr { session }] {
if (weakSession)
weakSession->updateMediaUsageIfChanged();
});
@@ -337,7 +337,7 @@ void MediaSessionManagerCocoa::sessionWillEndPlayback(PlatformMediaSession& sess
if (delayCallingUpdateNowPlaying == DelayCallingUpdateNowPlaying::No)
updateNowPlayingInfo();
else {
- callOnMainThread([this] {
+ enqueueTaskOnMainThread([this] {
updateNowPlayingInfo();
});
}