Low chrome UAF 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PerformanceAPIs
DescriptionUse after free in PerformanceAPIs
ComponentPerformanceAPIs
Sub-componentPaintTiming
Bug ClassUAF
Tracker497433155
Fix commitc89f1510200f (chromium/src) +264/-19
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
ForEachPaintTimingClient
third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.cc
modified
if
third_party/blink/renderer/core/paint/timing/paint_timing_utils.cc
modified
Document
third_party/blink/renderer/core/paint/timing/paint_timing_utils.h
modified
LocalDOMWindow
third_party/blink/renderer/core/paint/timing/paint_timing_utils.h
modified
LocalFrameView
third_party/blink/renderer/core/paint/timing/paint_timing_utils.h
modified
MediaTiming
third_party/blink/renderer/core/paint/timing/paint_timing_utils.h
modified
TEST_P
third_party/blink/renderer/core/paint/timing/image_element_timing_test.cc
modified
TEST_P
third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc
modified

Files Changed

  • third_party/blink/web_tests/media/video-default-poster-paint-timing.html
  • third_party/blink/renderer/core/paint/timing/paint_timing_utils.h
  • third_party/blink/renderer/core/paint/timing/image_element_timing.cc
  • third_party/blink/renderer/core/paint/timing/paint_timing_test_base.h
  • third_party/blink/renderer/core/paint/timing/image_paint_timing_detector_test.cc
  • third_party/blink/renderer/core/paint/timing/paint_timing_utils.cc
  • third_party/blink/renderer/core/paint/timing/image_element_timing_test.cc
  • third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.cc
From c89f1510200fbae83fe42fef454de7c50991214e Mon Sep 17 00:00:00 2001
From: Scott Haseley <shaseley@chromium.org>
Date: Wed, 23 Sep 2026 18:14:54 -0700
Subject: [PATCH] PaintTiming: Ignore default poster images

Default poster images on WebView are not good LCP candidates since they
come from internal resources, and their URLs can be privacy-sensitive.
This CL filters out these out from Paint Timing so they are not
considered by LCP, ICP, or Element Timing.

Fixed: 497433155
Change-Id: I52f633827abbe8b269f1b074ebe6d5a1819d62f8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8422585
Reviewed-by: Michal Mocny <mmocny@chromium.org>
Commit-Queue: Scott Haseley <shaseley@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1703980}
---
diff --git a/third_party/blink/renderer/core/paint/timing/image_element_timing.cc b/third_party/blink/renderer/core/paint/timing/image_element_timing.cc
index 73efdf1..fd81140 100644
--- a/third_party/blink/renderer/core/paint/timing/image_element_timing.cc
+++ b/third_party/blink/renderer/core/paint/timing/image_element_timing.cc
@@ -22,6 +22,7 @@
 #include "third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.h"
 #include "third_party/blink/renderer/core/paint/timing/paint_timing.h"
 #include "third_party/blink/renderer/core/paint/timing/paint_timing_detector.h"
+#include "third_party/blink/renderer/core/paint/timing/paint_timing_utils.h"
 #include "third_party/blink/renderer/core/style/style_fetched_image.h"
 #include "third_party/blink/renderer/core/svg/svg_image_element.h"
 #include "third_party/blink/renderer/core/timing/dom_window_performance.h"
@@ -61,7 +62,6 @@
 }
 }  // namespace internal
 
-
 AtomicString ImagePaintString() {
   DEFINE_STATIC_LOCAL(const AtomicString, kImagePaint, ("image-paint"));
   return kImagePaint;
@@ -82,6 +82,10 @@
     const MediaTiming& media_timing,
     const PropertyTreeStateOrAlias& current_paint_chunk_properties,
     const gfx::Rect& image_border) {
+  if (paint_timing::ShouldIgnoreImageContentForPaintTiming(layout_object,
+                                                           &media_timing)) {
+    return;
+  }
   Node* node = layout_object.GetNode();
   bool is_image_or_video_element = IsA<HTMLImageElement>(node) ||
                                    IsA<HTMLVideoElement>(node) ||
@@ -258,6 +262,10 @@
 
 void ImageElementTiming::NotifyImageRemoved(const LayoutObject& layout_object,
                                             const ImageResourceContent* image) {
+  if (paint_timing::ShouldIgnoreImageContentForPaintTiming(layout_object,
+                                                           image)) {
+    return;
+  }
   recorded_images_.erase(MediaRecordId::GenerateHash(&layout_object, image));
 }
 
diff --git a/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.cc b/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.cc
index 64f506cf..a9794531 100644
--- a/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.cc
+++ b/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.cc
@@ -97,6 +97,10 @@
 void ImagePaintTimingDetector::NotifyImageRemoved(
     const LayoutObject& object,
     const MediaTiming* media_timing) {
+  if (paint_timing::ShouldIgnoreImageContentForPaintTiming(object,
+                                                           media_timing)) {
+    return;
+  }
   RemoveRecord(MediaRecordId::GenerateHash(&object, media_timing));
   ForEachPaintTimingClient([&](PaintTimingClient* client) {
     client->OnImageRemoved(object, media_timing);
@@ -129,6 +133,11 @@
     return;
   }
 
+  if (paint_timing::ShouldIgnoreImageContentForPaintTiming(object,
+                                                           &media_timing)) {
+    return;
+  }
+
   // Before the image resource starts loading, <img> has no size info. We wait
   // until the size is known.
   if (image_border.IsEmpty()) {
@@ -295,6 +304,10 @@
 void ImagePaintTimingDetector::NotifyImageFinished(
     const LayoutObject& object,
     const MediaTiming* media_timing) {
+  if (paint_timing::ShouldIgnoreImageContentForPaintTiming(object,
+                                                           media_timing)) {
+    return;
+  }
   auto hash(MediaRecordId::GenerateHash(&object, media_timing));
   const auto& insertion_result =
       image_finished_times_.insert(hash, base::TimeTicks());
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing_test_base.h b/third_party/blink/renderer/core/paint/timing/paint_timing_test_base.h
index a518640..513ff1d 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing_test_base.h
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing_test_base.h
@@ -25,6 +25,23 @@
 
 namespace blink {
 
+#define SIMPLE_IMAGE       \
+  "data:image/gif;base64," \
+  "R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+
+#define LARGE_IMAGE                                                            \
+  "data:image/gif;base64,"                                                     \
+  "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSF" \
+  "lzAAAN1wAADdcBQiibeAAAAb5JREFUOMulkr1KA0EQgGdvTwwnYmER0gQsrFKmSy+pLESw9Qm0" \
+  "F/ICNnba+h6iEOuAEWslKJKTOyJJvIT72d1xZuOFC0giOLA77O7Mt/PnNptN+I+49Xr9GhH3f3" \
+  "mb0v1ht9vtLAUYYw5ItkgDL3KyD8PhcLvdbl/WarXT3DjLMnAcR/f7/YfxeKwtgC5RKQVhGILW" \
+  "eg4hQ6hUKjWyucmhLFEUuWR3QYBWAZABQ9i5CCmXy16pVALP80BKaaG+70MQBLvzFMjRKKXh8j" \
+  "6FSYKF7ITdEWLa4/ktokN74wiqjSMpnVcbQZqmEJHz+ckeCPFjWKwULpyspAqhdXVXdcnZcPjs" \
+  "Ign+2BsVA8jVYuWlgJ3yBj0icgq2uoK+lg4t+ZvLomSKamSQ4AI5BcMADtMhyNoSgNIISUaFNt" \
+  "wlazcDcBc4gjjVwCWid2usCWroYEhnaqbzFJLUzAHIXRDChXCcQP8zhkSZ5eNLgHAUzwDcRu4C" \
+  "oIRn/wsGUQIIy4Vr9TH6SYFCNzw4nALn5627K4vIttOUOwfa5YnrDYzt/9OLv9I5l8kk5hZ3XL" \
+  "O20b7tbR7zHLy/BX8G0IeBEM7ZN1NGIaFUaKLgAAAAAElFTkSuQmCC"
+
 // Mock platform support to provide localized string resources (such as button
 // labels) during tests so shadow DOM form controls (e.g. <input type="file">)
 // are rendered with non-zero dimensions and recorded by paint timing detectors.
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing_utils.cc b/third_party/blink/renderer/core/paint/timing/paint_timing_utils.cc
index 598d223..ca9e268 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing_utils.cc
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing_utils.cc
@@ -4,16 +4,25 @@
 
 #include "third_party/blink/renderer/core/paint/timing/paint_timing_utils.h"
 
+#include "base/feature_list.h"
 #include "cc/trees/layer_tree_host.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/media/html_video_element.h"
+#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
 #include "third_party/blink/renderer/core/loader/document_loader.h"
+#include "third_party/blink/renderer/platform/loader/fetch/media_timing.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
 
 namespace blink::paint_timing {
 
 namespace {
 
+BASE_FEATURE(kIgnoreDefaultVideoPosterForPaintTiming,
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 cc::HeadsUpDisplayLayer* GetHUDLayer(LocalFrameView* frame_view) {
   if (!frame_view) {
     return nullptr;
@@ -53,6 +62,28 @@
   return nullptr;
 }
 
+bool ShouldIgnoreImageContentForPaintTiming(const LayoutObject& object,
+                                            const MediaTiming* media_timing) {
+  if (!base::FeatureList::IsEnabled(kIgnoreDefaultVideoPosterForPaintTiming)) {
+    return false;
+  }
+  if (!media_timing || media_timing->IsVideo() ||
+      !IsA<HTMLVideoElement>(object.GetNode())) {
+    return false;
+  }
+  const Settings* settings = object.GetDocument().GetSettings();
+  if (!settings) {
+    return false;
+  }
+  StringView default_poster =
+      StripLeadingAndTrailingHtmlSpaces(settings->GetDefaultVideoPosterURL());
+  if (default_poster.empty()) {
+    return false;
+  }
+  return object.GetDocument().CompleteURL(default_poster) ==
+         media_timing->Url();
+}
+
 void NotifyLoaderPerformanceTimingChanged(LocalDOMWindow* window) {
   if (!window) {
     return;
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing_utils.h b/third_party/blink/renderer/core/paint/timing/paint_timing_utils.h
index a48da10..802458fb3 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing_utils.h
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing_utils.h
@@ -17,6 +17,7 @@
 class Document;
 class LocalDOMWindow;
 class LocalFrameView;
+class MediaTiming;
 }  // namespace blink
 
 namespace blink::paint_timing {
@@ -39,6 +40,12 @@
 CORE_EXPORT cc::HeadsUpDisplayLayer* GetHUDLayerIfLayoutShiftRectsEnabled(
     LocalFrameView* frame_view);
 
+// Returns whether or not the `media_timing` should be ignored. Currently only
+// `MediaTiming` objects representing the default poster image are ignored.
+CORE_EXPORT bool ShouldIgnoreImageContentForPaintTiming(
+    const LayoutObject& object,
+    const MediaTiming* media_timing);
+
 // Notifies the document loader that performance timing has changed in some way
 // if the window, document, and loader are non-null. Causes the current
 // performance timing values to be sent to UKM.
diff --git a/third_party/blink/renderer/core/paint/timing/image_element_timing_test.cc b/third_party/blink/renderer/core/paint/timing/image_element_timing_test.cc
index e5ff2aa..5dc4a4af 100644
--- a/third_party/blink/renderer/core/paint/timing/image_element_timing_test.cc
+++ b/third_party/blink/renderer/core/paint/timing/image_element_timing_test.cc
@@ -6,6 +6,7 @@
 
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
 #include "third_party/blink/renderer/core/layout/layout_image.h"
 #include "third_party/blink/renderer/core/layout/svg/layout_svg_image.h"
 #include "third_party/blink/renderer/core/loader/resource/image_resource_content.h"
@@ -15,6 +16,7 @@
 #include "third_party/blink/renderer/core/timing/performance.h"
 #include "third_party/blink/renderer/core/timing/performance_element_timing.h"
 #include "third_party/blink/renderer/platform/testing/paint_test_configurations.h"
+#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
 
 using testing::ElementsAre;
 using testing::IsEmpty;
@@ -198,7 +200,7 @@
       #target {
         width: 100px;
         height: 100px;
-        background: url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==);
+        background: url()HTML" SIMPLE_IMAGE R"HTML();
       }
     </style>
     <div elementtiming="time-my-background-image" id="target"></div>
@@ -261,4 +263,48 @@
   EXPECT_THAT(GetElementTimingEntries(), IsEmpty());
 }
 
+TEST_P(ImageElementTimingTest, VideoImage_DefaultPosterIgnored) {
+  GetDocument().GetSettings()->SetDefaultVideoPosterURL(
+      AtomicString(SIMPLE_IMAGE));
+  SetBodyInnerHTML(R"HTML(
+    <video id="target" elementtiming="video-et" width="100" height="100"></video>
+  )HTML");
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_EQ(RecordedImagesSize(), 0u);
+  EXPECT_THAT(GetElementTimingEntries(), IsEmpty());
+}
+
+TEST_P(ImageElementTimingTest,
+       VideoImage_ExplicitPosterRecordedWhenDefaultSet) {
+  GetDocument().GetSettings()->SetDefaultVideoPosterURL(
+      AtomicString(SIMPLE_IMAGE));
+  SetBodyInnerHTML(R"HTML(
+    <video id="target" elementtiming="video-et"
+           width="100" height="100"></video>
+  )HTML");
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_EQ(RecordedImagesSize(), 0u);
+  EXPECT_THAT(GetElementTimingEntries(), IsEmpty());
+
+  // Changing the poster to an explicit image after the default poster loaded
+  // should record the new explicit poster.
+  Element* video_element = GetElementById("target");
+  ASSERT_TRUE(video_element);
+  video_element->setAttribute(html_names::kPosterAttr,
+                              AtomicString(LARGE_IMAGE));
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_EQ(RecordedImagesSize(), 1u);
+  EXPECT_THAT(GetElementTimingEntries(), ElementsAre(ForId("target")));
+
+  // Removing the explicit poster attribute reverts to the default poster,
+  // which should remove the explicit poster record and not record the default.
+  video_element->removeAttribute(html_names::kPosterAttr);
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_EQ(RecordedImagesSize(), 0u);
+}
+
 }  // namespace blink
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 2c0382f..af0fbc0 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
@@ -46,23 +46,6 @@
 
 namespace blink {
 
-#define SIMPLE_IMAGE       \
-  "data:image/gif;base64," \
-  "R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
-
-#define LARGE_IMAGE                                                            \
-  "data:image/gif;base64,"                                                     \
-  "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSF" \
-  "lzAAAN1wAADdcBQiibeAAAAb5JREFUOMulkr1KA0EQgGdvTwwnYmER0gQsrFKmSy+pLESw9Qm0" \
-  "F/ICNnba+h6iEOuAEWslKJKTOyJJvIT72d1xZuOFC0giOLA77O7Mt/PnNptN+I+49Xr9GhH3f3" \
-  "mb0v1ht9vtLAUYYw5ItkgDL3KyD8PhcLvdbl/WarXT3DjLMnAcR/f7/YfxeKwtgC5RKQVhGILW" \
-  "eg4hQ6hUKjWyucmhLFEUuWR3QYBWAZABQ9i5CCmXy16pVALP80BKaaG+70MQBLvzFMjRKKXh8j" \
-  "6FSYKF7ITdEWLa4/ktokN74wiqjSMpnVcbQZqmEJHz+ckeCPFjWKwULpyspAqhdXVXdcnZcPjs" \
-  "Ign+2BsVA8jVYuWlgJ3yBj0icgq2uoK+lg4t+ZvLomSKamSQ4AI5BcMADtMhyNoSgNIISUaFNt" \
-  "wlazcDcBc4gjjVwCWid2usCWroYEhnaqbzFJLUzAHIXRDChXCcQP8zhkSZ5eNLgHAUzwDcRu4C" \
-  "oIRn/wsGUQIIy4Vr9TH6SYFCNzw4nALn5627K4vIttOUOwfa5YnrDYzt/9OLv9I5l8kk5hZ3XL" \
-  "O20b7tbR7zHLy/BX8G0IeBEM7ZN1NGIaFUaKLgAAAAAElFTkSuQmCC"
-
 #define TRANSPARENT_PLACEHOLDER_IMAGE \
   "data:image/gif;base64,"            \
   "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
@@ -825,6 +808,69 @@
   EXPECT_FALSE(record);
 }
 
+TEST_P(ImagePaintTimingDetectorTest, VideoImage_DefaultPosterIgnored) {
+  GetDocument().GetSettings()->SetDefaultVideoPosterURL(
+      AtomicString(LARGE_IMAGE));
+  SetMainFrameBodyContent(R"HTML(
+    <video id="target" width="300" height="200"></video>
+  )HTML");
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_FALSE(LargestImage());
+  EXPECT_EQ(CountImageRecords(), 0u);
+
+  // Verify that a subsequent first video frame is not blocked by the ignored
+  // default poster image.
+  Element* video_element = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(video_element);
+  VideoTiming* video_timing = MakeGarbageCollected<VideoTiming>();
+  video_timing->SetFirstVideoFrameTime(NowTicks());
+  video_timing->SetIsSufficientContentLoadedForPaint();
+  video_timing->SetUrl(KURL("http://test.com/video.mp4"));
+  video_timing->SetContentSizeForEntropy(1024 * 1024);
+
+  SimulateFirstVideoFrame(video_element, video_timing, 300, 200);
+  SimulateRenderingAndPresentationTime();
+  ImageRecord* record = LargestImage();
+  ASSERT_TRUE(record);
+  EXPECT_EQ(record->GetMediaTiming(), video_timing);
+  EXPECT_TRUE(record->HasPaintTime());
+}
+
+TEST_P(ImagePaintTimingDetectorTest,
+       VideoImage_ExplicitPosterRecordedWhenDefaultSet) {
+  GetDocument().GetSettings()->SetDefaultVideoPosterURL(
+      AtomicString(SIMPLE_IMAGE));
+  SetMainFrameBodyContent(R"HTML(
+    <video id="target" width="300" height="200"></video>
+  )HTML");
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_FALSE(LargestImage());
+  EXPECT_EQ(CountImageRecords(), 0u);
+
+  // Changing the poster to an explicit image after the default poster loaded
+  // should record the new explicit poster.
+  Element* video_element = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(video_element);
+  video_element->setAttribute(html_names::kPosterAttr,
+                              AtomicString(LARGE_IMAGE));
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  ImageRecord* record = LargestImage();
+  ASSERT_TRUE(record);
+  EXPECT_GT(record->EffectiveVisualSize(), 0ul);
+  EXPECT_TRUE(record->HasPaintTime());
+
+  // Removing the explicit poster attribute reverts to the default poster,
+  // which should remove the explicit poster record and not record the default.
+  video_element->removeAttribute(html_names::kPosterAttr);
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_EQ(CountImageRecords(), 0u);
+  EXPECT_EQ(ContainerTotalSize(), 0u);
+}
+
 TEST_P(ImagePaintTimingDetectorTest, SVGImage) {
   SetMainFrameBodyContent(R"HTML(
     <svg>
diff --git a/third_party/blink/web_tests/media/video-default-poster-paint-timing.html b/third_party/blink/web_tests/media/video-default-poster-paint-timing.html
new file mode 100644
index 0000000..954ed08
--- /dev/null
+++ b/third_party/blink/web_tests/media/video-default-poster-paint-timing.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<script src="../resources/testharness.js"></script>
+<script src="../resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async t => {
+  assert_implements(window.internals, 'window.internals is required');
+  internals.settings.setDefaultVideoPosterURL('content/abe.png');
+
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/paint/timing/image_element_timing_test.cc b/third_party/blink/renderer/core/paint/timing/image_element_timing_test.cc
index e5ff2aa..5dc4a4af 100644
--- a/third_party/blink/renderer/core/paint/timing/image_element_timing_test.cc
+++ b/third_party/blink/renderer/core/paint/timing/image_element_timing_test.cc
@@ -6,6 +6,7 @@
 
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
 #include "third_party/blink/renderer/core/layout/layout_image.h"
 #include "third_party/blink/renderer/core/layout/svg/layout_svg_image.h"
 #include "third_party/blink/renderer/core/loader/resource/image_resource_content.h"
@@ -15,6 +16,7 @@
 #include "third_party/blink/renderer/core/timing/performance.h"
 #include "third_party/blink/renderer/core/timing/performance_element_timing.h"
 #include "third_party/blink/renderer/platform/testing/paint_test_configurations.h"
+#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
 
 using testing::ElementsAre;
 using testing::IsEmpty;
@@ -198,7 +200,7 @@
       #target {
         width: 100px;
         height: 100px;
-        background: url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==);
+        background: url()HTML" SIMPLE_IMAGE R"HTML();
       }
     </style>
     <div elementtiming="time-my-background-image" id="target"></div>
@@ -261,4 +263,48 @@
   EXPECT_THAT(GetElementTimingEntries(), IsEmpty());
 }
 
+TEST_P(ImageElementTimingTest, VideoImage_DefaultPosterIgnored) {
+  GetDocument().GetSettings()->SetDefaultVideoPosterURL(
+      AtomicString(SIMPLE_IMAGE));
+  SetBodyInnerHTML(R"HTML(
+    <video id="target" elementtiming="video-et" width="100" height="100"></video>
+  )HTML");
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_EQ(RecordedImagesSize(), 0u);
+  EXPECT_THAT(GetElementTimingEntries(), IsEmpty());
+}
+
+TEST_P(ImageElementTimingTest,
+       VideoImage_ExplicitPosterRecordedWhenDefaultSet) {
+  GetDocument().GetSettings()->SetDefaultVideoPosterURL(
+      AtomicString(SIMPLE_IMAGE));
+  SetBodyInnerHTML(R"HTML(
+    <video id="target" elementtiming="video-et"
+           width="100" height="100"></video>
+  )HTML");
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_EQ(RecordedImagesSize(), 0u);
+  EXPECT_THAT(GetElementTimingEntries(), IsEmpty());
+
+  // Changing the poster to an explicit image after the default poster loaded
+  // should record the new explicit poster.
+  Element* video_element = GetElementById("target");
+  ASSERT_TRUE(video_element);
+  video_element->setAttribute(html_names::kPosterAttr,
+                              AtomicString(LARGE_IMAGE));
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_EQ(RecordedImagesSize(), 1u);
+  EXPECT_THAT(GetElementTimingEntries(), ElementsAre(ForId("target")));
+
+  // Removing the explicit poster attribute reverts to the default poster,
+  // which should remove the explicit poster record and not record the default.
+  video_element->removeAttribute(html_names::kPosterAttr);
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_EQ(RecordedImagesSize(), 0u);
+}
+
 }  // namespace blink
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 2c0382f..af0fbc0 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
@@ -46,23 +46,6 @@
 
 namespace blink {
 
-#define SIMPLE_IMAGE       \
-  "data:image/gif;base64," \
-  "R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
-
-#define LARGE_IMAGE                                                            \
-  "data:image/gif;base64,"                                                     \
-  "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSF" \
-  "lzAAAN1wAADdcBQiibeAAAAb5JREFUOMulkr1KA0EQgGdvTwwnYmER0gQsrFKmSy+pLESw9Qm0" \
-  "F/ICNnba+h6iEOuAEWslKJKTOyJJvIT72d1xZuOFC0giOLA77O7Mt/PnNptN+I+49Xr9GhH3f3" \
-  "mb0v1ht9vtLAUYYw5ItkgDL3KyD8PhcLvdbl/WarXT3DjLMnAcR/f7/YfxeKwtgC5RKQVhGILW" \
-  "eg4hQ6hUKjWyucmhLFEUuWR3QYBWAZABQ9i5CCmXy16pVALP80BKaaG+70MQBLvzFMjRKKXh8j" \
-  "6FSYKF7ITdEWLa4/ktokN74wiqjSMpnVcbQZqmEJHz+ckeCPFjWKwULpyspAqhdXVXdcnZcPjs" \
-  "Ign+2BsVA8jVYuWlgJ3yBj0icgq2uoK+lg4t+ZvLomSKamSQ4AI5BcMADtMhyNoSgNIISUaFNt" \
-  "wlazcDcBc4gjjVwCWid2usCWroYEhnaqbzFJLUzAHIXRDChXCcQP8zhkSZ5eNLgHAUzwDcRu4C" \
-  "oIRn/wsGUQIIy4Vr9TH6SYFCNzw4nALn5627K4vIttOUOwfa5YnrDYzt/9OLv9I5l8kk5hZ3XL" \
-  "O20b7tbR7zHLy/BX8G0IeBEM7ZN1NGIaFUaKLgAAAAAElFTkSuQmCC"
-
 #define TRANSPARENT_PLACEHOLDER_IMAGE \
   "data:image/gif;base64,"            \
   "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
@@ -825,6 +808,69 @@
   EXPECT_FALSE(record);
 }
 
+TEST_P(ImagePaintTimingDetectorTest, VideoImage_DefaultPosterIgnored) {
+  GetDocument().GetSettings()->SetDefaultVideoPosterURL(
+      AtomicString(LARGE_IMAGE));
+  SetMainFrameBodyContent(R"HTML(
+    <video id="target" width="300" height="200"></video>
+  )HTML");
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_FALSE(LargestImage());
+  EXPECT_EQ(CountImageRecords(), 0u);
+
+  // Verify that a subsequent first video frame is not blocked by the ignored
+  // default poster image.
+  Element* video_element = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(video_element);
+  VideoTiming* video_timing = MakeGarbageCollected<VideoTiming>();
+  video_timing->SetFirstVideoFrameTime(NowTicks());
+  video_timing->SetIsSufficientContentLoadedForPaint();
+  video_timing->SetUrl(KURL("http://test.com/video.mp4"));
+  video_timing->SetContentSizeForEntropy(1024 * 1024);
+
+  SimulateFirstVideoFrame(video_element, video_timing, 300, 200);
+  SimulateRenderingAndPresentationTime();
+  ImageRecord* record = LargestImage();
+  ASSERT_TRUE(record);
+  EXPECT_EQ(record->GetMediaTiming(), video_timing);
+  EXPECT_TRUE(record->HasPaintTime());
+}
+
+TEST_P(ImagePaintTimingDetectorTest,
+       VideoImage_ExplicitPosterRecordedWhenDefaultSet) {
+  GetDocument().GetSettings()->SetDefaultVideoPosterURL(
+      AtomicString(SIMPLE_IMAGE));
+  SetMainFrameBodyContent(R"HTML(
+    <video id="target" width="300" height="200"></video>
+  )HTML");
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_FALSE(LargestImage());
+  EXPECT_EQ(CountImageRecords(), 0u);
+
+  // Changing the poster to an explicit image after the default poster loaded
+  // should record the new explicit poster.
+  Element* video_element = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(video_element);
+  video_element->setAttribute(html_names::kPosterAttr,
+                              AtomicString(LARGE_IMAGE));
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  ImageRecord* record = LargestImage();
+  ASSERT_TRUE(record);
+  EXPECT_GT(record->EffectiveVisualSize(), 0ul);
+  EXPECT_TRUE(record->HasPaintTime());
+
+  // Removing the explicit poster attribute reverts to the default poster,
+  // which should remove the explicit poster record and not record the default.
+  video_element->removeAttribute(html_names::kPosterAttr);
+  test::RunPendingTasks();
+  SimulateRenderingAndPresentationTime();
+  EXPECT_EQ(CountImageRecords(), 0u);
+  EXPECT_EQ(ContainerTotalSize(), 0u);
+}
+
 TEST_P(ImagePaintTimingDetectorTest, SVGImage) {
   SetMainFrameBodyContent(R"HTML(
     <svg>
diff --git a/third_party/blink/web_tests/media/video-default-poster-paint-timing.html b/third_party/blink/web_tests/media/video-default-poster-paint-timing.html
new file mode 100644
index 0000000..954ed08
--- /dev/null
+++ b/third_party/blink/web_tests/media/video-default-poster-paint-timing.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<script src="../resources/testharness.js"></script>
+<script src="../resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async t => {
+  assert_implements(window.internals, 'window.internals is required');
+  internals.settings.setDefaultVideoPosterURL('content/abe.png');
+
+  // Preload both images so their resource loads don't race.
+  await Promise.all(['content/abe.png', 'content/greenbox.png'].map(src => {
+    return new Promise((resolve, reject) => {
+      const img = new Image();
+      img.onload = resolve;
+      img.onerror = reject;
+      img.src = src;
+    });
+  }));
+
+  const entries = [];
+  const observer = new PerformanceObserver(list => {
+    entries.push(...list.getEntries());
+  });
+  observer.observe({entryTypes: ['largest-contentful-paint', 'element']});
+
+  // 1. Video with default poster (no poster attribute) and elementtiming.
+  // Sized larger (300x300) than videoExplicit so it would win LCP if not filtered.
+  const videoDefault = document.createElement('video');
+  videoDefault.id = 'video-default';
+  videoDefault.setAttribute('elementtiming', 'default-et');
+  videoDefault.width = 300;
+  videoDefault.height = 300;
+  document.body.appendChild(videoDefault);
+
+  // Ensure videoDefault has gone through a rendering update before adding videoExplicit.
+  await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
+
+  // 2. Video with explicit poster and elementtiming.
+  const videoExplicit = document.createElement('video');
+  videoExplicit.id = 'video-explicit';
+  videoExplicit.poster = 'content/greenbox.png';
+  videoExplicit.setAttribute('elementtiming', 'explicit-et');
+  videoExplicit.width = 100;
+  videoExplicit.height = 100;
+  document.body.appendChild(videoExplicit);
+
+  // Wait for an LCP entry and the explicit ElementTiming entry.
+  // Do not filter LCP by id here so that if video-default wrongly wins LCP,
+  // step_wait resolves immediately and fails the assertion below instead of timing out.
+  await t.step_wait(() => {
+    const hasLcp = entries.some(e => e.entryType === 'largest-contentful-paint');
+    const hasEt = entries.some(
+        e => e.entryType === 'element' && e.identifier === 'explicit-et');
+    return hasLcp && hasEt;
+  }, 'Wait for LCP and ElementTiming entries');
+
+  // Verify that the default poster was never reported to LCP or ElementTiming,
+  // and that the explicit poster was reported to LCP.
+  const defaultLcp = entries.find(
+      e => e.entryType === 'largest-contentful-paint' &&
+           (e.id === 'video-default' || e.url.includes('abe.png')));
+  const defaultEt = entries.find(
+      e => e.entryType === 'element' &&
+           (e.identifier === 'default-et' || e.url.includes('abe.png')));
+  const explicitLcp = entries.find(
+      e => e.entryType === 'largest-contentful-paint' &&
+           e.id === 'video-explicit');
+
+  assert_equals(defaultLcp, undefined,
+      'Default video poster should not be reported to LargestContentfulPaint');
+  assert_equals(defaultEt, undefined,
+      'Default video poster should not be reported to ElementTiming');
+  assert_not_equals(explicitLcp, undefined,
+      'Explicit video poster should be reported to LargestContentfulPaint');
+}, 'Default video poster image is ignored by LargestContentfulPaint and ElementTiming');
+</script>
+</body>
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.