CVE-2026-11203
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fui/accelerated_widget_mac/ca_layer_tree_unittest_mac.mm |
modified | |
ifui/accelerated_widget_mac/ca_renderer_layer_tree.mm |
modified | |
switchui/accelerated_widget_mac/ca_renderer_layer_tree.mm |
modified |
Files Changed
ui/accelerated_widget_mac/ca_layer_tree_unittest_mac.mmui/accelerated_widget_mac/ca_renderer_layer_tree.mm
Patch
From 51a8376f9f5ee7e0a2eb566d9b86687119e9ce0a Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Thu, 30 Apr 2026 05:36:23 -0700
Subject: [PATCH] mac: Update preventsCapture when recycling AVSampleBufferDisplayLayer
When an AVSampleBufferDisplayLayer is recycled to display
hardware-protected content from a previously clear stream (or vice
versa), the preventsCapture property was not being updated. This allowed
DRM-protected content to be screen-recorded if it matched a recycled
clear video layer.
Fixed: 505192638
Change-Id: I2494c142ed258f3ad03ac84f22d12cf1fce607b3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7791340
Reviewed-by: Sunny Sachanandani <sunnyps@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1623107}
---
diff --git a/ui/accelerated_widget_mac/ca_layer_tree_unittest_mac.mm b/ui/accelerated_widget_mac/ca_layer_tree_unittest_mac.mm
index 9bbb1102..e930c2028 100644
--- a/ui/accelerated_widget_mac/ca_layer_tree_unittest_mac.mm
+++ b/ui/accelerated_widget_mac/ca_layer_tree_unittest_mac.mm
@@ -52,6 +52,8 @@
gfx::ScopedIOSurface io_surface;
gfx::ColorSpace color_space;
base::apple::ScopedCFTypeRef<CVPixelBufferRef> cv_pixel_buffer;
+ gfx::ProtectedVideoType protected_video_type =
+ gfx::ProtectedVideoType::kClear;
bool allow_av_layers = true;
bool allow_solid_color_layers = true;
@@ -79,7 +81,7 @@
properties->transform, io_surface, io_surface_color_space,
properties->contents_rect, properties->rect, properties->background_color,
properties->edge_aa_mask, properties->opacity, properties->filter,
- gfx::HDRMetadata(), gfx::ProtectedVideoType::kClear, false));
+ gfx::HDRMetadata(), properties->protected_video_type, false));
}
void UpdateCALayerTree(std::unique_ptr<ui::CARendererLayerTree>& ca_layer_tree,
@@ -925,6 +927,61 @@
}
}
+// Ensure that preventsCapture is updated when recycling
+// AVSampleBufferDisplayLayer. Regression test for crbug.com/505192638.
+TEST_F(CALayerTreeTest, AVLayerPreventsCapture) {
+ base::test::ScopedFeatureList features;
+ features.InitWithFeatures({ui::kFullscreenLowPowerBackdropMac}, {});
+
+ CALayerProperties properties;
+ properties.io_surface =
+ gfx::CreateIOSurface(gfx::Size(256, 256), viz::MultiPlaneFormat::kNV12);
+
+ std::unique_ptr<ui::CARendererLayerTree> ca_layer_tree;
+ AVSampleBufferDisplayLayer* av_layer_old = nil;
+ AVSampleBufferDisplayLayer* av_layer_new = nil;
+
+ // Initially, video is clear, so preventsCapture should be NO.
+ {
+ properties.protected_video_type = gfx::ProtectedVideoType::kClear;
+ UpdateCALayerTree(ca_layer_tree, &properties, superlayer_);
+ CALayer* content_layer = GetOnlyContentLayer();
+ EXPECT_TRUE([content_layer
+ isKindOfClass:NSClassFromString(@"AVSampleBufferDisplayLayer")]);
+ av_layer_new = (AVSampleBufferDisplayLayer*)content_layer;
+ EXPECT_FALSE(av_layer_new.preventsCapture);
+ }
+ av_layer_old = av_layer_new;
+
+ // Change to protected video. The layer should be recycled and preventsCapture
+ // should be updated to YES.
+ {
+ properties.protected_video_type =
+ gfx::ProtectedVideoType::kHardwareProtected;
+ UpdateCALayerTree(ca_layer_tree, &properties, superlayer_);
+ CALayer* content_layer = GetOnlyContentLayer();
+ EXPECT_TRUE([content_layer
+ isKindOfClass:NSClassFromString(@"AVSampleBufferDisplayLayer")]);
+ av_layer_new = (AVSampleBufferDisplayLayer*)content_layer;
+ EXPECT_EQ(av_layer_new, av_layer_old);
+ EXPECT_TRUE(av_layer_new.preventsCapture);
+ }
+ av_layer_old = av_layer_new;
+
+ // Change back to clear video. The layer should be recycled and
+ // preventsCapture should be updated to NO.
+ {
+ properties.protected_video_type = gfx::ProtectedVideoType::kClear;
+ UpdateCALayerTree(ca_layer_tree, &properties, superlayer_);
+ CALayer* content_layer = GetOnlyContentLayer();
+ EXPECT_TRUE([content_layer
+ isKindOfClass:NSClassFromString(@"AVSampleBufferDisplayLayer")]);
+ av_layer_new = (AVSampleBufferDisplayLayer*)content_layer;
+ EXPECT_EQ(av_layer_new, av_layer_old);
+ EXPECT_FALSE(av_layer_new.preventsCapture);
+ }
+}
+
// Test fullscreen low power detection.
TEST_F(CALayerTreeTest, FullscreenLowPower) {
base::test::ScopedFeatureList features;
diff --git a/ui/accelerated_widget_mac/ca_renderer_layer_tree.mm b/ui/accelerated_widget_mac/ca_renderer_layer_tree.mm
index e356484c..27862cc9 100644
--- a/ui/accelerated_widget_mac/ca_renderer_layer_tree.mm
+++ b/ui/accelerated_widget_mac/ca_renderer_layer_tree.mm
@@ -1121,6 +1121,10 @@
update_ca_edge_aa_mask = old_layer_->ca_edge_aa_mask_ != ca_edge_aa_mask_;
update_opacity = old_layer_->opacity_ != opacity_;
update_ca_filter = old_layer_->ca_filter_ != ca_filter_;
+ if (type_ == CALayerType::kVideo) {
+ av_layer_.preventsCapture =
+ protected_video_type_ != gfx::ProtectedVideoType::kClear;
+ }
} else {
switch (type_) {
case CALayerType::kHDRCopier:
Regression Test / PoC
diff --git a/ui/accelerated_widget_mac/ca_layer_tree_unittest_mac.mm b/ui/accelerated_widget_mac/ca_layer_tree_unittest_mac.mm
index 9bbb1102..e930c2028 100644
--- a/ui/accelerated_widget_mac/ca_layer_tree_unittest_mac.mm
+++ b/ui/accelerated_widget_mac/ca_layer_tree_unittest_mac.mm
@@ -52,6 +52,8 @@
gfx::ScopedIOSurface io_surface;
gfx::ColorSpace color_space;
base::apple::ScopedCFTypeRef<CVPixelBufferRef> cv_pixel_buffer;
+ gfx::ProtectedVideoType protected_video_type =
+ gfx::ProtectedVideoType::kClear;
bool allow_av_layers = true;
bool allow_solid_color_layers = true;
@@ -79,7 +81,7 @@
properties->transform, io_surface, io_surface_color_space,
properties->contents_rect, properties->rect, properties->background_color,
properties->edge_aa_mask, properties->opacity, properties->filter,
- gfx::HDRMetadata(), gfx::ProtectedVideoType::kClear, false));
+ gfx::HDRMetadata(), properties->protected_video_type, false));
}
void UpdateCALayerTree(std::unique_ptr<ui::CARendererLayerTree>& ca_layer_tree,
@@ -925,6 +927,61 @@
}
}
+// Ensure that preventsCapture is updated when recycling
+// AVSampleBufferDisplayLayer. Regression test for crbug.com/505192638.
+TEST_F(CALayerTreeTest, AVLayerPreventsCapture) {
+ base::test::ScopedFeatureList features;
+ features.InitWithFeatures({ui::kFullscreenLowPowerBackdropMac}, {});
+
+ CALayerProperties properties;
+ properties.io_surface =
+ gfx::CreateIOSurface(gfx::Size(256, 256), viz::MultiPlaneFormat::kNV12);
+
+ std::unique_ptr<ui::CARendererLayerTree> ca_layer_tree;
+ AVSampleBufferDisplayLayer* av_layer_old = nil;
+ AVSampleBufferDisplayLayer* av_layer_new = nil;
+
+ // Initially, video is clear, so preventsCapture should be NO.
+ {
+ properties.protected_video_type = gfx::ProtectedVideoType::kClear;
+ UpdateCALayerTree(ca_layer_tree, &properties, superlayer_);
+ CALayer* content_layer = GetOnlyContentLayer();
+ EXPECT_TRUE([content_layer
+ isKindOfClass:NSClassFromString(@"AVSampleBufferDisplayLayer")]);
+ av_layer_new = (AVSampleBufferDisplayLayer*)content_layer;
+ EXPECT_FALSE(av_layer_new.preventsCapture);
+ }
+ av_layer_old = av_layer_new;
+
+ // Change to protected video. The layer should be recycled and preventsCapture
+ // should be updated to YES.
+ {
+ properties.protected_video_type =
+ gfx::ProtectedVideoType::kHardwareProtected;
+ UpdateCALayerTree(ca_layer_tree, &properties, superlayer_);
+ CALayer* content_layer = GetOnlyContentLayer();
+ EXPECT_TRUE([content_layer
+ isKindOfClass:NSClassFromString(@"AVSampleBufferDisplayLayer")]);
+ av_layer_new = (AVSampleBufferDisplayLayer*)content_layer;
+ EXPECT_EQ(av_layer_new, av_layer_old);
+ EXPECT_TRUE(av_layer_new.preventsCapture);
+ }
+ av_layer_old = av_layer_new;
+
+ // Change back to clear video. The layer should be recycled and
+ // preventsCapture should be updated to NO.
+ {
+ properties.protected_video_type = gfx::ProtectedVideoType::kClear;
+ UpdateCALayerTree(ca_layer_tree, &properties, superlayer_);
+ CALayer* content_layer = GetOnlyContentLayer();
+ EXPECT_TRUE([content_layer
+ isKindOfClass:NSClassFromString(@"AVSampleBufferDisplayLayer")]);
+ av_layer_new = (AVSampleBufferDisplayLayer*)content_layer;
+ EXPECT_EQ(av_layer_new, av_layer_old);
+ EXPECT_FALSE(av_layer_new.preventsCapture);
+ }
+}
+
// Test fullscreen low power detection.
TEST_F(CALayerTreeTest, FullscreenLowPower) {
base::test::ScopedFeatureList features;
Original Bug Report
Potential DRM bypass on macOS via recycled CALayer's stale preventsCapture property
Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A potential DRM bypass exists in the macOS compositor where hardware-protected video can be screen-recorded. This occurs because the preventsCapture property of an AVSampleBufferDisplayLayer is not updated when a clear video layer is recycled to display protected content. An attacker can exploit this by smoothly transitioning a video from a clear stream to a DRM-protected stream within the same page layout.
Affected files:
ui/accelerated_widget_mac/ca_renderer_layer_tree.mm
Estimated timestamp from git blame: 2024-12-06
Overview
A potential DRM bypass vulnerability exists in the macOS compositor (ui/accelerated_widget_mac/ca_renderer_layer_tree.mm). When a webpage transitions from playing clear video to hardware-protected video, the GPU process may recycle the existing CoreAnimation layer (AVSampleBufferDisplayLayer) to save performance. However, it fails to update the layer’s preventsCapture property during this reuse, leaving the protected content vulnerable to screen recording tools.
Root Cause Analysis
In CARendererLayerTree::ContentLayer::CommitToCA, the code checks if an old_layer_ of the same type_ (in this case, CALayerType::kVideo) is available for reuse. If it is, the code swaps the underlying ca_layer_ and av_layer_:
// ui/accelerated_widget_mac/ca_renderer_layer_tree.mm
if (old_layer_ && old_layer_->type_ == type_) {
DCHECK(old_layer_->ca_layer_);
std::swap(ca_layer_, old_layer_->ca_layer_);
std::swap(av_layer_, old_layer_->av_layer_);
// ... visual properties are updated, but preventsCapture is untouched ...
} else {
switch (type_) {
// ...
case CALayerType::kVideo:
av_layer_ = [[AVSampleBufferDisplayLayer alloc] init];
// ...
if (protected_video_type_ != gfx::ProtectedVideoType::kClear) {
av_layer_.preventsCapture = true;
}
break;
Notice that av_layer_.preventsCapture is only set to true when a new layer is allocated. During layer recycling (the if block), preventsCapture is completely ignored.
Because the fallback layer matching algorithm (CALayerFallBack) matches layers based on their relative structural order in the tree, a new kHardwareProtected video frame can successfully match and reuse an old kClear video frame if the DOM structure hasn’t changed. The recycled layer retains its original preventsCapture = NO state, completely bypassing macOS screen capture restrictions for the protected frame.
Potential Attacker Steps
Note: These are suggested steps based on static analysis, as our automated tooling agent does not have the ability to run code or execute a live proof-of-concept.
- An attacker hosts a webpage with a
<video>element playing a clear, unprotected YUV video stream. - The macOS GPU process creates a new
AVSampleBufferDisplayLayerfor this video. Because the content is clear,preventsCapturedefaults tofalse. - The attacker uses JavaScript to dynamically switch the video source to a DRM-protected stream (e.g., using Widevine/EME) without altering the layout or layer hierarchy of the page.
- The GPU process receives the new hardware-protected
DrawQuad. - The compositor’s fallback matching algorithm pairs the new protected video layer with the old clear video layer, recycling the
AVSampleBufferDisplayLayer. - The protected video frames are enqueued into the recycled layer. Since
preventsCapturewas never updated totrue, a user or malicious application can successfully screen-record the DRM-protected video.
Suggested Fix
Update the preventsCapture property dynamically during the layer recycling path to ensure it correctly reflects the protected_video_type_ of the current frame.
if (old_layer_ && old_layer_->type_ == type_) {
DCHECK(old_layer_->ca_layer_);
std::swap(ca_layer_, old_layer_->ca_layer_);
std::swap(av_layer_, old_layer_->av_layer_);
if (type_ == CALayerType::kVideo) {
bool should_prevent_capture = (protected_video_type_ != gfx::ProtectedVideoType::kClear);
if (av_layer_.preventsCapture != should_prevent_capture) {
av_layer_.preventsCapture = should_prevent_capture;
}
}
// ...
Evaluated with Chrome root at commit: 4a3e9db74111a3c6c4b3acfd70050a05077cf27a
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. And please feel free to reach out to me directly if you have concerns or feedback on the project.