[Reland][Region Capture] Fix blocking of other-tab captures

Ensure the cropped track belongs in the same tab
as the issuer of the Crop() request before cropping.

This is a reland of crrev.com/c/3650512.

(cherry picked from commit 0cec84b97e54dc8db0bf28f53e37d5efa74f910e)

Bug: 1322873
Change-Id: I05692c3daf74b2d64506a73b85ff5fd40e74c729
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3654228
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Reviewed-by: Jordan Bayles <jophba@chromium.org>
Commit-Queue: Elad Alon <eladalon@chromium.org>
Cr-Original-Commit-Position: refs/heads/main@{#1005388}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3660227
Auto-Submit: Elad Alon <eladalon@chromium.org>
Commit-Queue: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/branch-heads/5060@{#173}
Cr-Branched-From: b83393d0f4038aeaf67f970a024d8101df7348d1-refs/heads/main@{#1002911}
diff --git a/chrome/browser/media/webrtc/region_capture_browsertest.cc b/chrome/browser/media/webrtc/region_capture_browsertest.cc
index a7c8804..baa9ac6 100644
--- a/chrome/browser/media/webrtc/region_capture_browsertest.cc
+++ b/chrome/browser/media/webrtc/region_capture_browsertest.cc
@@ -40,6 +40,11 @@
 namespace {
 
 using content::WebContents;
+using testing::Bool;
+using testing::Combine;
+using testing::TestParamInfo;
+using testing::Values;
+using testing::WithParamInterface;
 
 // TODO(crbug.com/1247761): Add tests that verify excessive calls to
 // produceCropId() yield the empty string.
@@ -67,7 +72,7 @@
   kServerCount  // Must be last.
 };
 
-enum {
+enum Tab {
   kMainTab,
   kOtherTab,
   kTabCount  // Must be last.
@@ -102,6 +107,9 @@
   }
 
   void StartCaptureFromEmbeddedFrame() {
+    // Bring the tab into focus. This avoids getDisplayMedia rejection.
+    browser->tab_strip_model()->ActivateTabAt(tab_strip_index);
+
     std::string script_result;
     EXPECT_TRUE(content::ExecuteScriptAndExtractString(
         web_contents->GetMainFrame(), "startCaptureFromEmbeddedFrame();",
@@ -155,6 +163,8 @@
 // detection of JS errors.
 class RegionCaptureBrowserTest : public WebRtcTestBase {
  public:
+  ~RegionCaptureBrowserTest() override = default;
+
   void SetUpInProcessBrowserTestFixture() override {
     WebRtcTestBase::SetUpInProcessBrowserTestFixture();
 
@@ -215,20 +225,18 @@
   // Set up all (necessary) tabs, loads iframes, and start capturing the
   // relevant tab.
   void SetUpTest(Frame capturing_entity, bool self_capture) {
-    // Main page (for self-capture).
+    // Other page (for other-tab-capture).
+    SetUpPage("/webrtc/region_capture_other_main.html",
+              servers_[kOtherPageTopLevelDocument].get(),
+              "/webrtc/region_capture_other_embedded.html",
+              servers_[kOtherPageEmbeddedDocument].get(), &tabs_[kOtherTab]);
+
+    // Main page (for self-capture). Instantiate it second to help it get focus.
     SetUpPage("/webrtc/region_capture_main.html",
               servers_[kMainPageTopLevelDocument].get(),
               "/webrtc/region_capture_embedded.html",
               servers_[kMainPageEmbeddedDocument].get(), &tabs_[kMainTab]);
 
-    if (!self_capture) {
-      // Other page (for other-tab-capture).
-      SetUpPage("/webrtc/region_capture_other_main.html",
-                servers_[kOtherPageTopLevelDocument].get(),
-                "/webrtc/region_capture_other_embedded.html",
-                servers_[kOtherPageEmbeddedDocument].get(), &tabs_[kOtherTab]);
-    }
-
     DCHECK(command_line_);
     command_line_->AppendSwitchASCII(
         switches::kAutoSelectTabCaptureSourceByTitle,
@@ -376,28 +384,6 @@
   EXPECT_EQ(tab.CropTo(""), "top-level-crop-success");
 }
 
-IN_PROC_BROWSER_TEST_F(RegionCaptureBrowserTest,
-                       CropToRejectedIfElementInAnotherTabTopLevel) {
-  SetUpTest(Frame::kTopLevelDocument, /*self_capture=*/false);
-
-  const std::string crop_id =
-      tabs_[kOtherTab].ProduceCropId(Frame::kTopLevelDocument);
-  ASSERT_THAT(crop_id, IsValidCropId());
-
-  EXPECT_EQ(tabs_[kMainTab].CropTo(crop_id), "top-level-crop-error");
-}
-
-IN_PROC_BROWSER_TEST_F(RegionCaptureBrowserTest,
-                       CropToRejectedIfElementInAnotherTabEmbeddedFrame) {
-  SetUpTest(Frame::kTopLevelDocument, /*self_capture=*/false);
-
-  const std::string crop_id =
-      tabs_[kOtherTab].ProduceCropId(Frame::kEmbeddedFrame);
-  ASSERT_THAT(crop_id, IsValidCropId());
-
-  EXPECT_EQ(tabs_[kMainTab].CropTo(crop_id), "top-level-crop-error");
-}
-
 IN_PROC_BROWSER_TEST_F(RegionCaptureBrowserTest, MaxCropIdsInTopLevelDocument) {
   SetUpTest(Frame::kNone, /*self_capture=*/false);
   TabInfo& tab = tabs_[kMainTab];
@@ -495,4 +481,85 @@
             "embedded-produce-crop-id-error");
 }
 
+// Suite of tests ensuring that only self-capture may crop, and that it may
+// only crop to targets in its own tab, but that any target in its own tab
+// is permitted.
+class RegionCaptureSelfCaptureOnlyBrowserTest
+    : public RegionCaptureBrowserTest,
+      public WithParamInterface<std::tuple<Frame, bool, Tab, Frame>> {
+ public:
+  RegionCaptureSelfCaptureOnlyBrowserTest()
+      : capturing_entity_(std::get<0>(GetParam())),
+        self_capture_(std::get<1>(GetParam())),
+        target_element_tab_(std::get<2>(GetParam())),
+        target_frame_(std::get<3>(GetParam())) {}
+  ~RegionCaptureSelfCaptureOnlyBrowserTest() override = default;
+
+ protected:
+  // The capture is done from kMainTab in all instances of this parameterized
+  // test. |capturing_entity_| controls whether the capture is initiated
+  // from the top-level document of said tab, or an embedded frame.
+  const Frame capturing_entity_;
+
+  // Whether capturing self, or capturing the other tab.
+  const bool self_capture_;
+
+  // Whether the element on whose crop-ID we'll call cropTo():
+  // * |target_element_tab_| - whether it's in kMainTab or in kOtherTab.
+  // * |target_frame_| - whether it's in the top-level or an embedded frame.
+  const Tab target_element_tab_;
+  const Frame target_frame_;  // Top-level or embedded frame.
+};
+
+std::string ParamsToString(
+    const TestParamInfo<RegionCaptureSelfCaptureOnlyBrowserTest::ParamType>&
+        info) {
+  return base::StrCat(
+      {std::get<0>(info.param) == Frame::kTopLevelDocument ? "TopLevel"
+                                                           : "EmbeddedFrame",
+       std::get<1>(info.param) ? "SelfCapturing" : "CapturingOtherTab",
+       "AndCroppingToElementIn",
+       std::get<2>(info.param) == kMainTab ? "OwnTabs" : "OtherTabs",
+       std::get<3>(info.param) == Frame::kTopLevelDocument ? "TopLevel"
+                                                           : "EmbeddedFrame"});
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    _,
+    RegionCaptureSelfCaptureOnlyBrowserTest,
+    Combine(Values(Frame::kTopLevelDocument, Frame::kEmbeddedFrame),
+            Bool(),
+            Values(kMainTab, kOtherTab),
+            Values(Frame::kTopLevelDocument, Frame::kEmbeddedFrame)),
+    ParamsToString);
+
+IN_PROC_BROWSER_TEST_P(RegionCaptureSelfCaptureOnlyBrowserTest, CropTo) {
+  SetUpTest(capturing_entity_, self_capture_);
+
+  // Prevent test false-positive - ensure that both tabs participating in the
+  // test have at least one associated crop-ID, or otherwise they would not
+  // have a CropIdWebContentsHelper.
+  // To make things even clearer, ensure both the top-level and the embedded
+  // frame have produced crop-IDs. (This should not be necessary, but is
+  // done as an extra buffer against false-positives.)
+  tabs_[kMainTab].ProduceCropId(Frame::kTopLevelDocument);
+  tabs_[kMainTab].ProduceCropId(Frame::kEmbeddedFrame);
+  tabs_[kOtherTab].ProduceCropId(Frame::kTopLevelDocument);
+  tabs_[kOtherTab].ProduceCropId(Frame::kEmbeddedFrame);
+
+  const std::string crop_id =
+      tabs_[target_element_tab_].ProduceCropId(target_frame_);
+  ASSERT_THAT(crop_id, IsValidCropId());
+
+  // Cropping only permitted if both conditions hold.
+  const bool expect_permitted =
+      (self_capture_ && target_element_tab_ == kMainTab);
+
+  const std::string expected_result = base::StrCat(
+      {capturing_entity_ == Frame::kTopLevelDocument ? "top-level" : "embedded",
+       "-", expect_permitted ? "crop-success" : "crop-error"});
+
+  EXPECT_EQ(tabs_[kMainTab].CropTo(crop_id), expected_result);
+}
+
 #endif  //  !BUILDFLAG(IS_CHROMEOS_LACROS)
diff --git a/content/browser/renderer_host/media/media_stream_dispatcher_host.cc b/content/browser/renderer_host/media/media_stream_dispatcher_host.cc
index e8d31bee..bff670e 100644
--- a/content/browser/renderer_host/media/media_stream_dispatcher_host.cc
+++ b/content/browser/renderer_host/media/media_stream_dispatcher_host.cc
@@ -17,6 +17,7 @@
 #include "content/browser/renderer_host/media/video_capture_manager.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
+#include "content/public/browser/global_routing_id.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_process_host.h"
 #include "content/public/browser/render_view_host.h"
@@ -68,26 +69,40 @@
 }
 
 #if !BUILDFLAG(IS_ANDROID)
+// Helper for getting the top-level WebContents associated with a given ID.
+// Returns nullptr if one does not exist (e.g. has gone away).
+WebContents* GetMainFrameWebContents(const GlobalRoutingID& global_routing_id) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+  if (global_routing_id == GlobalRoutingID()) {
+    return nullptr;
+  }
+
+  RenderFrameHost* const rfh = RenderFrameHost::FromID(
+      global_routing_id.child_id, global_routing_id.route_id);
+  return rfh ? WebContents::FromRenderFrameHost(rfh->GetMainFrame()) : nullptr;
+}
+
 // Checks whether a track living in the WebContents indicated by
 // (render_process_id, render_frame_id) may be cropped to the crop-target
 // indicated by |crop_id|.
-bool IsCropTargetValid(int render_process_id,
-                       int render_frame_id,
-                       const base::Token& crop_id) {
-  RenderFrameHost* const rfh =
-      RenderFrameHost::FromID(render_process_id, render_frame_id);
-  if (!rfh) {
+bool MayCrop(const GlobalRoutingID& capturing_id,
+             const GlobalRoutingID& captured_id,
+             const base::Token& crop_id) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+  WebContents* const capturing_wc = GetMainFrameWebContents(capturing_id);
+  if (!capturing_wc) {
     return false;
   }
 
-  WebContents* const web_contents =
-      WebContents::FromRenderFrameHost(rfh->GetMainFrame());
-  if (!web_contents) {
+  WebContents* const captured_wc = GetMainFrameWebContents(captured_id);
+  if (capturing_wc != captured_wc) {  // Null or not-same-tab.
     return false;
   }
 
   CropIdWebContentsHelper* const helper =
-      CropIdWebContentsHelper::FromWebContents(web_contents);
+      CropIdWebContentsHelper::FromWebContents(captured_wc);
   if (!helper) {
     // No crop-IDs were ever produced on this WebContents.
     // Any non-zero crop-ID should be rejected on account of being
@@ -531,6 +546,10 @@
                                      CropCallback callback) {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
 
+  const GlobalRoutingID captured_id =
+      media_stream_manager_->video_capture_manager()->GetGlobalRoutingID(
+          device_id);
+
   // Hop to the UI thread to verify that cropping to |crop_id| is permitted
   // from this particular context. Namely, cropping is currently only allowed
   // for self-capture, so the crop_id has to be associated with the top-level
@@ -539,8 +558,9 @@
   // when SelfOwnedReceiver properly supports this.
   GetUIThreadTaskRunner({})->PostTaskAndReplyWithResult(
       FROM_HERE,
-      base::BindOnce(&IsCropTargetValid, render_process_id_, render_frame_id_,
-                     crop_id),
+      base::BindOnce(&MayCrop,
+                     GlobalRoutingID(render_process_id_, render_frame_id_),
+                     captured_id, crop_id),
       base::BindOnce(&MediaStreamDispatcherHost::OnCropValidationComplete,
                      weak_factory_.GetWeakPtr(), device_id, crop_id,
                      crop_version,
diff --git a/content/browser/renderer_host/media/video_capture_manager.cc b/content/browser/renderer_host/media/video_capture_manager.cc
index 1047d87..ce668fb 100644
--- a/content/browser/renderer_host/media/video_capture_manager.cc
+++ b/content/browser/renderer_host/media/video_capture_manager.cc
@@ -596,6 +596,29 @@
   return device_in_use ? device_in_use->GetVideoCaptureFormat() : absl::nullopt;
 }
 
+GlobalRoutingID VideoCaptureManager::GetGlobalRoutingID(
+    const base::UnguessableToken& session_id) const {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+
+  VideoCaptureController* const controller =
+      LookupControllerBySessionId(session_id);
+  if (!controller || !controller->IsDeviceAlive() ||
+      !blink::IsVideoDesktopCaptureMediaType(controller->stream_type())) {
+    return GlobalRoutingID();
+  }
+
+  const DesktopMediaID desktop_media_id =
+      DesktopMediaID::Parse(controller->device_id());
+
+  if (desktop_media_id.type != DesktopMediaID::Type::TYPE_WEB_CONTENTS ||
+      desktop_media_id.web_contents_id.is_null()) {
+    return GlobalRoutingID();
+  }
+
+  return GlobalRoutingID(desktop_media_id.web_contents_id.render_process_id,
+                         desktop_media_id.web_contents_id.main_render_frame_id);
+}
+
 void VideoCaptureManager::SetDesktopCaptureWindowId(
     const media::VideoCaptureSessionId& session_id,
     gfx::NativeViewId window_id) {
@@ -798,7 +821,7 @@
 }
 
 VideoCaptureController* VideoCaptureManager::LookupControllerBySessionId(
-    const base::UnguessableToken& session_id) {
+    const base::UnguessableToken& session_id) const {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
   SessionMap::const_iterator session_it = sessions_.find(session_id);
   if (session_it == sessions_.end())
diff --git a/content/browser/renderer_host/media/video_capture_manager.h b/content/browser/renderer_host/media/video_capture_manager.h
index cc30ec2..34a0e70 100644
--- a/content/browser/renderer_host/media/video_capture_manager.h
+++ b/content/browser/renderer_host/media/video_capture_manager.h
@@ -26,6 +26,7 @@
 #include "content/browser/renderer_host/media/video_capture_device_launch_observer.h"
 #include "content/browser/renderer_host/media/video_capture_provider.h"
 #include "content/common/content_export.h"
+#include "content/public/browser/global_routing_id.h"
 #include "content/public/browser/screenlock_observer.h"
 #include "media/base/video_facing.h"
 #include "media/capture/video/video_capture_device.h"
@@ -178,6 +179,12 @@
       blink::mojom::MediaStreamType stream_type,
       const std::string& device_id);
 
+  // If there is a capture session associated with |session_id|, and the
+  // captured entity a tab, return the GlobalRoutingID of the captured tab.
+  // Otherwise, returns an empty GlobalRoutingID.
+  GlobalRoutingID GetGlobalRoutingID(
+      const base::UnguessableToken& session_id) const;
+
   // Sets the platform-dependent window ID for the desktop capture notification
   // UI for the given session.
   void SetDesktopCaptureWindowId(const media::VideoCaptureSessionId& session_id,
@@ -251,7 +258,7 @@
   // |device_id| and |type| (if it is already opened), by its |controller| or by
   // its |serial_id|. In all cases, if not found, nullptr is returned.
   VideoCaptureController* LookupControllerBySessionId(
-      const base::UnguessableToken& session_id);
+      const base::UnguessableToken& session_id) const;
   VideoCaptureController* LookupControllerByMediaTypeAndDeviceId(
       blink::mojom::MediaStreamType type,
       const std::string& device_id) const;