Skip to content

Commit

Permalink
Cherry-pick e938617. rdar://problem/105112595
Browse files Browse the repository at this point in the history
    Cherry-pick 263868@main (e938617). rdar://105112595

        The Document object is leaked on some pages using media (like YouTube.com)
        https://bugs.webkit.org/show_bug.cgi?id=251835
        rdar://105112595

        Reviewed by Chris Dumez.

        Re-land of 263660@main (and 263715@main) fixing crashes due to
        prematurely garbage collected MediaSessionActionHandler JS wrappers.

        By default a callback holds a Strong<> reference to the JS Function
        object. This has the effect of making the callback a GC root. Another
        option is to annotate the callback with the IsWeakCallback extended
        attribute which will hold the callback object as a Weak reference and
        keep it alive via the visitJSFunction mechanism instead of making it a
        root.

        In the case of MediaSessionActionHandler the strong reference will
        prevent an HTMLDocument from being garbage collected even after
        navigating away and clearing the caches (after a low memory warning, for
        example). This change adds the IsWeakCallback attribute and the
        necessary virtual function to the MediaSessionActionHandler base class
        and makes changes to allow the MediaSession to mark any action handlers
        that have been added to it.

        LayoutTests:

            Add a test to check that action handlers installed by the page are
            not leaked. Use an iframe to install and exercise the action
            handlers before the iframe is navigated away and a garbage
            collection is triggered (repeatedly). If after 500 attempts at GC
            the document containing the action handlers still exists we consider
            the document leaked.

            Also add a test to check that action handlers survive garbage
            collection and can be called when appropriate.

        * LayoutTests/media/media-session/actionHandler-lifetime-expected.txt: Added.
        * LayoutTests/media/media-session/actionHandler-lifetime.html: Added.
        * LayoutTests/media/media-session/actionHandler-no-document-leak-expected.txt: Added.
        * LayoutTests/media/media-session/actionHandler-no-document-leak.html: Added.
        * LayoutTests/media/media-session/resources/media-session-action-handler-document-leak-frame.html: Added.

        * Source/WebCore/Modules/mediasession/MediaSession.cpp:
        (WebCore::MediaSession::virtualHasPendingActivity const):
        (WebCore::MediaSession::setActionHandler):
        (WebCore::MediaSession::callActionHandler):
        * Source/WebCore/Modules/mediasession/MediaSession.h:
        (WebCore::MediaSession::hasActiveActionHandlers const):
        (WebCore::MediaSession::visitActionHandlers const):
        * Source/WebCore/Modules/mediasession/MediaSession.idl:
        * Source/WebCore/Modules/mediasession/MediaSessionActionHandler.h:
        * Source/WebCore/Modules/mediasession/MediaSessionActionHandler.idl:
        * Source/WebCore/Sources.txt:
        * Source/WebCore/WebCore.xcodeproj/project.pbxproj:
        * Source/WebCore/bindings/js/JSMediaSessionCustom.cpp: Added.
        (WebCore::JSMediaSession::visitAdditionalChildren):

        Canonical link: https://commits.webkit.org/263868@main

Identifier: 259548.810@safari-7615-branch
  • Loading branch information
rreno authored and MyahCobbs committed Jun 8, 2023
1 parent 0a592f3 commit 84e53d9
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Tests media session action handlers are not prematurely garbage collected. Test passes if it doesn't crash.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS () => areObjectsEqual(window.actionDetails, {action: "play"}) is true
PASS () => areObjectsEqual(window.actionDetails, {action: "pause"}) is true
PASS () => areObjectsEqual(window.actionDetails, {action: "play"}) is true
PASS () => areObjectsEqual(window.actionDetails, {action: "pause"}) is true
PASS successfullyParsed is true

TEST COMPLETE

44 changes: 44 additions & 0 deletions LayoutTests/media/media-session/actionHandler-lifetime.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<body>
<script src="../../resources/js-test.js"></script>
<script>
description("Tests media session action handlers are not prematurely garbage collected. Test passes if it doesn't crash.");
jsTestIsAsync = true;

function runTest() {
internals.sendMediaSessionAction(navigator.mediaSession, {action: "play"});
shouldBeTrue(() => areObjectsEqual(window.actionDetails, {action: "play"}));

internals.sendMediaSessionAction(navigator.mediaSession, {action: "pause"});
shouldBeTrue(() => areObjectsEqual(window.actionDetails, {action: "pause"}));
}

function forceGCAndRunTest() {
gc();
requestAnimationFrame(() => {
runTest();
finishJSTest();
});
}

onload = () => {
if (!window.internals) {
testFailed("Test requires internals.");
finishJSTest();
return;
}

function callback(actionDetails) {
window.actionDetails = actionDetails;
};

let actions = ["play", "pause"];
for (action of actions)
navigator.mediaSession.setActionHandler(action, callback);

runTest();
requestAnimationFrame(() => forceGCAndRunTest());

};
</script>
</body>
27 changes: 22 additions & 5 deletions Source/WebCore/Modules/mediasession/MediaSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ void MediaSession::stop()
#endif
}

bool MediaSession::virtualHasPendingActivity() const
{
return hasActiveActionHandlers();
}

void MediaSession::setMetadata(RefPtr<MediaMetadata>&& metadata)
{
ALWAYS_LOG(LOGIDENTIFIER);
Expand Down Expand Up @@ -239,15 +244,22 @@ void MediaSession::setActionHandler(MediaSessionAction action, RefPtr<MediaSessi
{
if (handler) {
ALWAYS_LOG(LOGIDENTIFIER, "adding ", action);
m_actionHandlers.set(action, handler);
{
Locker lock { m_actionHandlersLock };
m_actionHandlers.set(action, handler);
}
auto platformCommand = platformCommandForMediaSessionAction(action);
if (platformCommand != PlatformMediaSession::NoCommand)
PlatformMediaSessionManager::sharedManager().addSupportedCommand(platformCommand);
} else {
if (m_actionHandlers.contains(action)) {
ALWAYS_LOG(LOGIDENTIFIER, "removing ", action);
m_actionHandlers.remove(action);
bool containedAction;
{
Locker lock { m_actionHandlersLock };
containedAction = m_actionHandlers.remove(action);
}

if (containedAction)
ALWAYS_LOG(LOGIDENTIFIER, "removing ", action);
PlatformMediaSessionManager::sharedManager().removeSupportedCommand(platformCommandForMediaSessionAction(action));
}

Expand All @@ -268,7 +280,12 @@ void MediaSession::callActionHandler(const MediaSessionActionDetails& actionDeta

bool MediaSession::callActionHandler(const MediaSessionActionDetails& actionDetails, TriggerGestureIndicator triggerGestureIndicator)
{
if (auto handler = m_actionHandlers.get(actionDetails.action)) {
RefPtr<MediaSessionActionHandler> handler;
{
Locker lock { m_actionHandlersLock };
handler = m_actionHandlers.get(actionDetails.action);
}
if (handler) {
std::optional<UserGestureIndicator> maybeGestureIndicator;
if (triggerGestureIndicator == TriggerGestureIndicator::Yes)
maybeGestureIndicator.emplace(ProcessingUserGesture, document());
Expand Down
25 changes: 23 additions & 2 deletions Source/WebCore/Modules/mediasession/MediaSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ class MediaSession : public RefCounted<MediaSession>, public ActiveDOMObject, pu

void callActionHandler(const MediaSessionActionDetails&, DOMPromiseDeferred<void>&&);

template <typename Visitor> void visitActionHandlers(Visitor&) const;

ExceptionOr<void> setPositionState(std::optional<MediaPositionState>&&);
std::optional<MediaPositionState> positionState() const { return m_positionState; }

Expand All @@ -88,7 +90,8 @@ class MediaSession : public RefCounted<MediaSession>, public ActiveDOMObject, pu
ExceptionOr<void> setPlaylist(ScriptExecutionContext&, Vector<RefPtr<MediaMetadata>>&&);
#endif

bool hasActiveActionHandlers() const { return !m_actionHandlers.isEmpty(); }
bool hasActiveActionHandlers() const;

enum class TriggerGestureIndicator {
No,
Yes,
Expand Down Expand Up @@ -135,14 +138,15 @@ class MediaSession : public RefCounted<MediaSession>, public ActiveDOMObject, pu
const char* activeDOMObjectName() const final { return "MediaSession"; }
void suspend(ReasonForSuspension) final;
void stop() final;
bool virtualHasPendingActivity() const final;

WeakPtr<Navigator> m_navigator;
RefPtr<MediaMetadata> m_metadata;
MediaSessionPlaybackState m_playbackState { MediaSessionPlaybackState::None };
std::optional<MediaPositionState> m_positionState;
std::optional<double> m_lastReportedPosition;
MonotonicTime m_timeAtLastPositionUpdate;
HashMap<MediaSessionAction, RefPtr<MediaSessionActionHandler>, IntHash<MediaSessionAction>, WTF::StrongEnumHashTraits<MediaSessionAction>> m_actionHandlers;
HashMap<MediaSessionAction, RefPtr<MediaSessionActionHandler>, IntHash<MediaSessionAction>, WTF::StrongEnumHashTraits<MediaSessionAction>> m_actionHandlers WTF_GUARDED_BY_LOCK(m_actionHandlersLock);
RefPtr<const Logger> m_logger;
const void* m_logIdentifier;

Expand All @@ -156,11 +160,28 @@ class MediaSession : public RefCounted<MediaSession>, public ActiveDOMObject, pu
#if ENABLE(MEDIA_SESSION_PLAYLIST)
Vector<Ref<MediaMetadata>> m_playlist;
#endif
mutable Lock m_actionHandlersLock;
};

String convertEnumerationToString(MediaSessionPlaybackState);
String convertEnumerationToString(MediaSessionAction);

inline bool MediaSession::hasActiveActionHandlers() const
{
Locker lock { m_actionHandlersLock };
return !m_actionHandlers.isEmpty();
}

template <typename Visitor>
void MediaSession::visitActionHandlers(Visitor& visitor) const
{
Locker lock { m_actionHandlersLock };
for (auto& actionHandler : m_actionHandlers) {
if (actionHandler.value)
actionHandler.value->visitJSFunction(visitor);
}
}

}

namespace WTF {
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/Modules/mediasession/MediaSession.idl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
Conditional=MEDIA_SESSION,
Exposed=Window,
ExportMacro=WEBCORE_EXPORT,
JSCustomMarkFunction
] interface MediaSession
{
attribute MediaMetadata? metadata;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class MediaSessionActionHandler : public RefCounted<MediaSessionActionHandler>,

virtual CallbackResult<void> handleEvent(const MediaSessionActionDetails&) = 0;

protected:
private:
virtual bool hasCallback() const = 0;
};

Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/Sources.txt
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ bindings/js/JSIntersectionObserverEntryCustom.cpp
bindings/js/JSKeyframeEffectCustom.cpp
bindings/js/JSLazyEventListener.cpp
bindings/js/JSLocationCustom.cpp
bindings/js/JSMediaSessionCustom.cpp
bindings/js/JSMediaStreamTrackCustom.cpp
bindings/js/JSMessageChannelCustom.cpp
bindings/js/JSMessageEventCustom.cpp
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/WebCore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9637,6 +9637,7 @@
44EEA0FC274727F400594A83 /* ImageControlsMac.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ImageControlsMac.cpp; sourceTree = "<group>"; };
4512502015DCE37D002F84E2 /* SpinButtonElement.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SpinButtonElement.cpp; sourceTree = "<group>"; };
4512502115DCE37D002F84E2 /* SpinButtonElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SpinButtonElement.h; sourceTree = "<group>"; };
45160A892A09FA22009E39A2 /* JSMediaSessionCustom.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = JSMediaSessionCustom.cpp; sourceTree = "<group>"; };
451A49F8F8726BE071518BE2 /* ISOProtectionSystemSpecificHeaderBox.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ISOProtectionSystemSpecificHeaderBox.cpp; sourceTree = "<group>"; };
453EB635159C570400001BB7 /* DateTimeFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DateTimeFormat.h; sourceTree = "<group>"; };
457CECE328ECF8C1006FE9FB /* PolicyContainer.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PolicyContainer.cpp; sourceTree = "<group>"; };
Expand Down Expand Up @@ -26324,6 +26325,7 @@
77C13F042165658A002D9C5F /* JSIntersectionObserverEntryCustom.cpp */,
71601718279F2D7E005A25AE /* JSKeyframeEffectCustom.cpp */,
AD726FE716D9F204003A4E6D /* JSMediaListCustom.h */,
45160A892A09FA22009E39A2 /* JSMediaSessionCustom.cpp */,
415CDAF61E6CE0D3004F11EE /* JSMediaStreamTrackCustom.cpp */,
E1A5F99A0E7EAA2500AF85EA /* JSMessageChannelCustom.cpp */,
E1ADED460E76B8DD004A1A5E /* JSMessagePortCustom.cpp */,
Expand Down
45 changes: 45 additions & 0 deletions Source/WebCore/bindings/js/JSMediaSessionCustom.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2023 Apple, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#if ENABLE(MEDIA_SESSION)

#include "config.h"
#include "JSMediaSession.h"

#include <JavaScriptCore/JSCInlines.h>

namespace WebCore {

template <typename Visitor>
void JSMediaSession::visitAdditionalChildren(Visitor& visitor)
{
wrapped().visitActionHandlers(visitor);
}

DEFINE_VISIT_ADDITIONAL_CHILDREN(JSMediaSession);

}

#endif

0 comments on commit 84e53d9

Please sign in to comment.