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 <[email protected]> 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 <[email protected]> Reviewed-by: Michal Mocny <[email protected]> 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