Low chrome Memory Corruption 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in PerformanceAPIs
DescriptionMemory corruption in PerformanceAPIs
ComponentPerformanceAPIs
Sub-componentLCP
Bug ClassMemory Corruption
Tracker502288792
Fix commitdf73937d5c69 (chromium/src) +210/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
effective_visual_size_result_
third_party/blink/renderer/core/paint/timing/paint_timing_record.cc
modified
CrossOriginNullImageResourceInfo
third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc
modified
LargestContentfulPaintCalculatorTest
third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc
modified
TEST_F
third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/loader/resource/video_timing.h
  • third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.cc
  • third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc
  • third_party/blink/renderer/core/paint/timing/paint_timing_record.h
  • third_party/blink/renderer/platform/loader/fetch/media_timing.h
  • third_party/blink/renderer/core/paint/timing/paint_timing_record.cc
  • third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc
  • third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h
  • third_party/blink/renderer/core/html/media/html_video_element.cc
  • third_party/blink/renderer/core/loader/resource/image_resource_content.h
From df73937d5c69d60dd6f38493805deee2786aaede Mon Sep 17 00:00:00 2001
From: Scott Haseley <shaseley@chromium.org>
Date: Wed, 23 Sep 2026 08:39:21 -0700
Subject: [PATCH] LCP: Bypass entropy check for images that don't pass the CORS check

This CL gates the LCP/ICP entropy check on whether image and video
resources are CORS-same-origin to prevent leaking size information. Note
that Resource Timing currently gates body size on TAO, but is moving
towards using CORS (ResourceTimingUseCORSForBodySizes).

Bug: 502288792
Change-Id: Ie35d0fb5833993133c8445772104506465db3b7d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8423520
Commit-Queue: Scott Haseley <shaseley@chromium.org>
Reviewed-by: Michal Mocny <mmocny@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1703469}
---
diff --git a/third_party/blink/renderer/core/html/media/html_video_element.cc b/third_party/blink/renderer/core/html/media/html_video_element.cc
index f811ed644..07d37b8 100644
--- a/third_party/blink/renderer/core/html/media/html_video_element.cc
+++ b/third_party/blink/renderer/core/html/media/html_video_element.cc
@@ -803,6 +803,7 @@
     video_timing->SetIsSufficientContentLoadedForPaint();
     video_timing->SetUrl(currentSrc());
     video_timing->SetContentSizeForEntropy(bytes_to_first_frame);
+    video_timing->SetIsCorsSameOrigin(!WouldTaintOrigin());
 
     PaintTimingDetector::NotifyFirstVideoFrame(
         *layout_object, videoVisibleSize(), *video_timing,
diff --git a/third_party/blink/renderer/core/loader/resource/image_resource_content.h b/third_party/blink/renderer/core/loader/resource/image_resource_content.h
index 461f45c0..714db22 100644
--- a/third_party/blink/renderer/core/loader/resource/image_resource_content.h
+++ b/third_party/blink/renderer/core/loader/resource/image_resource_content.h
@@ -149,7 +149,7 @@
   base::TimeTicks LoadStart() const override;
   base::TimeTicks LoadEnd() const override;
   AtomicString MediaType() const override;
-  bool IsCorsSameOrigin() const;
+  bool IsCorsSameOrigin() const override;
   const ResourceResponse& GetResponse() const;
   std::optional<ResourceError> GetResourceError() const;
 
diff --git a/third_party/blink/renderer/core/loader/resource/video_timing.h b/third_party/blink/renderer/core/loader/resource/video_timing.h
index 95895e0..61b330b 100644
--- a/third_party/blink/renderer/core/loader/resource/video_timing.h
+++ b/third_party/blink/renderer/core/loader/resource/video_timing.h
@@ -43,6 +43,11 @@
     return first_frame_time_;
   }
 
+  void SetIsCorsSameOrigin(bool is_cors_same_origin) {
+    is_cors_same_origin_ = is_cors_same_origin;
+  }
+  bool IsCorsSameOrigin() const override { return is_cors_same_origin_; }
+
   uint64_t ContentSizeForEntropy() const override {
     // We don't do anything clever here to try to isolate the encoded size of
     // just the first frame; if we're calling this, then at least enough data
@@ -77,6 +82,7 @@
  private:
   KURL url_;
   bool is_loaded_ = false;
+  bool is_cors_same_origin_ = false;
   base::TimeTicks first_frame_time_;
   size_t content_size_ = 0;
 };
diff --git a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.cc b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.cc
index 203987a..d1bdb68 100644
--- a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.cc
+++ b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.cc
@@ -29,6 +29,8 @@
 
 BASE_FEATURE(kSoftNavigationTraceEvents, base::FEATURE_ENABLED_BY_DEFAULT);
 
+BASE_FEATURE(kLcpEntropyGatedOnCors, base::FEATURE_ENABLED_BY_DEFAULT);
+
 namespace {
 
 constexpr const char kTraceCategories[] = "loading,rail,devtools.timeline";
@@ -655,6 +657,13 @@
     return false;
   }
 
+  // Only apply the minimum entropy check to CORS-same-origin resources to
+  // prevent cross-origin size leaks (crbug.com/502288792).
+  if (base::FeatureList::IsEnabled(kLcpEntropyGatedOnCors) &&
+      !record.IsCorsSameOrigin()) {
+    return true;
+  }
+
   // The first video frame often fails to meet the entropy check, e.g. due to
   // being a solid color or blank frame. This is problematic for ICP, so we
   // ignore the entropy check in that case.
diff --git a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h
index 8a3ad37..6461487 100644
--- a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h
+++ b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h
@@ -30,6 +30,10 @@
 // Kill switch for Soft Nav/LCP trace events.
 BASE_DECLARE_FEATURE(kSoftNavigationTraceEvents);
 
+// Kill switch for filtering entropy check on CORS. When enabled, the entropy
+// check only applies to images that are CORS-same-origin.
+CORE_EXPORT BASE_DECLARE_FEATURE(kLcpEntropyGatedOnCors);
+
 // `LargestContentfulPaintCalculator` is responsible for tracking the largest
 // image and the largest text paints and notifying its `Delegate` whenever a new
 // PerformanceEntry entry should be dispatched, as well as maintaintaining the
@@ -143,10 +147,11 @@
   void MaybeFlushCandidates();
 
   // Returns true iff the `ImageRecord` is an eligible LCP candidate, which is
-  // true as long as the the image is not covering the viewport and the minimum
-  // entropy requirement has been met. `ImageRecord` is required to have a
-  // non-zero size. Note that this differs from `ShouldTrackForPaintTiming()`
-  // which also takes into account the size of the current LCP candidate.
+  // true as long as the image is not covering the viewport and, if the image
+  // is CORS-same-origin, the minimum entropy requirement has been met.
+  // `ImageRecord` is required to have a non-zero size. Note that this differs
+  // from `ShouldTrackForPaintTiming()` which also takes into account the size
+  // of the current LCP candidate.
   bool IsEligibleForLcp(const ImageRecord&) const;
 
   // Returns true iff the `TextRecord` is an eligible LCP candidate, which is
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing_record.cc b/third_party/blink/renderer/core/paint/timing/paint_timing_record.cc
index 04a031a..a5e8d3c 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing_record.cc
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing_record.cc
@@ -72,6 +72,8 @@
     : PaintTimingRecord(node, frame_visual_rect, root_visual_rect),
       media_timing_(new_media_timing),
       hash_(hash),
+      is_cors_same_origin_(new_media_timing &&
+                           new_media_timing->IsCorsSameOrigin()),
       effective_visual_size_result_(effective_visual_size_result) {
   CHECK_GT(EffectiveVisualSize(), 0u);
 }
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing_record.h b/third_party/blink/renderer/core/paint/timing/paint_timing_record.h
index c230555..e95f3c01 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing_record.h
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing_record.h
@@ -210,12 +210,15 @@
     return effective_visual_size_result_;
   }
 
+  bool IsCorsSameOrigin() const { return is_cors_same_origin_; }
+
  private:
   const WeakMember<const MediaTiming> media_timing_;
   const MediaRecordIdHash hash_;
   base::TimeTicks load_time_;
   base::TimeTicks first_animated_frame_time_;
   bool is_sufficiently_loaded_for_reporting_ = false;
+  const bool is_cors_same_origin_;
   const EffectiveVisualSizeResult effective_visual_size_result_;
 };
 
diff --git a/third_party/blink/renderer/platform/loader/fetch/media_timing.h b/third_party/blink/renderer/platform/loader/fetch/media_timing.h
index 9628aa1..4ebc437c 100644
--- a/third_party/blink/renderer/platform/loader/fetch/media_timing.h
+++ b/third_party/blink/renderer/platform/loader/fetch/media_timing.h
@@ -28,6 +28,10 @@
   // for animated media.)
   virtual bool IsSufficientContentLoadedForPaint() const = 0;
 
+  // True if the resource is CORS-same-origin (i.e. its response is not
+  // opaque and does not taint the origin).
+  virtual bool IsCorsSameOrigin() const = 0;
+
   // Returns the number of bytes of data used to represent the image (or to
   // represent enough of the content that it can be displayed on screen.) This
   // is used to approximate the entropy of the image so that very-low-entropy
diff --git a/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc b/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc
index bbe6a4a..a39e3e8 100644
--- a/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc
+++ b/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc
@@ -1479,6 +1479,7 @@
 
   // Ensure the image has enough entropy to be considered for LCP.
   uint64_t ContentSizeForEntropy() const override { return 100000; }
+  bool IsCorsSameOrigin() const override { return true; }
 
   void Trace(Visitor*) const override {}
 
diff --git a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc
index 087178e8..f23b637 100644
--- a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc
+++ b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc
@@ -4,12 +4,16 @@
 
 #include "third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h"
 
+#include <optional>
+
 #include "base/test/scoped_feature_list.h"
 #include "base/test/tracing/trace_event_analyzer.h"
 #include "base/test/tracing/trace_test_utils.h"
+#include "base/time/time.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
 #include "third_party/blink/renderer/core/html/html_image_element.h"
+#include "third_party/blink/renderer/core/loader/resource/image_resource_info.h"
 #include "third_party/blink/renderer/core/paint/paint_flags.h"
 #include "third_party/blink/renderer/core/paint/timing/largest_contentful_paint_manager.h"
 #include "third_party/blink/renderer/core/paint/timing/mock_paint_timing_callback_manager.h"
@@ -20,15 +24,68 @@
 #include "third_party/blink/renderer/core/timing/dom_window_performance.h"
 #include "third_party/blink/renderer/platform/graphics/paint/paint_record_builder.h"
 #include "third_party/blink/renderer/platform/graphics/unaccelerated_static_bitmap_image.h"
+#include "third_party/blink/renderer/platform/loader/fetch/fetch_parameters.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
 #include "third_party/skia/include/core/SkImage.h"
 #include "third_party/skia/include/core/SkSurface.h"
 
 namespace blink {
 
 namespace {
-constexpr const char kTraceCategories[] = "loading,rail,devtools.timeline";
 
+constexpr const char kTraceCategories[] = "loading,rail,devtools.timeline";
 constexpr const char kLCPCandidate[] = "largestContentfulPaint::Candidate";
+
+class CrossOriginNullImageResourceInfo final
+    : public GarbageCollected<CrossOriginNullImageResourceInfo>,
+      public ImageResourceInfo {
+ public:
+  CrossOriginNullImageResourceInfo() = default;
+
+  void Trace(Visitor* visitor) const override {
+    ImageResourceInfo::Trace(visitor);
+  }
+
+ private:
+  const KURL& Url() const override { return url_; }
+  bool IsAutomaticUpgrade() const override { return false; }
+  base::TimeTicks LoadResponseEnd() const override { return base::TimeTicks(); }
+  base::TimeTicks LoadStart() const override { return base::TimeTicks(); }
+  base::TimeTicks LoadEnd() const override { return base::TimeTicks(); }
+  base::TimeTicks DiscoveryTime() const override { return base::TimeTicks(); }
+  const ResourceResponse& GetResponse() const override { return response_; }
+  bool IsCacheValidator() const override { return false; }
+  bool IsCorsSameOrigin(
+      DoesCurrentFrameHaveSingleSecurityOrigin) const override {
+    return false;
+  }
+  std::optional<ResourceError> GetResourceError() const override {
+    return std::nullopt;
+  }
+  void SetDecodedSize(size_t) override {}
+  void WillAddClientOrObserver() override {}
+  void DidRemoveClientOrObserver() override {}
+  void EmulateLoadStartedForInspector(
+      ResourceFetcher*,
+      const AtomicString& initiator_name) override {}
+  void LoadDeferredImage(ResourceFetcher* fetcher) override {}
+  const std::optional<AdProvenance>& GetAdProvenance() const override {
+    return ad_provenance_;
+  }
+  const HashSet<String>* GetUnsupportedImageMimeTypes() const override {
+    return nullptr;
+  }
+  std::optional<WebURLRequest::Priority> RequestPriority() const override {
+    return std::nullopt;
+  }
+
+  const KURL url_;
+  const ResourceResponse response_;
+  std::optional<AdProvenance> ad_provenance_;
+};
+
 }  // namespace
 
 class LargestContentfulPaintCalculatorTest : public PaintTimingTestBase {
@@ -252,7 +309,6 @@
 }
 
 TEST_F(LargestContentfulPaintCalculatorTest, SingleImageExcludedForEntropy) {
-  base::test::ScopedFeatureList scoped_features;
   SetMainFrameBodyContent(R"HTML(
     <!DOCTYPE html>
     <img id='target'/>
@@ -268,7 +324,6 @@
 }
 
 TEST_F(LargestContentfulPaintCalculatorTest, LargerImageExcludedForEntropy) {
-  base::test::ScopedFeatureList scoped_features;
   SetMainFrameBodyContent(R"HTML(
     <!DOCTYPE html>
     <img id='small'/>
@@ -287,15 +342,126 @@
 }
 
 TEST_F(LargestContentfulPaintCalculatorTest,
-       LowEntropyImageNotExcludedAtLowerThreshold) {
+       SingleImageExcludedForEntropyWhenCorsFails) {
   base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitWithFeatures({}, {kLcpEntropyGatedOnCors});
+
+  SetMainFrameBodyContent(R"HTML(
+    <!DOCTYPE html>
+    <img id='target'/>
+  )HTML");
+  // 60 bytes at 100x150 has an entropy of 0.032 bpp (below 0.05 bpp). Because
+  // the image is CORS-same-origin, the entropy check filters out the image.
+  ImageResourceContent* content = SetImageContent("target", 100, 150, 60);
+  content->SetImageResourceInfo(
+      MakeGarbageCollected<CrossOriginNullImageResourceInfo>());
+  SimulateRenderingAndPresentationTime();
+
+  EXPECT_EQ(LargestReportedSize(), 0u);
+  EXPECT_EQ(CandidateCount(), 0u);
+  trace_analyzer::Stop();
+}
+
+TEST_F(LargestContentfulPaintCalculatorTest,
+       SingleImageNotExcludedForEntropyWhenCorsFails) {
+  base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitWithFeatures({kLcpEntropyGatedOnCors}, {});
+
+  SetMainFrameBodyContent(R"HTML(
+    <!DOCTYPE html>
+    <img id='target'/>
+  )HTML");
+  // 60 bytes at 100x150 has an entropy of 0.032 bpp (below 0.05 bpp),
+  // but because the image is not CORS-same-origin, the entropy check is
+  // bypassed.
+  ImageResourceContent* content = SetImageContent("target", 100, 150, 60);
+  content->SetImageResourceInfo(
+      MakeGarbageCollected<CrossOriginNullImageResourceInfo>());
+  SimulateRenderingAndPresentationTime();
+
+  EXPECT_EQ(LargestReportedSize(), 15000u);
+  EXPECT_FLOAT_EQ(LargestContentfulPaintCandidateImageBPP(), 0.032f);
+  EXPECT_EQ(CandidateCount(), 1u);
+  trace_analyzer::Stop();
+}
+
+TEST_F(LargestContentfulPaintCalculatorTest,
+       LargerImageNotExcludedForEntropyWhenCorsFails) {
+  base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitWithFeatures({kLcpEntropyGatedOnCors}, {});
+
+  SetMainFrameBodyContent(R"HTML(
+    <!DOCTYPE html>
+    <img id='small'/>
+    <img id='large'/>
+  )HTML");
+  SetImageContent("small", 3, 3, 18);
+  ImageResourceContent* large_content = SetImageContent("large", 100, 200, 80);
+  large_content->SetImageResourceInfo(
+      MakeGarbageCollected<CrossOriginNullImageResourceInfo>());
+  SimulateRenderingAndPresentationTime();
+
+  EXPECT_EQ(LargestReportedSize(), 20000u);
+  EXPECT_FLOAT_EQ(LargestContentfulPaintCandidateImageBPP(), 0.032f);
+  EXPECT_EQ(CandidateCount(), 1u);
+  trace_analyzer::Stop();
+}
+
+TEST_F(LargestContentfulPaintCalculatorTest, DataUrlImageExcludedForEntropy) {
+  base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitWithFeatures({kLcpEntropyGatedOnCors}, {});
+
+  SetMainFrameBodyContent(R"HTML(
+    <!DOCTYPE html>
+    <img id='target' width=100 height=150/>
+  )HTML");
+  KURL data_url(
+      "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' "
+      "height='150'></svg>");
+  ResourceRequest request(data_url);
+  FetchParameters fetch_params =
+      FetchParameters::CreateForTest(std::move(request));
+  ImageResourceContent* content =
+      ImageResourceContent::Fetch(fetch_params, GetDocument().Fetcher());
+  ASSERT_TRUE(content);
+  EXPECT_TRUE(content->IsCorsSameOrigin());
+
+  To<HTMLImageElement>(GetElementById("target"))->SetImageForTest(content);
+  SimulateRenderingAndPresentationTime();
+
+  EXPECT_EQ(LargestReportedSize(), 0u);
+  EXPECT_EQ(CandidateCount(), 0u);
+  trace_analyzer::Stop();
+}
+
+TEST_F(LargestContentfulPaintCalculatorTest,
+       ViewportCoveringImageExcludedEvenWhenCorsFails) {
+  base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitWithFeatures({kLcpEntropyGatedOnCors}, {});
+
+  SetMainFrameBodyContent(R"HTML(
+    <style>body {margin: 0px;}</style>
+    <img id='target'/>
+  )HTML");
+  ImageResourceContent* content = SetImageContent("target", 3000, 3000, 60);
+  content->SetImageResourceInfo(
+      MakeGarbageCollected<CrossOriginNullImageResourceInfo>());
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc b/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc
index bbe6a4a..a39e3e8 100644
--- a/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc
+++ b/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc
@@ -1479,6 +1479,7 @@
 
   // Ensure the image has enough entropy to be considered for LCP.
   uint64_t ContentSizeForEntropy() const override { return 100000; }
+  bool IsCorsSameOrigin() const override { return true; }
 
   void Trace(Visitor*) const override {}
diff --git a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc
index 087178e8..f23b637 100644
--- a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc
+++ b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc
@@ -4,12 +4,16 @@
 
 #include "third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h"
 
+#include <optional>
+
 #include "base/test/scoped_feature_list.h"
 #include "base/test/tracing/trace_event_analyzer.h"
 #include "base/test/tracing/trace_test_utils.h"
+#include "base/time/time.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
 #include "third_party/blink/renderer/core/html/html_image_element.h"
+#include "third_party/blink/renderer/core/loader/resource/image_resource_info.h"
 #include "third_party/blink/renderer/core/paint/paint_flags.h"
 #include "third_party/blink/renderer/core/paint/timing/largest_contentful_paint_manager.h"
 #include "third_party/blink/renderer/core/paint/timing/mock_paint_timing_callback_manager.h"
@@ -20,15 +24,68 @@
 #include "third_party/blink/renderer/core/timing/dom_window_performance.h"
 #include "third_party/blink/renderer/platform/graphics/paint/paint_record_builder.h"
 #include "third_party/blink/renderer/platform/graphics/unaccelerated_static_bitmap_image.h"
+#include "third_party/blink/renderer/platform/loader/fetch/fetch_parameters.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
 #include "third_party/skia/include/core/SkImage.h"
 #include "third_party/skia/include/core/SkSurface.h"
 
 namespace blink {
 
 namespace {
-constexpr const char kTraceCategories[] = "loading,rail,devtools.timeline";
 
+constexpr const char kTraceCategories[] = "loading,rail,devtools.timeline";
 constexpr const char kLCPCandidate[] = "largestContentfulPaint::Candidate";
+
+class CrossOriginNullImageResourceInfo final
+    : public GarbageCollected<CrossOriginNullImageResourceInfo>,
+      public ImageResourceInfo {
+ public:
+  CrossOriginNullImageResourceInfo() = default;
+
+  void Trace(Visitor* visitor) const override {
+    ImageResourceInfo::Trace(visitor);
+  }
+
+ private:
+  const KURL& Url() const override { return url_; }
+  bool IsAutomaticUpgrade() const override { return false; }
+  base::TimeTicks LoadResponseEnd() const override { return base::TimeTicks(); }
+  base::TimeTicks LoadStart() const override { return base::TimeTicks(); }
+  base::TimeTicks LoadEnd() const override { return base::TimeTicks(); }
+  base::TimeTicks DiscoveryTime() const override { return base::TimeTicks(); }
+  const ResourceResponse& GetResponse() const override { return response_; }
+  bool IsCacheValidator() const override { return false; }
+  bool IsCorsSameOrigin(
+      DoesCurrentFrameHaveSingleSecurityOrigin) const override {
+    return false;
+  }
+  std::optional<ResourceError> GetResourceError() const override {
+    return std::nullopt;
+  }
+  void SetDecodedSize(size_t) override {}
+  void WillAddClientOrObserver() override {}
+  void DidRemoveClientOrObserver() override {}
+  void EmulateLoadStartedForInspector(
+      ResourceFetcher*,
+      const AtomicString& initiator_name) override {}
+  void LoadDeferredImage(ResourceFetcher* fetcher) override {}
+  const std::optional<AdProvenance>& GetAdProvenance() const override {
+    return ad_provenance_;
+  }
+  const HashSet<String>* GetUnsupportedImageMimeTypes() const override {
+    return nullptr;
+  }
+  std::optional<WebURLRequest::Priority> RequestPriority() const override {
+    return std::nullopt;
+  }
+
+  const KURL url_;
+  const ResourceResponse response_;
+  std::optional<AdProvenance> ad_provenance_;
+};
+
 }  // namespace
 
 class LargestContentfulPaintCalculatorTest : public PaintTimingTestBase {
@@ -252,7 +309,6 @@
 }
 
 TEST_F(LargestContentfulPaintCalculatorTest, SingleImageExcludedForEntropy) {
-  base::test::ScopedFeatureList scoped_features;
   SetMainFrameBodyContent(R"HTML(
     <!DOCTYPE html>
     <img id='target'/>
@@ -268,7 +324,6 @@
 }
 
 TEST_F(LargestContentfulPaintCalculatorTest, LargerImageExcludedForEntropy) {
-  base::test::ScopedFeatureList scoped_features;
   SetMainFrameBodyContent(R"HTML(
     <!DOCTYPE html>
     <img id='small'/>
@@ -287,15 +342,126 @@
 }
 
 TEST_F(LargestContentfulPaintCalculatorTest,
-       LowEntropyImageNotExcludedAtLowerThreshold) {
+       SingleImageExcludedForEntropyWhenCorsFails) {
   base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitWithFeatures({}, {kLcpEntropyGatedOnCors});
+
+  SetMainFrameBodyContent(R"HTML(
+    <!DOCTYPE html>
+    <img id='target'/>
+  )HTML");
+  // 60 bytes at 100x150 has an entropy of 0.032 bpp (below 0.05 bpp). Because
+  // the image is CORS-same-origin, the entropy check filters out the image.
+  ImageResourceContent* content = SetImageContent("target", 100, 150, 60);
+  content->SetImageResourceInfo(
+      MakeGarbageCollected<CrossOriginNullImageResourceInfo>());
+  SimulateRenderingAndPresentationTime();
+
+  EXPECT_EQ(LargestReportedSize(), 0u);
+  EXPECT_EQ(CandidateCount(), 0u);
+  trace_analyzer::Stop();
+}
+
+TEST_F(LargestContentfulPaintCalculatorTest,
+       SingleImageNotExcludedForEntropyWhenCorsFails) {
+  base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitWithFeatures({kLcpEntropyGatedOnCors}, {});
+
+  SetMainFrameBodyContent(R"HTML(
+    <!DOCTYPE html>
+    <img id='target'/>
+  )HTML");
+  // 60 bytes at 100x150 has an entropy of 0.032 bpp (below 0.05 bpp),
+  // but because the image is not CORS-same-origin, the entropy check is
+  // bypassed.
+  ImageResourceContent* content = SetImageContent("target", 100, 150, 60);
+  content->SetImageResourceInfo(
+      MakeGarbageCollected<CrossOriginNullImageResourceInfo>());
+  SimulateRenderingAndPresentationTime();
+
+  EXPECT_EQ(LargestReportedSize(), 15000u);
+  EXPECT_FLOAT_EQ(LargestContentfulPaintCandidateImageBPP(), 0.032f);
+  EXPECT_EQ(CandidateCount(), 1u);
+  trace_analyzer::Stop();
+}
+
+TEST_F(LargestContentfulPaintCalculatorTest,
+       LargerImageNotExcludedForEntropyWhenCorsFails) {
+  base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitWithFeatures({kLcpEntropyGatedOnCors}, {});
+
+  SetMainFrameBodyContent(R"HTML(
+    <!DOCTYPE html>
+    <img id='small'/>
+    <img id='large'/>
+  )HTML");
+  SetImageContent("small", 3, 3, 18);
+  ImageResourceContent* large_content = SetImageContent("large", 100, 200, 80);
+  large_content->SetImageResourceInfo(
+      MakeGarbageCollected<CrossOriginNullImageResourceInfo>());
+  SimulateRenderingAndPresentationTime();
+
+  EXPECT_EQ(LargestReportedSize(), 20000u);
+  EXPECT_FLOAT_EQ(LargestContentfulPaintCandidateImageBPP(), 0.032f);
+  EXPECT_EQ(CandidateCount(), 1u);
+  trace_analyzer::Stop();
+}
+
+TEST_F(LargestContentfulPaintCalculatorTest, DataUrlImageExcludedForEntropy) {
+  base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitWithFeatures({kLcpEntropyGatedOnCors}, {});
+
+  SetMainFrameBodyContent(R"HTML(
+    <!DOCTYPE html>
+    <img id='target' width=100 height=150/>
+  )HTML");
+  KURL data_url(
+      "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' "
+      "height='150'></svg>");
+  ResourceRequest request(data_url);
+  FetchParameters fetch_params =
+      FetchParameters::CreateForTest(std::move(request));
+  ImageResourceContent* content =
+      ImageResourceContent::Fetch(fetch_params, GetDocument().Fetcher());
+  ASSERT_TRUE(content);
+  EXPECT_TRUE(content->IsCorsSameOrigin());
+
+  To<HTMLImageElement>(GetElementById("target"))->SetImageForTest(content);
+  SimulateRenderingAndPresentationTime();
+
+  EXPECT_EQ(LargestReportedSize(), 0u);
+  EXPECT_EQ(CandidateCount(), 0u);
+  trace_analyzer::Stop();
+}
+
+TEST_F(LargestContentfulPaintCalculatorTest,
+       ViewportCoveringImageExcludedEvenWhenCorsFails) {
+  base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitWithFeatures({kLcpEntropyGatedOnCors}, {});
+
+  SetMainFrameBodyContent(R"HTML(
+    <style>body {margin: 0px;}</style>
+    <img id='target'/>
+  )HTML");
+  ImageResourceContent* content = SetImageContent("target", 3000, 3000, 60);
+  content->SetImageResourceInfo(
+      MakeGarbageCollected<CrossOriginNullImageResourceInfo>());
+  SimulateRenderingAndPresentationTime();
+
+  EXPECT_EQ(LargestReportedSize(), 0u);
+  EXPECT_EQ(CandidateCount(), 0u);
+  trace_analyzer::Stop();
+}
+
+TEST_F(LargestContentfulPaintCalculatorTest,
+       LowEntropyImageNotExcludedAtLowerThreshold) {
   SetMainFrameBodyContent(R"HTML(
     <!DOCTYPE html>
     <img id='small'/>
     <img id='large'/>
   )HTML");
   // Smaller image has 16 bpp of entropy, enough to be considered for LCP.
-  // Larger image has 0.32 bpp, which is now above the 0.2bpp threshold.
+  // Larger image has 0.32 bpp, which is now above the 0.05 bpp threshold.
   SetImageContent("small", 3, 3, 18);
   SetImageContent("large", 100, 200, 800);
   SimulateRenderingAndPresentationTime();
@@ -312,7 +478,7 @@
     <img id='large' width=100 height=300 />
   )HTML");
   // Smaller image has 16 bpp of entropy, enough to be considered for LCP.
-  // Larger image has 0.32 bpp, which is now above the 0.2bpp threshold.
+  // Larger image has 0.32 bpp, which is now above the 0.05 bpp threshold.
   SetImageContent("small", 3, 3, 18);
   SetImageContent("large", 100, 300, 800, ImageStatus::kPending);
   SimulateRenderingAndPresentationTime();
@@ -333,7 +499,7 @@
     <img id='large' width=100 height=300 />
   )HTML");
   // Smaller image has 16 bpp of entropy, enough to be considered for LCP.
-  // Larger image has 0.32 bpp, which is now above the 0.2bpp threshold.
+  // Larger image has 0.32 bpp, which is now above the 0.05 bpp threshold.
   SetImageContent("small", 3, 3, 18);
   SetImageContent("large", 100, 300, 800, ImageStatus::kPending);
   SimulateRenderingAndPresentationTime();
@@ -372,7 +538,7 @@
     <div><img id='largest' width=150 height=200 /></div>
   )HTML");
   // Smaller image has 16 bpp of entropy, enough to be considered for LCP.
-  // Larger image has 0.32 bpp, which is now above the 0.2bpp threshold.
+  // Larger image has 0.32 bpp, which is now above the 0.05 bpp threshold.
   SetImageContent("small", 3, 3, 18);
   SetImageContent("large", 100, 100, 800, ImageStatus::kPending);
   SetImageContent("largest", 150, 200, 800, ImageStatus::kPending);
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.