Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Compositing
DescriptionMemory corruption in Compositing
ComponentCompositing
Sub-componentInternals>Compositing, Internals>Sandbox>SiteIsolation
Bug ClassMemory Corruption
Tracker496644575
Fix commit1d197eb5a26b (chromium/src) +97/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
for
components/viz/service/frame_sinks/frame_sink_manager_impl.cc
modified

Files Changed

  • components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
  • components/viz/service/frame_sinks/frame_sink_manager_impl.cc
From 1d197eb5a26b0212a49437d5e32d03bee6215e11 Mon Sep 17 00:00:00 2001
From: Palak Agarwal <[email protected]>
Date: Tue, 15 Sep 2026 13:08:53 -0700
Subject: [PATCH] Restrict Region Capture frame sink resolution to target hierarchy

This change modifies the function FindCapturableFrameSink to only allow
candidate frame sinks that match target.frame_sink_id or its child frame
sinks in the hierarchy.

Bug: 550238190, 496644575
Change-Id: Ia2a19830ed79b848c3c7e221b4015185b0e6a18a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8358594
Reviewed-by: Kyle Charbonneau <[email protected]>
Commit-Queue: Jordan Bayles <[email protected]>
Reviewed-by: Jordan Bayles <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1698326}
---
diff --git a/components/viz/service/frame_sinks/frame_sink_manager_impl.cc b/components/viz/service/frame_sinks/frame_sink_manager_impl.cc
index 743df44..a7c11cba 100644
--- a/components/viz/service/frame_sinks/frame_sink_manager_impl.cc
+++ b/components/viz/service/frame_sinks/frame_sink_manager_impl.cc
@@ -831,26 +831,33 @@
 
 CapturableFrameSink* FrameSinkManagerImpl::FindCapturableFrameSink(
     const VideoCaptureTarget& target) {
+  if (!target.frame_sink_id.is_valid()) {
+    return nullptr;
+  }
+
   // Search the known CompositorFrameSinkSupport objects for region capture
-  // bounds matching the crop ID specified by |target| (if one was set), and
-  // return the corresponding frame sink.
+  // bounds matching the crop ID specified by `target` (if one was set), and
+  // return the corresponding frame sink. The search must be restricted to
+  // `target.frame_sink_id` and its child frame sinks (e.g. Out-Of-Process
+  // iframes) to prevent capturing an unrelated frame sink (such as an
+  // unselected tab).
   if (IsRegionCapture(target.sub_target)) {
     const auto crop_id = std::get<RegionCaptureCropId>(target.sub_target);
     for (const auto& id_and_sink : support_map_) {
       const RegionCaptureBounds& bounds =
           id_and_sink.second->current_capture_bounds();
-      if (bounds.bounds().contains(crop_id)) {
+      if (!bounds.bounds().contains(crop_id)) {
+        continue;
+      }
+      if (id_and_sink.first == target.frame_sink_id ||
+          ChildContains(target.frame_sink_id, id_and_sink.first)) {
         return id_and_sink.second;
       }
     }
     return nullptr;
   }
 
-  FrameSinkId frame_sink_id = target.frame_sink_id;
-  if (!frame_sink_id.is_valid())
-    return nullptr;
-
-  return base::FindPtrOrNull(support_map_, frame_sink_id);
+  return base::FindPtrOrNull(support_map_, target.frame_sink_id);
 }
 
 void FrameSinkManagerImpl::OnCapturerConnectionLost(
diff --git a/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc b/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
index 81cb55ee..232553c 100644
--- a/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
+++ b/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
@@ -14,6 +14,7 @@
 #include "components/viz/common/constants.h"
 #include "components/viz/common/frame_sinks/begin_frame_source.h"
 #include "components/viz/common/surfaces/parent_local_surface_id_allocator.h"
+#include "components/viz/common/surfaces/region_capture_bounds.h"
 #include "components/viz/service/frame_sinks/compositor_frame_sink_support.h"
 #include "components/viz/service/gl/mock_gpu_service_impl.h"
 #include "components/viz/service/input/mock_input_manager.h"
@@ -115,6 +116,11 @@
     return manager_->FindCapturableFrameSink(VideoCaptureTarget(id));
   }
 
+  CapturableFrameSink* FindCapturableFrameSink(
+      const VideoCaptureTarget& target) {
+    return manager_->FindCapturableFrameSink(target);
+  }
+
   // Verifies the frame sinks with provided id in |ids| are throttled at
   // |interval|.
   void VerifyThrottling(base::TimeDelta interval,
@@ -2189,6 +2195,82 @@
   manager_->InvalidateFrameSinkId(kFrameSinkIdC, {});
 }
 
+TEST_F(FrameSinkManagerTest,
+       FindCapturableFrameSinkRegionCaptureRestrictedToHierarchy) {
+  manager_->RegisterFrameSinkId(kFrameSinkIdRoot, true);
+  manager_->RegisterFrameSinkId(kFrameSinkIdA, true);
+  manager_->RegisterFrameSinkId(kFrameSinkIdB, true);
 
+  // Set up hierarchy: Root -> Child (A).
+  // B is an unrelated frame sink (e.g. another tab).
+  manager_->RegisterFrameSinkHierarchy(kFrameSinkIdRoot, kFrameSinkIdA);
+
+  auto sink_root = CreateCompositorFrameSinkSupport(kFrameSinkIdRoot);
+  auto sink_a = CreateCompositorFrameSinkSupport(kFrameSinkIdA);
+  auto sink_b = CreateCompositorFrameSinkSupport(kFrameSinkIdB);
+
+  ParentLocalSurfaceIdAllocator allocator_root;
+  ParentLocalSurfaceIdAllocator allocator_a;
+  ParentLocalSurfaceIdAllocator allocator_b;
+  allocator_root.GenerateId();
+  allocator_a.GenerateId();
+  allocator_b.GenerateId();
+  LocalSurfaceId surface_id_root = allocator_root.GetCurrentLocalSurfaceId();
+  LocalSurfaceId surface_id_a = allocator_a.GetCurrentLocalSurfaceId();
+  LocalSurfaceId surface_id_b = allocator_b.GetCurrentLocalSurfaceId();
+
+  const auto crop_id = RegionCaptureCropId::CreateRandom();
+  RegionCaptureBounds bounds;
+  bounds.Set(crop_id, gfx::Rect(0, 0, 100, 100));
+
+  constexpr gfx::Rect kDamageRect(0, 0, 100, 100);
+
+  // Set crop ID on sink_b.
+  auto frame_b = MakeDefaultCompositorFrame();
+  frame_b.metadata.capture_bounds = bounds;
+  sink_b->OnSurfaceAggregatedDamage(nullptr, surface_id_b, frame_b, kDamageRect,
+                                    base::TimeTicks::Now());
+
+  // Set empty capture bounds on root and child A.
+  auto empty_frame = MakeDefaultCompositorFrame();
+  sink_root->OnSurfaceAggregatedDamage(nullptr, surface_id_root, empty_frame,
+                                       kDamageRect, base::TimeTicks::Now());
+  sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, empty_frame,
+                                    kDamageRect, base::TimeTicks::Now());
+
+  // Searching for crop_id targeting kFrameSinkIdRoot must NOT return sink_b,
+  // since B is not in the hierarchy of Root.
+  VideoCaptureTarget target_root(kFrameSinkIdRoot, crop_id);
+  EXPECT_EQ(FindCapturableFrameSink(target_root), nullptr);
+
+  // Now set the crop ID on child A.
+  auto frame_a = MakeDefaultCompositorFrame();
+  frame_a.metadata.capture_bounds = bounds;
+  sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, frame_a, kDamageRect,
+                                    base::TimeTicks::Now());
+
+  // Searching for crop_id targeting kFrameSinkIdRoot should find child A.
+  EXPECT_EQ(FindCapturableFrameSink(target_root), sink_a.get());
+
+  // Clear crop ID from child A and set on root itself.
+  sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, empty_frame,
+                                    kDamageRect, base::TimeTicks::Now());
+  auto frame_root = MakeDefaultCompositorFrame();
+  frame_root.metadata.capture_bounds = bounds;
+  sink_root->OnSurfaceAggregatedDamage(nullptr, surface_id_root, frame_root,
+                                       kDamageRect, base::TimeTicks::Now());
+
+  // Searching for crop_id targeting kFrameSinkIdRoot should find root.
+  EXPECT_EQ(FindCapturableFrameSink(target_root), sink_root.get());
+
+  // An invalid target frame sink id must return nullptr.
+  VideoCaptureTarget invalid_target;
+  EXPECT_EQ(FindCapturableFrameSink(invalid_target), nullptr);
+
+  manager_->UnregisterFrameSinkHierarchy(kFrameSinkIdRoot, kFrameSinkIdA);
+  manager_->InvalidateFrameSinkId(kFrameSinkIdRoot, {});
+  manager_->InvalidateFrameSinkId(kFrameSinkIdA, {});
+  manager_->InvalidateFrameSinkId(kFrameSinkIdB, {});
+}
 
 }  // namespace viz
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc b/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
index 81cb55ee..232553c 100644
--- a/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
+++ b/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
@@ -14,6 +14,7 @@
 #include "components/viz/common/constants.h"
 #include "components/viz/common/frame_sinks/begin_frame_source.h"
 #include "components/viz/common/surfaces/parent_local_surface_id_allocator.h"
+#include "components/viz/common/surfaces/region_capture_bounds.h"
 #include "components/viz/service/frame_sinks/compositor_frame_sink_support.h"
 #include "components/viz/service/gl/mock_gpu_service_impl.h"
 #include "components/viz/service/input/mock_input_manager.h"
@@ -115,6 +116,11 @@
     return manager_->FindCapturableFrameSink(VideoCaptureTarget(id));
   }
 
+  CapturableFrameSink* FindCapturableFrameSink(
+      const VideoCaptureTarget& target) {
+    return manager_->FindCapturableFrameSink(target);
+  }
+
   // Verifies the frame sinks with provided id in |ids| are throttled at
   // |interval|.
   void VerifyThrottling(base::TimeDelta interval,
@@ -2189,6 +2195,82 @@
   manager_->InvalidateFrameSinkId(kFrameSinkIdC, {});
 }
 
+TEST_F(FrameSinkManagerTest,
+       FindCapturableFrameSinkRegionCaptureRestrictedToHierarchy) {
+  manager_->RegisterFrameSinkId(kFrameSinkIdRoot, true);
+  manager_->RegisterFrameSinkId(kFrameSinkIdA, true);
+  manager_->RegisterFrameSinkId(kFrameSinkIdB, true);
 
+  // Set up hierarchy: Root -> Child (A).
+  // B is an unrelated frame sink (e.g. another tab).
+  manager_->RegisterFrameSinkHierarchy(kFrameSinkIdRoot, kFrameSinkIdA);
+
+  auto sink_root = CreateCompositorFrameSinkSupport(kFrameSinkIdRoot);
+  auto sink_a = CreateCompositorFrameSinkSupport(kFrameSinkIdA);
+  auto sink_b = CreateCompositorFrameSinkSupport(kFrameSinkIdB);
+
+  ParentLocalSurfaceIdAllocator allocator_root;
+  ParentLocalSurfaceIdAllocator allocator_a;
+  ParentLocalSurfaceIdAllocator allocator_b;
+  allocator_root.GenerateId();
+  allocator_a.GenerateId();
+  allocator_b.GenerateId();
+  LocalSurfaceId surface_id_root = allocator_root.GetCurrentLocalSurfaceId();
+  LocalSurfaceId surface_id_a = allocator_a.GetCurrentLocalSurfaceId();
+  LocalSurfaceId surface_id_b = allocator_b.GetCurrentLocalSurfaceId();
+
+  const auto crop_id = RegionCaptureCropId::CreateRandom();
+  RegionCaptureBounds bounds;
+  bounds.Set(crop_id, gfx::Rect(0, 0, 100, 100));
+
+  constexpr gfx::Rect kDamageRect(0, 0, 100, 100);
+
+  // Set crop ID on sink_b.
+  auto frame_b = MakeDefaultCompositorFrame();
+  frame_b.metadata.capture_bounds = bounds;
+  sink_b->OnSurfaceAggregatedDamage(nullptr, surface_id_b, frame_b, kDamageRect,
+                                    base::TimeTicks::Now());
+
+  // Set empty capture bounds on root and child A.
+  auto empty_frame = MakeDefaultCompositorFrame();
+  sink_root->OnSurfaceAggregatedDamage(nullptr, surface_id_root, empty_frame,
+                                       kDamageRect, base::TimeTicks::Now());
+  sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, empty_frame,
+                                    kDamageRect, base::TimeTicks::Now());
+
+  // Searching for crop_id targeting kFrameSinkIdRoot must NOT return sink_b,
+  // since B is not in the hierarchy of Root.
+  VideoCaptureTarget target_root(kFrameSinkIdRoot, crop_id);
+  EXPECT_EQ(FindCapturableFrameSink(target_root), nullptr);
+
+  // Now set the crop ID on child A.
+  auto frame_a = MakeDefaultCompositorFrame();
+  frame_a.metadata.capture_bounds = bounds;
+  sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, frame_a, kDamageRect,
+                                    base::TimeTicks::Now());
+
+  // Searching for crop_id targeting kFrameSinkIdRoot should find child A.
+  EXPECT_EQ(FindCapturableFrameSink(target_root), sink_a.get());
+
+  // Clear crop ID from child A and set on root itself.
+  sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, empty_frame,
+                                    kDamageRect, base::TimeTicks::Now());
+  auto frame_root = MakeDefaultCompositorFrame();
+  frame_root.metadata.capture_bounds = bounds;
+  sink_root->OnSurfaceAggregatedDamage(nullptr, surface_id_root, frame_root,
+                                       kDamageRect, base::TimeTicks::Now());
+
+  // Searching for crop_id targeting kFrameSinkIdRoot should find root.
+  EXPECT_EQ(FindCapturableFrameSink(target_root), sink_root.get());
+
+  // An invalid target frame sink id must return nullptr.
+  VideoCaptureTarget invalid_target;
+  EXPECT_EQ(FindCapturableFrameSink(invalid_target), nullptr);
+
+  manager_->UnregisterFrameSinkHierarchy(kFrameSinkIdRoot, kFrameSinkIdA);
+  manager_->InvalidateFrameSinkId(kFrameSinkIdRoot, {});
+  manager_->InvalidateFrameSinkId(kFrameSinkIdA, {});
+  manager_->InvalidateFrameSinkId(kFrameSinkIdB, {});
+}
 
 }  // namespace viz
Loading diff…

Original Bug Report

reported by [email protected]

Site Isolation bypass via Region Capture target hijacking in Viz

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A logic flaw in Viz’s FrameSinkManagerImpl::FindCapturableFrameSink allows a compromised renderer to hijack an active screen capture session. By forging Region Capture metadata, an attacker can force Viz to capture an unrelated tab, bypassing Site Isolation and user consent to exfiltrate cross-origin pixels.

Affected files:

  • components/viz/service/frame_sinks/frame_sink_manager_impl.cc
  • services/viz/public/cpp/compositing/region_capture_bounds_mojom_traits.h
  • content/browser/renderer_host/media/media_stream_dispatcher_host.cc
  • components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.cc
  • content/browser/renderer_host/media/media_devices_dispatcher_host.cc

Estimated timestamp from git blame: 2026-01-06

Description

There is a potential Site Isolation bypass in the Viz component related to Region Capture. When a capture session is cropped using a RegionCaptureCropId, the target resolution logic in Viz completely ignores the browser-provided, authorized frame_sink_id. Instead, it searches globally across all frame sinks to find one claiming the crop ID.

A compromised renderer can exploit this by minting a legitimate token in one tab, passing it to a compromised renderer in a background tab, and forging CompositorFrame metadata to hijack the capture session. This allows the attacker to steal pixels from cross-origin iframes loaded in the background tab.

Root Cause

In components/viz/service/frame_sinks/frame_sink_manager_impl.cc, the FrameSinkManagerImpl::FindCapturableFrameSink function resolves the capture target.

CapturableFrameSink* FrameSinkManagerImpl::FindCapturableFrameSink(
    const VideoCaptureTarget& target) {
  if (IsRegionCapture(target.sub_target)) {
    const auto crop_id = std::get<RegionCaptureCropId>(target.sub_target);
    for (const auto& id_and_sink : support_map_) { // <-- GLOBAL ITERATION
      const RegionCaptureBounds& bounds =
          id_and_sink.second->current_capture_bounds();
      if (bounds.bounds().contains(crop_id)) {
        return id_and_sink.second;
      }
    }
    return nullptr;
  }
  // ... (code that normally uses target.frame_sink_id is skipped)
}

Because support_map_ contains all CompositorFrameSinkSupport instances across the entire browser, any compromised renderer can claim the crop_id via forged IPC metadata, and Viz will happily attach the capture session to it, completely ignoring the target.frame_sink_id that restricts capture to the user-authorized tab.

Potential Attack Scenario

Note: These are suggested steps based on code analysis; a runnable PoC has not yet been executed.

  1. Setup: An attacker controls Tab A (an ordinary web page) and has compromised the renderer process for Tab C. Tab C embeds a sensitive cross-origin iframe (e.g., banking.com).
  2. Mint Token: Tab A’s JavaScript calls CropTarget.fromElement(). The browser securely generates a crop_id token and associates it with Tab A.
  3. Exfiltrate Token: Tab A sends the minted crop_id string to the compromised renderer in Tab C via a side channel (e.g., BroadcastChannel).
  4. Forge Metadata: The compromised renderer in Tab C submits a viz::CompositorFrame with a forged viz::RegionCaptureBounds object in its metadata. This maps the stolen crop_id to a gfx::Rect covering the banking.com iframe.
  5. Initiate Capture: The user is induced to share Tab A (e.g., via getDisplayMedia({preferCurrentTab: true})). Tab A obtains a MediaStreamTrack capturing its own content.
  6. Hijack Target: Tab A calls track.cropTo(crop_id). The browser validates the request (which succeeds since the token was legitimately minted for Tab A) and forwards the VideoCaptureTarget to Viz.
  7. Pixel Exfiltration: Viz’s FindCapturableFrameSink ignores Tab A’s frame_sink_id, searches support_map_ globally, and finds Tab C’s forged claim first. The capture session seamlessly redirects to Tab C, sending the cross-origin banking.com pixels back to Tab A’s JavaScript.

Suggested Fix

In FrameSinkManagerImpl::FindCapturableFrameSink, do not iterate over the global support_map_. Instead:

  1. First resolve the legitimate CapturableFrameSink using target.frame_sink_id.
  2. Verify that the resolved frame sink (or its descendants in the surface tree) actually contains the crop_id in their current_capture_bounds_.
  3. Return the specific sink within that authorized surface tree, preventing cross-tree hijacking.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker