getUserMedia sandbox extensions should not be revoked when a getUserMedia allowed request is being processed
https://bugs.webkit.org/show_bug.cgi?id=197851

Reviewed by Alex Christensen.

Source/WebCore:

Add a completion handler to create a new capture stream.
This is used by WK2 layer to acknowledge the pending capture request is completed.
Just after the completion handler, make sure to update the document media state.
This is done to ensure that, should capture failing, the UIProcess
knows about it and can manage proper sandbox extension revocation.

Test: fast/mediastream/gum-stop-track.html

* Modules/mediastream/UserMediaRequest.cpp:
(WebCore::UserMediaRequest::allow):
(WebCore::UserMediaRequest::PendingActivationMediaStream::PendingActivationMediaStream):
(WebCore::UserMediaRequest::PendingActivationMediaStream::~PendingActivationMediaStream):
* Modules/mediastream/UserMediaRequest.h:
(WebCore::UserMediaRequest::PendingActivationMediaStream::create):
* platform/mock/MockRealtimeMediaSourceCenter.cpp:
(WebCore::MockRealtimeMediaSourceCenter::mockRealtimeMediaSourceCenterEnabled):
* platform/mock/MockRealtimeMediaSourceCenter.h:

Source/WebKit:

Before the patch, stopping capture in a document and quickly triggering a new capture
might fail as the UIProcess would grant access and revoke sandbox access based on the fact
the page is no longer capturing.
To fix that issue, keep a state in the UIProcess to not revoke sandbox extensions in case of
capture being started.
Add an IPC message back to tell UIProcess when an allowed capture is finished.
Just after doing that, make sure the document is updating the media state to UIProcess, which will trigger proper sandbox extension handling.

This should also trigger the case of an allowed getUserMedia call that fails to start for some reason.
In that case, the patch will automatically trigger a document media state refresh which will trigger a sandbox revokation.

Covered by added test that exercise a newly added debug assertion.
This assertion ensures that we revoke extensions while a document is not capturing.

* UIProcess/UserMediaPermissionRequestManagerProxy.cpp:
(WebKit::UserMediaPermissionRequestManagerProxy::~UserMediaPermissionRequestManagerProxy):
(WebKit::UserMediaPermissionRequestManagerProxy::grantAccess):
(WebKit::UserMediaPermissionRequestManagerProxy::captureStateChanged):
* UIProcess/UserMediaPermissionRequestManagerProxy.h:
* UIProcess/UserMediaProcessManager.cpp:
(WebKit::UserMediaProcessManager::willCreateMediaStream):
(WebKit::UserMediaProcessManager::revokeSandboxExtensionsIfNeeded):
* UIProcess/UserMediaProcessManager.h:
* UIProcess/WebPageProxy.h:
(WebKit::WebPageProxy::isCapturingAudio const):
(WebKit::WebPageProxy::isCapturingVideo const):
* WebProcess/MediaStream/UserMediaPermissionRequestManager.cpp:
(WebKit::UserMediaPermissionRequestManager::userMediaAccessWasGranted):
* WebProcess/MediaStream/UserMediaPermissionRequestManager.h:
* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::userMediaAccessWasGranted):
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/WebPage.messages.in:
* WebProcess/WebProcess.cpp:
(WebKit::checkDocumentsCaptureStateConsistency):
(WebKit::WebProcess::revokeUserMediaDeviceSandboxExtensions):

LayoutTests:

* fast/mediastream/gum-stop-track-expected.txt: Added.
* fast/mediastream/gum-stop-track.html: Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@245335 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index f3e86bb..802360d 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,13 @@
+2019-05-15  Youenn Fablet  <youenn@apple.com>
+
+        getUserMedia sandbox extensions should not be revoked when a getUserMedia allowed request is being processed
+        https://bugs.webkit.org/show_bug.cgi?id=197851
+
+        Reviewed by Alex Christensen.
+
+        * fast/mediastream/gum-stop-track-expected.txt: Added.
+        * fast/mediastream/gum-stop-track.html: Added.
+
 2019-05-15  Fujii Hironori  <Hironori.Fujii@sony.com>
 
         Unreviewed test gardening for WinCairo
diff --git a/LayoutTests/fast/mediastream/gum-stop-track-expected.txt b/LayoutTests/fast/mediastream/gum-stop-track-expected.txt
new file mode 100644
index 0000000..3f22642
--- /dev/null
+++ b/LayoutTests/fast/mediastream/gum-stop-track-expected.txt
@@ -0,0 +1,3 @@
+
+PASS Call getUserMedia and track.stop in a loop 
+
diff --git a/LayoutTests/fast/mediastream/gum-stop-track.html b/LayoutTests/fast/mediastream/gum-stop-track.html
new file mode 100644
index 0000000..555baca
--- /dev/null
+++ b/LayoutTests/fast/mediastream/gum-stop-track.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+    <head>
+        <script src="../../resources/testharness.js"></script>
+        <script src="../../resources/testharnessreport.js"></script>
+    </head>
+    <body>
+        <script>
+promise_test(async (test) => {
+    if (window.testRunner)
+        testRunner.setUserMediaPermission(true);
+
+    for (var cptr = 0; cptr < 5; ++cptr) {
+        const stream = await navigator.mediaDevices.getUserMedia({audio:false, video:true});
+        stream.getTracks().forEach((track) => track.stop());
+    }
+}, "Call getUserMedia and track.stop in a loop");
+        </script>
+    </body>
+</html>
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index a6e881a..a608751 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,28 @@
+2019-05-15  Youenn Fablet  <youenn@apple.com>
+
+        getUserMedia sandbox extensions should not be revoked when a getUserMedia allowed request is being processed
+        https://bugs.webkit.org/show_bug.cgi?id=197851
+
+        Reviewed by Alex Christensen.
+
+        Add a completion handler to create a new capture stream.
+        This is used by WK2 layer to acknowledge the pending capture request is completed.
+        Just after the completion handler, make sure to update the document media state.
+        This is done to ensure that, should capture failing, the UIProcess
+        knows about it and can manage proper sandbox extension revocation.
+
+        Test: fast/mediastream/gum-stop-track.html
+
+        * Modules/mediastream/UserMediaRequest.cpp:
+        (WebCore::UserMediaRequest::allow):
+        (WebCore::UserMediaRequest::PendingActivationMediaStream::PendingActivationMediaStream):
+        (WebCore::UserMediaRequest::PendingActivationMediaStream::~PendingActivationMediaStream):
+        * Modules/mediastream/UserMediaRequest.h:
+        (WebCore::UserMediaRequest::PendingActivationMediaStream::create):
+        * platform/mock/MockRealtimeMediaSourceCenter.cpp:
+        (WebCore::MockRealtimeMediaSourceCenter::mockRealtimeMediaSourceCenterEnabled):
+        * platform/mock/MockRealtimeMediaSourceCenter.h:
+
 2019-05-15  Simon Fraser  <simon.fraser@apple.com>
 
         Move RenderLayerCompositor's OverlapMap to its own file
diff --git a/Source/WebCore/Modules/mediastream/UserMediaRequest.cpp b/Source/WebCore/Modules/mediastream/UserMediaRequest.cpp
index da2628f..f061bc7 100644
--- a/Source/WebCore/Modules/mediastream/UserMediaRequest.cpp
+++ b/Source/WebCore/Modules/mediastream/UserMediaRequest.cpp
@@ -47,6 +47,7 @@
 #include "SchemeRegistry.h"
 #include "Settings.h"
 #include "UserMediaController.h"
+#include <wtf/Scope.h>
 
 namespace WebCore {
 
@@ -214,11 +215,14 @@
     controller->requestUserMediaAccess(*this);
 }
 
-void UserMediaRequest::allow(CaptureDevice&& audioDevice, CaptureDevice&& videoDevice, String&& deviceIdentifierHashSalt)
+void UserMediaRequest::allow(CaptureDevice&& audioDevice, CaptureDevice&& videoDevice, String&& deviceIdentifierHashSalt, CompletionHandler<void()>&& completionHandler)
 {
     RELEASE_LOG(MediaStream, "UserMediaRequest::allow %s %s", audioDevice ? audioDevice.persistentId().utf8().data() : "", videoDevice ? videoDevice.persistentId().utf8().data() : "");
 
-    auto callback = [this, protector = makePendingActivity(*this)](RefPtr<MediaStreamPrivate>&& privateStream) mutable {
+    auto callback = [this, protector = makePendingActivity(*this), completionHandler = WTFMove(completionHandler)](RefPtr<MediaStreamPrivate>&& privateStream) mutable {
+        auto scopeExit = makeScopeExit([&] {
+            completionHandler();
+        });
         if (!m_scriptExecutionContext)
             return;
 
@@ -235,7 +239,8 @@
             return;
         }
 
-        m_pendingActivationMediaStream = PendingActivationMediaStream::create(WTFMove(protector), *this, WTFMove(stream));
+        scopeExit.release();
+        m_pendingActivationMediaStream = PendingActivationMediaStream::create(WTFMove(protector), *this, WTFMove(stream), WTFMove(completionHandler));
     };
 
     auto& document = downcast<Document>(*scriptExecutionContext());
@@ -330,10 +335,11 @@
     return downcast<Document>(m_scriptExecutionContext);
 }
 
-UserMediaRequest::PendingActivationMediaStream::PendingActivationMediaStream(Ref<PendingActivity<UserMediaRequest>>&& protectingUserMediaRequest, UserMediaRequest& userMediaRequest, Ref<MediaStream>&& stream)
+UserMediaRequest::PendingActivationMediaStream::PendingActivationMediaStream(Ref<PendingActivity<UserMediaRequest>>&& protectingUserMediaRequest, UserMediaRequest& userMediaRequest, Ref<MediaStream>&& stream, CompletionHandler<void()>&& completionHandler)
     : m_protectingUserMediaRequest(WTFMove(protectingUserMediaRequest))
     , m_userMediaRequest(userMediaRequest)
     , m_mediaStream(WTFMove(stream))
+    , m_completionHandler(WTFMove(completionHandler))
 {
     m_mediaStream->privateStream().addObserver(*this);
     m_mediaStream->startProducingData();
@@ -342,6 +348,9 @@
 UserMediaRequest::PendingActivationMediaStream::~PendingActivationMediaStream()
 {
     m_mediaStream->privateStream().removeObserver(*this);
+    m_completionHandler();
+    if (auto* document = m_mediaStream->document())
+        document->updateIsPlayingMedia();
 }
 
 void UserMediaRequest::PendingActivationMediaStream::characteristicsChanged()
diff --git a/Source/WebCore/Modules/mediastream/UserMediaRequest.h b/Source/WebCore/Modules/mediastream/UserMediaRequest.h
index 6de7e5c..e93d134 100644
--- a/Source/WebCore/Modules/mediastream/UserMediaRequest.h
+++ b/Source/WebCore/Modules/mediastream/UserMediaRequest.h
@@ -40,6 +40,7 @@
 #include "MediaConstraints.h"
 #include "MediaStreamPrivate.h"
 #include "MediaStreamRequest.h"
+#include <wtf/CompletionHandler.h>
 
 namespace WebCore {
 
@@ -54,7 +55,7 @@
     void start();
 
     WEBCORE_EXPORT void setAllowedMediaDeviceUIDs(const String& audioDeviceUID, const String& videoDeviceUID);
-    WEBCORE_EXPORT void allow(CaptureDevice&& audioDevice, CaptureDevice&& videoDevice, String&& deviceIdentifierHashSalt);
+    WEBCORE_EXPORT void allow(CaptureDevice&& audioDevice, CaptureDevice&& videoDevice, String&& deviceIdentifierHashSalt, CompletionHandler<void()>&&);
 
     enum MediaAccessDenialReason { NoConstraints, UserMediaDisabled, NoCaptureDevices, InvalidConstraint, HardwareError, PermissionDenied, InvalidAccess, IllegalConstraint, OtherFailure };
     WEBCORE_EXPORT void deny(MediaAccessDenialReason, const String& errorMessage = emptyString());
@@ -83,20 +84,21 @@
 
     class PendingActivationMediaStream : public RefCounted<PendingActivationMediaStream>, private MediaStreamPrivate::Observer {
     public:
-        static Ref<PendingActivationMediaStream> create(Ref<PendingActivity<UserMediaRequest>>&& protectingUserMediaRequest, UserMediaRequest& userMediaRequest, Ref<MediaStream>&& stream)
+        static Ref<PendingActivationMediaStream> create(Ref<PendingActivity<UserMediaRequest>>&& protectingUserMediaRequest, UserMediaRequest& userMediaRequest, Ref<MediaStream>&& stream, CompletionHandler<void()>&& completionHandler)
         {
-            return adoptRef(*new PendingActivationMediaStream { WTFMove(protectingUserMediaRequest), userMediaRequest, WTFMove(stream) });
+            return adoptRef(*new PendingActivationMediaStream { WTFMove(protectingUserMediaRequest), userMediaRequest, WTFMove(stream), WTFMove(completionHandler) });
         }
         ~PendingActivationMediaStream();
 
     private:
-        PendingActivationMediaStream(Ref<PendingActivity<UserMediaRequest>>&&, UserMediaRequest&, Ref<MediaStream>&&);
+        PendingActivationMediaStream(Ref<PendingActivity<UserMediaRequest>>&&, UserMediaRequest&, Ref<MediaStream>&&, CompletionHandler<void()>&&);
 
         void characteristicsChanged() final;
 
         Ref<PendingActivity<UserMediaRequest>> m_protectingUserMediaRequest;
         UserMediaRequest& m_userMediaRequest;
         Ref<MediaStream> m_mediaStream;
+        CompletionHandler<void()> m_completionHandler;
     };
 
     Vector<String> m_videoDeviceUIDs;
diff --git a/Source/WebCore/platform/mock/MockRealtimeMediaSourceCenter.cpp b/Source/WebCore/platform/mock/MockRealtimeMediaSourceCenter.cpp
index c4de5bd..69fad4b 100644
--- a/Source/WebCore/platform/mock/MockRealtimeMediaSourceCenter.cpp
+++ b/Source/WebCore/platform/mock/MockRealtimeMediaSourceCenter.cpp
@@ -208,6 +208,14 @@
         center.unsetDisplayCaptureFactory(mock.displayCaptureFactory());
 }
 
+bool MockRealtimeMediaSourceCenter::mockRealtimeMediaSourceCenterEnabled()
+{
+    MockRealtimeMediaSourceCenter& mock = singleton();
+    RealtimeMediaSourceCenter& center = RealtimeMediaSourceCenter::singleton();
+
+    return &center.audioCaptureFactory() == &mock.audioCaptureFactory() || &center.videoCaptureFactory() == &mock.videoCaptureFactory() || &center.displayCaptureFactory() == &mock.displayCaptureFactory();
+}
+
 static void createCaptureDevice(const MockMediaDevice& device)
 {
     deviceListForDevice(device).append(MockRealtimeMediaSourceCenter::captureDeviceWithPersistentID(device.type(), device.persistentId).value());
diff --git a/Source/WebCore/platform/mock/MockRealtimeMediaSourceCenter.h b/Source/WebCore/platform/mock/MockRealtimeMediaSourceCenter.h
index deea053..6d16fe7 100644
--- a/Source/WebCore/platform/mock/MockRealtimeMediaSourceCenter.h
+++ b/Source/WebCore/platform/mock/MockRealtimeMediaSourceCenter.h
@@ -41,6 +41,7 @@
     WEBCORE_EXPORT static MockRealtimeMediaSourceCenter& singleton();
 
     WEBCORE_EXPORT static void setMockRealtimeMediaSourceCenterEnabled(bool);
+    WEBCORE_EXPORT static bool mockRealtimeMediaSourceCenterEnabled();
 
     WEBCORE_EXPORT static void setDevices(Vector<MockMediaDevice>&&);
     WEBCORE_EXPORT static void addDevice(const MockMediaDevice&);
diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index 648254c..bdee3c96 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,47 @@
+2019-05-15  Youenn Fablet  <youenn@apple.com>
+
+        getUserMedia sandbox extensions should not be revoked when a getUserMedia allowed request is being processed
+        https://bugs.webkit.org/show_bug.cgi?id=197851
+
+        Reviewed by Alex Christensen.
+
+        Before the patch, stopping capture in a document and quickly triggering a new capture
+        might fail as the UIProcess would grant access and revoke sandbox access based on the fact
+        the page is no longer capturing.
+        To fix that issue, keep a state in the UIProcess to not revoke sandbox extensions in case of
+        capture being started.
+        Add an IPC message back to tell UIProcess when an allowed capture is finished.
+        Just after doing that, make sure the document is updating the media state to UIProcess, which will trigger proper sandbox extension handling.
+
+        This should also trigger the case of an allowed getUserMedia call that fails to start for some reason.
+        In that case, the patch will automatically trigger a document media state refresh which will trigger a sandbox revokation.
+
+        Covered by added test that exercise a newly added debug assertion.
+        This assertion ensures that we revoke extensions while a document is not capturing.
+
+        * UIProcess/UserMediaPermissionRequestManagerProxy.cpp:
+        (WebKit::UserMediaPermissionRequestManagerProxy::~UserMediaPermissionRequestManagerProxy):
+        (WebKit::UserMediaPermissionRequestManagerProxy::grantAccess):
+        (WebKit::UserMediaPermissionRequestManagerProxy::captureStateChanged):
+        * UIProcess/UserMediaPermissionRequestManagerProxy.h:
+        * UIProcess/UserMediaProcessManager.cpp:
+        (WebKit::UserMediaProcessManager::willCreateMediaStream):
+        (WebKit::UserMediaProcessManager::revokeSandboxExtensionsIfNeeded):
+        * UIProcess/UserMediaProcessManager.h:
+        * UIProcess/WebPageProxy.h:
+        (WebKit::WebPageProxy::isCapturingAudio const):
+        (WebKit::WebPageProxy::isCapturingVideo const):
+        * WebProcess/MediaStream/UserMediaPermissionRequestManager.cpp:
+        (WebKit::UserMediaPermissionRequestManager::userMediaAccessWasGranted):
+        * WebProcess/MediaStream/UserMediaPermissionRequestManager.h:
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::userMediaAccessWasGranted):
+        * WebProcess/WebPage/WebPage.h:
+        * WebProcess/WebPage/WebPage.messages.in:
+        * WebProcess/WebProcess.cpp:
+        (WebKit::checkDocumentsCaptureStateConsistency):
+        (WebKit::WebProcess::revokeUserMediaDeviceSandboxExtensions):
+
 2019-05-15  Chris Dumez  <cdumez@apple.com>
 
         [WK2][iOS] UIProcess may get killed because it is taking too long to release its background task after expiration
diff --git a/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp b/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
index 46e0a6a..1f26029 100644
--- a/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
+++ b/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
@@ -83,7 +83,7 @@
 UserMediaPermissionRequestManagerProxy::~UserMediaPermissionRequestManagerProxy()
 {
 #if ENABLE(MEDIA_STREAM)
-    UserMediaProcessManager::singleton().endedCaptureSession(*this);
+    UserMediaProcessManager::singleton().revokeSandboxExtensionsIfNeeded(page().process());
     proxies().remove(this);
 #endif
     invalidatePendingRequests();
@@ -295,7 +295,12 @@
         return false;
     }
 
-    m_page.process().send(Messages::WebPage::UserMediaAccessWasGranted(request.userMediaID(), request.audioDevice(), request.videoDevice(), request.deviceIdentifierHashSalt()), m_page.pageID());
+    ++m_hasPendingCapture;
+    m_page.process().connection()->sendWithAsyncReply(Messages::WebPage::UserMediaAccessWasGranted { request.userMediaID(), request.audioDevice(), request.videoDevice(), request.deviceIdentifierHashSalt() }, [this, weakThis = makeWeakPtr(this)] {
+        if (!weakThis)
+            return;
+        --m_hasPendingCapture;
+    }, m_page.pageID());
     return true;
 }
 #endif
@@ -632,13 +637,8 @@
         return;
 
 #if ENABLE(MEDIA_STREAM)
-    bool wasCapturingAudio = oldState & MediaProducer::AudioCaptureMask;
-    bool wasCapturingVideo = oldState & MediaProducer::VideoCaptureMask;
-    bool isCapturingAudio = newState & MediaProducer::AudioCaptureMask;
-    bool isCapturingVideo = newState & MediaProducer::VideoCaptureMask;
-
-    if ((wasCapturingAudio && !isCapturingAudio) || (wasCapturingVideo && !isCapturingVideo))
-        UserMediaProcessManager::singleton().endedCaptureSession(*this);
+    if (!m_hasPendingCapture)
+        UserMediaProcessManager::singleton().revokeSandboxExtensionsIfNeeded(page().process());
 
     if (m_captureState == (newState & activeCaptureMask))
         return;
diff --git a/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.h b/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.h
index d87af41..da3ea27 100644
--- a/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.h
+++ b/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.h
@@ -144,6 +144,7 @@
     const void* m_logIdentifier;
 #endif
     bool m_hasFilteredDeviceList { false };
+    uint64_t m_hasPendingCapture { 0 };
 };
 
 String convertEnumerationToString(UserMediaPermissionRequestManagerProxy::RequestAction);
diff --git a/Source/WebKit/UIProcess/UserMediaProcessManager.cpp b/Source/WebKit/UIProcess/UserMediaProcessManager.cpp
index 9f83de8..36e095b 100644
--- a/Source/WebKit/UIProcess/UserMediaProcessManager.cpp
+++ b/Source/WebKit/UIProcess/UserMediaProcessManager.cpp
@@ -72,26 +72,26 @@
     }
     
 #if ENABLE(SANDBOX_EXTENSIONS) && USE(APPLE_INTERNAL_SDK)
-    if (!proxy.page().preferences().mockCaptureDevicesEnabled()) {
-        auto& process = proxy.page().process();
-        size_t extensionCount = 0;
+    auto& process = proxy.page().process();
+    size_t extensionCount = 0;
 
-        if (withAudio && !process.hasAudioCaptureExtension())
-            extensionCount++;
-        else
-            withAudio = false;
+    if (withAudio && !process.hasAudioCaptureExtension())
+        extensionCount++;
+    else
+        withAudio = false;
 
-        if (withVideo && !process.hasVideoCaptureExtension())
-            extensionCount++;
-        else
-            withVideo = false;
+    if (withVideo && !process.hasVideoCaptureExtension())
+        extensionCount++;
+    else
+        withVideo = false;
 
-        if (extensionCount) {
-            SandboxExtension::HandleArray handles;
+    if (extensionCount) {
+        SandboxExtension::HandleArray handles;
+        Vector<String> ids;
+
+        if (!proxy.page().preferences().mockCaptureDevicesEnabled()) {
             handles.allocate(extensionCount);
-
-            Vector<String> ids;
-            ids.reserveCapacity(extensionCount);
+            ids.reserveInitialCapacity(extensionCount);
 
             if (withAudio && SandboxExtension::createHandleForGenericExtension(audioExtensionPath, handles[--extensionCount]))
                 ids.append(audioExtensionPath);
@@ -103,16 +103,16 @@
                 WTFLogAlways("Could not create a required sandbox extension, capture will fail!");
                 return false;
             }
-
-            for (const auto& id : ids)
-                RELEASE_LOG(WebRTC, "UserMediaProcessManager::willCreateMediaStream - granting extension %s", id.utf8().data());
-
-            if (withAudio)
-                process.grantAudioCaptureExtension();
-            if (withVideo)
-                process.grantVideoCaptureExtension();
-            process.send(Messages::WebProcess::GrantUserMediaDeviceSandboxExtensions(MediaDeviceSandboxExtensions(ids, WTFMove(handles))), proxy.page().pageID());
         }
+
+        for (const auto& id : ids)
+            RELEASE_LOG(WebRTC, "UserMediaProcessManager::willCreateMediaStream - granting extension %s", id.utf8().data());
+
+        if (withAudio)
+            process.grantAudioCaptureExtension();
+        if (withVideo)
+            process.grantVideoCaptureExtension();
+        process.send(Messages::WebProcess::GrantUserMediaDeviceSandboxExtensions(MediaDeviceSandboxExtensions(ids, WTFMove(handles))), 0);
     }
 #else
     UNUSED_PARAM(proxy);
@@ -125,20 +125,17 @@
     return true;
 }
 
-void UserMediaProcessManager::endedCaptureSession(UserMediaPermissionRequestManagerProxy& proxy)
+void UserMediaProcessManager::revokeSandboxExtensionsIfNeeded(WebProcessProxy& process)
 {
 #if ENABLE(SANDBOX_EXTENSIONS)
     bool hasAudioCapture = false;
     bool hasVideoCapture = false;
 
-    auto& process = proxy.page().process();
-    UserMediaPermissionRequestManagerProxy::forEach([&hasAudioCapture, &hasVideoCapture, &proxy, &process](auto& managerProxy) {
-        if (&proxy == &managerProxy || &process != &managerProxy.page().process())
+    UserMediaPermissionRequestManagerProxy::forEach([&hasAudioCapture, &hasVideoCapture, &process](auto& managerProxy) {
+        if (&process != &managerProxy.page().process())
             return;
-        if (managerProxy.page().hasActiveAudioStream())
-            hasAudioCapture = true;
-        if (managerProxy.page().hasActiveVideoStream())
-            hasVideoCapture = true;
+        hasAudioCapture |= managerProxy.page().isCapturingAudio();
+        hasVideoCapture |= managerProxy.page().isCapturingVideo();
     });
 
     if (hasAudioCapture && hasVideoCapture)
@@ -160,7 +157,7 @@
     for (const auto& id : params)
         RELEASE_LOG(WebRTC, "UserMediaProcessManager::endedCaptureSession - revoking extension %s", id.utf8().data());
 
-    process.send(Messages::WebProcess::RevokeUserMediaDeviceSandboxExtensions(params), proxy.page().pageID());
+    process.send(Messages::WebProcess::RevokeUserMediaDeviceSandboxExtensions(params), 0);
 #endif
 }
 
diff --git a/Source/WebKit/UIProcess/UserMediaProcessManager.h b/Source/WebKit/UIProcess/UserMediaProcessManager.h
index fe87eaa..ae9e446 100644
--- a/Source/WebKit/UIProcess/UserMediaProcessManager.h
+++ b/Source/WebKit/UIProcess/UserMediaProcessManager.h
@@ -38,7 +38,7 @@
     bool willCreateMediaStream(UserMediaPermissionRequestManagerProxy&, bool withAudio, bool withVideo);
     void muteCaptureMediaStreamsExceptIn(WebPageProxy&);
 
-    void endedCaptureSession(UserMediaPermissionRequestManagerProxy&);
+    void revokeSandboxExtensionsIfNeeded(WebProcessProxy&);
 
     void setCaptureEnabled(bool);
     bool captureEnabled() const { return m_captureEnabled; }
diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h
index 738b31f..df8a4f5 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.h
+++ b/Source/WebKit/UIProcess/WebPageProxy.h
@@ -1297,6 +1297,8 @@
     bool isPlayingAudio() const { return !!(m_mediaState & WebCore::MediaProducer::IsPlayingAudio); }
     void isPlayingMediaDidChange(WebCore::MediaProducer::MediaStateFlags, uint64_t);
     void updatePlayingMediaDidChange(WebCore::MediaProducer::MediaStateFlags);
+    bool isCapturingAudio() const { return m_mediaState & WebCore::MediaProducer::AudioCaptureMask; }
+    bool isCapturingVideo() const { return m_mediaState & WebCore::MediaProducer::VideoCaptureMask; }
     bool hasActiveAudioStream() const { return m_mediaState & WebCore::MediaProducer::HasActiveAudioCaptureDevice; }
     bool hasActiveVideoStream() const { return m_mediaState & WebCore::MediaProducer::HasActiveVideoCaptureDevice; }
     WebCore::MediaProducer::MediaStateFlags mediaStateFlags() const { return m_mediaState; }
diff --git a/Source/WebKit/WebProcess/MediaStream/UserMediaPermissionRequestManager.cpp b/Source/WebKit/WebProcess/MediaStream/UserMediaPermissionRequestManager.cpp
index 6c4db3b..67dcb19 100644
--- a/Source/WebKit/WebProcess/MediaStream/UserMediaPermissionRequestManager.cpp
+++ b/Source/WebKit/WebProcess/MediaStream/UserMediaPermissionRequestManager.cpp
@@ -137,14 +137,14 @@
     m_userMediaRequestToIDMap.remove(&request);
 }
 
-void UserMediaPermissionRequestManager::userMediaAccessWasGranted(uint64_t requestID, CaptureDevice&& audioDevice, CaptureDevice&& videoDevice, String&& deviceIdentifierHashSalt)
+void UserMediaPermissionRequestManager::userMediaAccessWasGranted(uint64_t requestID, CaptureDevice&& audioDevice, CaptureDevice&& videoDevice, String&& deviceIdentifierHashSalt, CompletionHandler<void()>&& completionHandler)
 {
     auto request = m_idToUserMediaRequestMap.take(requestID);
     if (!request)
         return;
     removeMediaRequestFromMaps(*request);
 
-    request->allow(WTFMove(audioDevice), WTFMove(videoDevice), WTFMove(deviceIdentifierHashSalt));
+    request->allow(WTFMove(audioDevice), WTFMove(videoDevice), WTFMove(deviceIdentifierHashSalt), WTFMove(completionHandler));
 }
 
 void UserMediaPermissionRequestManager::userMediaAccessWasDenied(uint64_t requestID, WebCore::UserMediaRequest::MediaAccessDenialReason reason, String&& invalidConstraint)
diff --git a/Source/WebKit/WebProcess/MediaStream/UserMediaPermissionRequestManager.h b/Source/WebKit/WebProcess/MediaStream/UserMediaPermissionRequestManager.h
index b0c2c35..d7c5c68 100644
--- a/Source/WebKit/WebProcess/MediaStream/UserMediaPermissionRequestManager.h
+++ b/Source/WebKit/WebProcess/MediaStream/UserMediaPermissionRequestManager.h
@@ -42,7 +42,7 @@
 
     void startUserMediaRequest(WebCore::UserMediaRequest&);
     void cancelUserMediaRequest(WebCore::UserMediaRequest&);
-    void userMediaAccessWasGranted(uint64_t, WebCore::CaptureDevice&& audioDevice, WebCore::CaptureDevice&& videoDevice, String&& deviceIdentifierHashSalt);
+    void userMediaAccessWasGranted(uint64_t, WebCore::CaptureDevice&& audioDevice, WebCore::CaptureDevice&& videoDevice, String&& deviceIdentifierHashSalt, CompletionHandler<void()>&&);
     void userMediaAccessWasDenied(uint64_t, WebCore::UserMediaRequest::MediaAccessDenialReason, String&&);
 
     void enumerateMediaDevices(WebCore::MediaDevicesEnumerationRequest&);
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
index 140cd6a..93301cc 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
@@ -4153,9 +4153,9 @@
 
 #if ENABLE(MEDIA_STREAM)
 
-void WebPage::userMediaAccessWasGranted(uint64_t userMediaID, WebCore::CaptureDevice&& audioDevice, WebCore::CaptureDevice&& videoDevice, String&& mediaDeviceIdentifierHashSalt)
+void WebPage::userMediaAccessWasGranted(uint64_t userMediaID, WebCore::CaptureDevice&& audioDevice, WebCore::CaptureDevice&& videoDevice, String&& mediaDeviceIdentifierHashSalt, CompletionHandler<void()>&& completionHandler)
 {
-    m_userMediaPermissionRequestManager->userMediaAccessWasGranted(userMediaID, WTFMove(audioDevice), WTFMove(videoDevice), WTFMove(mediaDeviceIdentifierHashSalt));
+    m_userMediaPermissionRequestManager->userMediaAccessWasGranted(userMediaID, WTFMove(audioDevice), WTFMove(videoDevice), WTFMove(mediaDeviceIdentifierHashSalt), WTFMove(completionHandler));
 }
 
 void WebPage::userMediaAccessWasDenied(uint64_t userMediaID, uint64_t reason, String&& invalidConstraint)
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.h b/Source/WebKit/WebProcess/WebPage/WebPage.h
index ec15c43..1c9b99b 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.h
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.h
@@ -1445,7 +1445,7 @@
     void didReceiveNotificationPermissionDecision(uint64_t notificationID, bool allowed);
 
 #if ENABLE(MEDIA_STREAM)
-    void userMediaAccessWasGranted(uint64_t userMediaID, WebCore::CaptureDevice&& audioDeviceUID, WebCore::CaptureDevice&& videoDeviceUID, String&& mediaDeviceIdentifierHashSalt);
+    void userMediaAccessWasGranted(uint64_t userMediaID, WebCore::CaptureDevice&& audioDeviceUID, WebCore::CaptureDevice&& videoDeviceUID, String&& mediaDeviceIdentifierHashSalt, CompletionHandler<void()>&&);
     void userMediaAccessWasDenied(uint64_t userMediaID, uint64_t reason, String&& invalidConstraint);
 
     void didCompleteMediaDeviceEnumeration(uint64_t userMediaID, const Vector<WebCore::CaptureDevice>& devices, String&& deviceIdentifierHashSalt, bool originHasPersistentAccess);
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in
index f2f49d5..941c607 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in
@@ -351,7 +351,7 @@
 
 #if ENABLE(MEDIA_STREAM)
     # MediaSteam
-    UserMediaAccessWasGranted(uint64_t userMediaID, WebCore::CaptureDevice audioDevice, WebCore::CaptureDevice videoDevice, String mediaDeviceIdentifierHashSalt)
+    UserMediaAccessWasGranted(uint64_t userMediaID, WebCore::CaptureDevice audioDevice, WebCore::CaptureDevice videoDevice, String mediaDeviceIdentifierHashSalt) -> () Async
     UserMediaAccessWasDenied(uint64_t userMediaID, uint64_t reason, String invalidConstraint)
     DidCompleteMediaDeviceEnumeration(uint64_t userMediaID, Vector<WebCore::CaptureDevice> devices, String mediaDeviceIdentifierHashSalt, bool hasPersistentAccess)
     CaptureDevicesChanged()
diff --git a/Source/WebKit/WebProcess/WebProcess.cpp b/Source/WebKit/WebProcess/WebProcess.cpp
index d21bbf6..5c2a9b9 100644
--- a/Source/WebKit/WebProcess/WebProcess.cpp
+++ b/Source/WebKit/WebProcess/WebProcess.cpp
@@ -1857,11 +1857,30 @@
     }
 }
 
+static inline void checkDocumentsCaptureStateConsistency(const Vector<String>& extensionIDs)
+{
+#if !ASSERT_DISABLED
+    bool isCapturingAudio = WTF::anyOf(Document::allDocumentsMap().values(), [](auto* document) {
+        return document->mediaState() & MediaProducer::AudioCaptureMask;
+    });
+    bool isCapturingVideo = WTF::anyOf(Document::allDocumentsMap().values(), [](auto* document) {
+        return document->mediaState() & MediaProducer::VideoCaptureMask;
+    });
+
+    if (isCapturingAudio)
+        ASSERT(extensionIDs.findMatching([](auto& id) { return id.contains("microphone"); }) == notFound);
+    if (isCapturingVideo)
+        ASSERT(extensionIDs.findMatching([](auto& id) { return id.contains("camera"); }) == notFound);
+#endif
+}
+
 void WebProcess::revokeUserMediaDeviceSandboxExtensions(const Vector<String>& extensionIDs)
 {
+    checkDocumentsCaptureStateConsistency(extensionIDs);
+
     for (const auto& extensionID : extensionIDs) {
         auto extension = m_mediaCaptureSandboxExtensions.take(extensionID);
-        ASSERT(extension);
+        ASSERT(extension || MockRealtimeMediaSourceCenter::mockRealtimeMediaSourceCenterEnabled());
         if (extension) {
             extension->revoke();
             RELEASE_LOG(WebRTC, "UserMediaPermissionRequestManager::revokeUserMediaDeviceSandboxExtensions - revoked extension %s", extensionID.utf8().data());