Chrome · PerformanceAPIs › LCP
CVE-2026-113056
Memory Corruption in PerformanceAPIs (LCP)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
effective_visual_size_result_third_party/blink/renderer/core/paint/timing/paint_timing_record.cc |
modified | |
CrossOriginNullImageResourceInfothird_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc |
modified | |
LargestContentfulPaintCalculatorTestthird_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/loader/resource/video_timing.hthird_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.ccthird_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.ccthird_party/blink/renderer/core/paint/timing/paint_timing_record.hthird_party/blink/renderer/platform/loader/fetch/media_timing.hthird_party/blink/renderer/core/paint/timing/paint_timing_record.ccthird_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator_test.ccthird_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.hthird_party/blink/renderer/core/html/media/html_video_element.ccthird_party/blink/renderer/core/loader/resource/image_resource_content.h
Patch
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.
References
On This Page