Low watch CVSS 6.5 chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in ServiceWorker
DescriptionMissing authorization in ServiceWorker
ComponentServiceWorker
Sub-componentPLMO
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497025031
Fix commit157d2e767580 (chromium/src) +223/-1
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-27 (per vendor advisory)
Fix landed2026-08-26 (public git)
Disclosed2026-09-08
Patch gap13 days — fix public in source before the advisory

Background

`PageLoadMetricsObserver` (PLMO)
a browser-process observer interface whose implementations are notified of a page’s resource loads and lifecycle events, and which embedders can use to apply per-load policy or bookkeeping.
In-renderer `MemoryCache`
Blink’s per-renderer memory cache that can satisfy a subresource request entirely inside the renderer, without a network/HTTP-cache round trip.
`DidLoadResourceFromMemoryCache`
the WebContentsObserver callback that fires when a subresource is served from that in-renderer memory cache, carrying the url, mime_type, and network::mojom::RequestDestination.
`MemoryResourceLoadInfo`
the new struct added by the patch that packages a memory-cache load’s url (url::SchemeHostPort), request_destination, and frame_tree_node_id for delivery to observers.

Root Cause Analysis

Before the patch, MetricsWebContentsObserver implemented OnLoadedResource() but did not implement DidLoadResourceFromMemoryCache(), and the comment added in page_load_metrics_observer_interface.h states the invariant explicitly: “in-renderer memory cache loads do not trigger OnLoadedResource().”

As a result, any subresource that Blink served from its in-renderer MemoryCache was never surfaced to PageLoadTracker or its PageLoadMetricsObservers, so an observer that inspects loaded subresources (and the destination/origin they came from) simply never saw those loads.

The patch closes that gap by overriding MetricsWebContentsObserver::DidLoadResourceFromMemoryCache, validating the url (via url.is_valid() and ShouldTrackScheme), locating the owning PageLoadTracker for the render_frame_host, and forwarding a MemoryResourceLoadInfo down through PageLoadTracker::DidLoadResourceFromMemoryCache to every observer (and through PageLoadMetricsForwardObserver to parent observers). Because the interface method is now pure-virtual (= 0), every observer must handle memory-cache loads, restoring the invariant that observers see all subresource loads regardless of whether they were served from the network or from the in-renderer cache.

The fix is a plumbing/notification change: it does not add a bounds check or free, it adds a previously-missing observation edge so downstream policy/authorization logic can act on memory-cache-served resources.

Key insight
The single mistake was an incomplete observation surface: memory-cache-served subresources bypassed OnLoadedResource() and had no other notification path, so observer-based enforcement was silently skipped for them; the fix adds a mandatory DidLoadResourceFromMemoryCache() callback so those loads are observed like any other.

Attack Path

  1. Prime the in-renderer memory cache An attacker page arranges for a subresource (e.g. a script, identified by RequestDestination::kScript) to be resident in Blink’s in-renderer MemoryCache.
  2. Re-request the cached subresource A subsequent request for that resource is satisfied entirely from the in-renderer cache, firing WebContentsObserver::DidLoadResourceFromMemoryCache but never OnLoadedResource().
  3. Observer never sees the load Because MetricsWebContentsObserver did not override DidLoadResourceFromMemoryCache, PageLoadTracker and its PageLoadMetricsObservers were never told the resource loaded, so any per-load authorization or policy check that keys off observed subresource loads did not run for it (inferred).
  4. Enforcement gap The advisory characterizes the result as a missing authorization affecting ServiceWorker; the concrete effect an observer-based check would have applied to that memory-cache load is not shown in the diff and is (inferred).

Impact Assessment

The patch demonstrates that browser-process PageLoadMetricsObservers were not notified of in-renderer memory-cache subresource loads, so any authorization or policy decision implemented as such an observer would be bypassed for those loads; the advisory classifies this as missing authorization in ServiceWorker. The precondition is that the resource be served from Blink’s in-renderer MemoryCache (which skips OnLoadedResource()). The advisory does not state exploitation in the wild, and any concrete authorization bypass or data exposure beyond the missing notification is (inferred).

Changed Functions

FunctionChangeNotes
if
components/page_load_metrics/browser/page_load_metrics_forward_observer.cc
modified
for
components/page_load_metrics/browser/page_load_tracker.cc
modified
TEST_P
components/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc
modified

Files Changed

  • components/page_load_metrics/browser/metrics_web_contents_observer.h
  • components/page_load_metrics/browser/page_load_metrics_observer.h
  • components/page_load_metrics/browser/page_load_metrics_forward_observer.h
  • components/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.cc
  • components/page_load_metrics/browser/observers/assert_page_load_metrics_observer.h
  • components/page_load_metrics/browser/page_load_tracker.h
  • components/page_load_metrics/browser/metrics_web_contents_observer.cc
  • components/page_load_metrics/browser/page_load_metrics_observer_interface.h
  • components/page_load_metrics/browser/page_load_tracker.cc
  • components/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc
  • components/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.h
  • components/page_load_metrics/browser/page_load_metrics_forward_observer.cc

Audit Directions

  • Parallel notification paths
    Wherever a resource or event can be delivered through more than one path (network vs. in-renderer cache, main frame vs. subframe), verify every observer/policy hook is invoked on all paths, not just the common one.
  • Optional-becoming-mandatory hooks
    Audit observer interfaces where a callback was empty-bodied/default rather than pure-virtual; a = {} default silently opts implementations out of seeing certain events, which can defeat enforcement built on that interface.
  • Cache hits that skip instrumentation
    Review other cache layers (HTTP cache, bfcache, ServiceWorker CacheStorage) for load types that satisfy requests without emitting the standard resource-load notifications observers rely on.
From 157d2e767580b1fefa6a0ee0be2f85e0a28f85a1 Mon Sep 17 00:00:00 2001
From: Keita Suzuki <suzukikeita@chromium.org>
Date: Wed, 26 Aug 2026 08:52:56 -0700
Subject: [PATCH] [PLMO] Forward in-renderer memory cache loads to PLMO

This CL adds DidLoadResourceFromMemoryCache to PageLoadMetricsObserver
(PLMO) and wires it from
WebContentsObserver::DidLoadResourceFromMemoryCache via
MetricsWebContentsObserver and PageLoadTracker.

This allows PLMO implementations to observe subresources loaded from
Blink's in-renderer MemoryCache along with their RequestDestination.

Bug: 497025031
TAG=agy
CONV=83fb7234-7696-46df-a1f2-d333725caf1b

Change-Id: I81395ef374d46a9be1c992e25cdf8d6c9b8eb068
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8278710
Reviewed-by: Shunya Shishido <sisidovski@chromium.org>
Commit-Queue: Keita Suzuki <suzukikeita@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1686427}
---
diff --git a/components/page_load_metrics/browser/metrics_web_contents_observer.cc b/components/page_load_metrics/browser/metrics_web_contents_observer.cc
index 243328a..0d157043 100644
--- a/components/page_load_metrics/browser/metrics_web_contents_observer.cc
+++ b/components/page_load_metrics/browser/metrics_web_contents_observer.cc
@@ -505,6 +505,23 @@
   }
 }
 
+void MetricsWebContentsObserver::DidLoadResourceFromMemoryCache(
+    content::RenderFrameHost* render_frame_host,
+    const GURL& url,
+    const std::string& mime_type,
+    network::mojom::RequestDestination request_destination) {
+  if (!url.is_valid() || !ShouldTrackScheme(url.scheme())) {
+    return;
+  }
+
+  if (PageLoadTracker* tracker = GetPageLoadTracker(render_frame_host)) {
+    MemoryResourceLoadInfo memory_resource_load_info{
+        url::SchemeHostPort(url), request_destination,
+        render_frame_host->GetFrameTreeNodeId()};
+    tracker->DidLoadResourceFromMemoryCache(memory_resource_load_info);
+  }
+}
+
 void MetricsWebContentsObserver::FrameReceivedUserActivation(
     content::RenderFrameHost* render_frame_host) {
   if (PageLoadTracker* tracker = GetPageLoadTracker(render_frame_host)) {
diff --git a/components/page_load_metrics/browser/metrics_web_contents_observer.h b/components/page_load_metrics/browser/metrics_web_contents_observer.h
index 81e7088..528d0599 100644
--- a/components/page_load_metrics/browser/metrics_web_contents_observer.h
+++ b/components/page_load_metrics/browser/metrics_web_contents_observer.h
@@ -123,6 +123,11 @@
       const content::GlobalRequestID& request_id,
       const GURL& original_url,
       const blink::mojom::ResourceLoadInfo& resource_load_info) override;
+  void DidLoadResourceFromMemoryCache(
+      content::RenderFrameHost* render_frame_host,
+      const GURL& url,
+      const std::string& mime_type,
+      network::mojom::RequestDestination request_destination) override;
   void FrameReceivedUserActivation(
       content::RenderFrameHost* render_frame_host) override;
   void FrameDisplayStateChanged(content::RenderFrameHost* render_frame_host,
diff --git a/components/page_load_metrics/browser/observers/assert_page_load_metrics_observer.h b/components/page_load_metrics/browser/observers/assert_page_load_metrics_observer.h
index 660cf505..0d0449c2 100644
--- a/components/page_load_metrics/browser/observers/assert_page_load_metrics_observer.h
+++ b/components/page_load_metrics/browser/observers/assert_page_load_metrics_observer.h
@@ -194,6 +194,9 @@
       const base::flat_map<int, gfx::Rect>& main_frame_ad_rects) override {}
   void OnLoadedResource(const page_load_metrics::ExtraRequestCompleteInfo&
                             extra_request_complete_info) override {}
+  void DidLoadResourceFromMemoryCache(
+      const page_load_metrics::MemoryResourceLoadInfo&
+          memory_resource_load_info) override {}
   void FrameReceivedUserActivation(
       content::RenderFrameHost* render_frame_host) override {}
   void FrameDisplayStateChanged(content::RenderFrameHost* render_frame_host,
diff --git a/components/page_load_metrics/browser/page_load_metrics_forward_observer.cc b/components/page_load_metrics/browser/page_load_metrics_forward_observer.cc
index 6e1d255..b4ac9397 100644
--- a/components/page_load_metrics/browser/page_load_metrics_forward_observer.cc
+++ b/components/page_load_metrics/browser/page_load_metrics_forward_observer.cc
@@ -365,6 +365,14 @@
   parent_observer_->OnLoadedResource(extra_request_complete_info);
 }
 
+void PageLoadMetricsForwardObserver::DidLoadResourceFromMemoryCache(
+    const MemoryResourceLoadInfo& memory_resource_load_info) {
+  if (!parent_observer_) {
+    return;
+  }
+  parent_observer_->DidLoadResourceFromMemoryCache(memory_resource_load_info);
+}
+
 void PageLoadMetricsForwardObserver::FrameReceivedUserActivation(
     content::RenderFrameHost* render_frame_host) {
   if (!parent_observer_)
diff --git a/components/page_load_metrics/browser/page_load_metrics_forward_observer.h b/components/page_load_metrics/browser/page_load_metrics_forward_observer.h
index 6005cd29..d1f89f17 100644
--- a/components/page_load_metrics/browser/page_load_metrics_forward_observer.h
+++ b/components/page_load_metrics/browser/page_load_metrics_forward_observer.h
@@ -151,6 +151,8 @@
       const FailedProvisionalLoadInfo& failed_provisional_load_info) override;
   void OnLoadedResource(
       const ExtraRequestCompleteInfo& extra_request_complete_info) override;
+  void DidLoadResourceFromMemoryCache(
+      const MemoryResourceLoadInfo& memory_resource_load_info) override;
   void FrameReceivedUserActivation(
       content::RenderFrameHost* render_frame_host) override;
   void FrameDisplayStateChanged(content::RenderFrameHost* render_frame_host,
diff --git a/components/page_load_metrics/browser/page_load_metrics_observer.h b/components/page_load_metrics/browser/page_load_metrics_observer.h
index 29932d6..f52e412 100644
--- a/components/page_load_metrics/browser/page_load_metrics_observer.h
+++ b/components/page_load_metrics/browser/page_load_metrics_observer.h
@@ -231,6 +231,8 @@
       const FailedProvisionalLoadInfo& failed_provisional_load_info) override {}
   void OnLoadedResource(
       const ExtraRequestCompleteInfo& extra_request_complete_info) override {}
+  void DidLoadResourceFromMemoryCache(
+      const MemoryResourceLoadInfo& memory_resource_load_info) override {}
   void FrameReceivedUserActivation(
       content::RenderFrameHost* render_frame_host) override {}
   void FrameDisplayStateChanged(content::RenderFrameHost* render_frame_host,
diff --git a/components/page_load_metrics/browser/page_load_metrics_observer_interface.h b/components/page_load_metrics/browser/page_load_metrics_observer_interface.h
index cef7fb8..f800c27 100644
--- a/components/page_load_metrics/browser/page_load_metrics_observer_interface.h
+++ b/components/page_load_metrics/browser/page_load_metrics_observer_interface.h
@@ -25,6 +25,7 @@
 #include "third_party/blink/public/common/input/web_input_event.h"
 #include "third_party/blink/public/common/use_counter/use_counter_feature.h"
 #include "url/gurl.h"
+#include "url/scheme_host_port.h"
 
 namespace blink {
 struct JavaScriptFrameworkDetectionResult;
@@ -108,6 +109,18 @@
   const std::unique_ptr<net::LoadTimingInfo> load_timing_info;
 };
 
+// Information related to resource loads from the in-renderer memory cache.
+struct MemoryResourceLoadInfo {
+  // The scheme, host, and port of the loaded memory cache resource.
+  const url::SchemeHostPort url;
+
+  // The request destination of the resource (e.g. script, image, style).
+  const network::mojom::RequestDestination request_destination;
+
+  // The ID of the frame tree node that initiated the request.
+  const content::FrameTreeNodeId frame_tree_node_id;
+};
+
 // Information related to failed provisional loads.
 struct FailedProvisionalLoadInfo {
   FailedProvisionalLoadInfo(
@@ -551,6 +564,14 @@
   virtual void OnLoadedResource(
       const ExtraRequestCompleteInfo& extra_request_complete_info) = 0;
 
+  // Called whenever a resource is loaded from the in-renderer memory cache.
+  // Note: in-renderer memory cache loads do not trigger OnLoadedResource().
+  // Observers that need to track all resource loads (both network/HTTP cache
+  // and in-renderer memory cache) should implement both OnLoadedResource() and
+  // DidLoadResourceFromMemoryCache().
+  virtual void DidLoadResourceFromMemoryCache(
+      const MemoryResourceLoadInfo& memory_resource_load_info) = 0;
+
   virtual void FrameReceivedUserActivation(
       content::RenderFrameHost* render_frame_host) = 0;
 
diff --git a/components/page_load_metrics/browser/page_load_tracker.cc b/components/page_load_metrics/browser/page_load_tracker.cc
index eac9164..ae868b38 100644
--- a/components/page_load_metrics/browser/page_load_tracker.cc
+++ b/components/page_load_metrics/browser/page_load_tracker.cc
@@ -765,6 +765,13 @@
   }
 }
 
+void PageLoadTracker::DidLoadResourceFromMemoryCache(
+    const MemoryResourceLoadInfo& memory_resource_load_info) {
+  for (const auto& observer : observers_) {
+    observer->DidLoadResourceFromMemoryCache(memory_resource_load_info);
+  }
+}
+
 void PageLoadTracker::FrameReceivedUserActivation(
     content::RenderFrameHost* rfh) {
   for (const auto& observer : observers_) {
diff --git a/components/page_load_metrics/browser/page_load_tracker.h b/components/page_load_metrics/browser/page_load_tracker.h
index 0ae3528..858cca3 100644
--- a/components/page_load_metrics/browser/page_load_tracker.h
+++ b/components/page_load_metrics/browser/page_load_tracker.h
@@ -340,6 +340,8 @@
 
   void OnLoadedResource(
       const ExtraRequestCompleteInfo& extra_request_complete_info);
+  void DidLoadResourceFromMemoryCache(
+      const MemoryResourceLoadInfo& memory_resource_load_info);
 
   void FrameReceivedUserActivation(content::RenderFrameHost* rfh);
   void FrameDisplayStateChanged(content::RenderFrameHost* render_frame_host,
diff --git a/components/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.cc b/components/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.cc
index be830dd..351c023 100644
--- a/components/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.cc
+++ b/components/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.cc
@@ -28,6 +28,7 @@
       std::vector<mojom::CpuTimingPtr>* updated_cpu_timings,
       std::vector<mojom::CustomUserTimingMarkPtr>* updated_custom_user_timings,
       std::vector<ExtraRequestCompleteInfo>* loaded_resources,
+      std::vector<MemoryResourceLoadInfo>* memory_cached_resources,
       std::vector<GURL>* observed_committed_urls,
       std::vector<GURL>* observed_aborted_urls,
       std::vector<blink::UseCounterFeature>* observed_features,
@@ -39,6 +40,7 @@
         updated_cpu_timings_(updated_cpu_timings),
         updated_custom_user_timings_(updated_custom_user_timings),
         loaded_resources_(loaded_resources),
+        memory_cached_resources_(memory_cached_resources),
         observed_features_(observed_features),
         observed_committed_urls_(observed_committed_urls),
         observed_aborted_urls_(observed_aborted_urls),
@@ -107,6 +109,11 @@
     loaded_resources_->emplace_back(extra_request_complete_info);
   }
 
+  void DidLoadResourceFromMemoryCache(
+      const MemoryResourceLoadInfo& memory_resource_load_info) override {
+    memory_cached_resources_->push_back(memory_resource_load_info);
+  }
+
   void OnFeaturesUsageObserved(
       content::RenderFrameHost* rfh,
       const std::vector<blink::UseCounterFeature>& features) override {
@@ -134,6 +141,7 @@
   const raw_ptr<std::vector<mojom::CustomUserTimingMarkPtr>>
       updated_custom_user_timings_;
   const raw_ptr<std::vector<ExtraRequestCompleteInfo>> loaded_resources_;
+  const raw_ptr<std::vector<MemoryResourceLoadInfo>> memory_cached_resources_;
   const raw_ptr<std::vector<blink::UseCounterFeature>> observed_features_;
   const raw_ptr<std::vector<GURL>> observed_committed_urls_;
   const raw_ptr<std::vector<GURL>> observed_aborted_urls_;
@@ -207,7 +215,8 @@
   tracker->AddObserver(std::make_unique<TimingLoggingPageLoadMetricsObserver>(
       &updated_timings_, &updated_subframe_timings_, &complete_timings_,
       &updated_cpu_timings_, &updated_custom_user_timings_, &loaded_resources_,
-      &observed_committed_urls_, &observed_aborted_urls_, &observed_features_,
+      &memory_cached_resources_, &observed_committed_urls_,
+      &observed_aborted_urls_, &observed_features_,
       &is_first_navigation_in_web_contents_,
       &count_on_enter_back_forward_cache_));
   tracker->AddObserver(std::make_unique<FilteringPageLoadMetricsObserver>(
diff --git a/components/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.h b/components/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.h
index dea75ad5..5115420 100644
--- a/components/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.h
+++ b/components/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.h
@@ -82,6 +82,10 @@
     return loaded_resources_;
   }
 
+  const std::vector<MemoryResourceLoadInfo>& memory_cached_resources() const {
+    return memory_cached_resources_;
+  }
+
   // committed URLs passed to FilteringPageLoadMetricsObserver::OnComplete().
   const std::vector<GURL>& completed_filtered_urls() const {
     return completed_filtered_urls_;
@@ -100,6 +104,7 @@
   std::vector<GURL> observed_committed_urls_;
   std::vector<GURL> observed_aborted_urls_;
   std::vector<ExtraRequestCompleteInfo> loaded_resources_;
+  std::vector<MemoryResourceLoadInfo> memory_cached_resources_;
   std::vector<GURL> completed_filtered_urls_;
   std::vector<blink::UseCounterFeature> observed_features_;
   std::optional<bool> is_first_navigation_in_web_contents_;
diff --git a/components/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc b/components/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc
index 8aa7ee3f..8e41205 100644
--- a/components/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc
+++ b/components/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc
@@ -273,6 +273,10 @@
     return embedder_interface_->loaded_resources();
   }
 
+  const std::vector<MemoryResourceLoadInfo>& memory_cached_resources() const {
+    return embedder_interface_->memory_cached_resources();
+  }
+
  protected:
   MetricsWebContentsObserver* observer() {
     return MetricsWebContentsObserver::FromWebContents(web_contents());
@@ -1075,6 +1079,143 @@
   EXPECT_TRUE(loaded_resources().empty());
 }
 
+TEST_P(MetricsWebContentsObserverTest, DidLoadResourceFromMemoryCache) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+  GURL memory_cached_url("https://www.google.com/test.js");
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), memory_cached_url,
+      "application/javascript", network::mojom::RequestDestination::kScript);
+
+  ASSERT_EQ(1u, memory_cached_resources().size());
+  EXPECT_EQ(url::SchemeHostPort(memory_cached_url),
+            memory_cached_resources().back().url);
+  EXPECT_EQ(network::mojom::RequestDestination::kScript,
+            memory_cached_resources().back().request_destination);
+  EXPECT_EQ(web_contents()->GetPrimaryMainFrame()->GetFrameTreeNodeId(),
+            memory_cached_resources().back().frame_tree_node_id);
+}
+
+TEST_P(MetricsWebContentsObserverTest,
+       DidLoadResourceFromMemoryCache_ResourceFromOtherRFHIgnored) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+
+  content::RenderFrameHost* old_rfh = web_contents()->GetPrimaryMainFrame();
+  content::LeaveInPendingDeletionState(old_rfh);
+
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl2));
+
+  ASSERT_FALSE(old_rfh->IsActive());
+  GURL other_url("https://www.google.com/test.js");
+  observer()->DidLoadResourceFromMemoryCache(
+      old_rfh, other_url, "application/javascript",
+      network::mojom::RequestDestination::kScript);
+
+  EXPECT_TRUE(memory_cached_resources().empty());
+}
+
+TEST_P(MetricsWebContentsObserverTest,
+       DidLoadResourceFromMemoryCache_Subframe) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+
+  content::RenderFrameHostTester* rfh_tester =
+      content::RenderFrameHostTester::For(main_rfh());
+  content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
+  subframe = content::NavigationSimulator::NavigateAndCommitFromDocument(
+      GURL(kDefaultTestUrl2), subframe);
+
+  GURL memory_cached_url("https://www.google.com/subframe_script.js");
+  observer()->DidLoadResourceFromMemoryCache(
+      subframe, memory_cached_url, "application/javascript",
+      network::mojom::RequestDestination::kScript);
+
+  ASSERT_EQ(1u, memory_cached_resources().size());
+  EXPECT_EQ(url::SchemeHostPort(memory_cached_url),
+            memory_cached_resources().back().url);
+  EXPECT_EQ(network::mojom::RequestDestination::kScript,
+            memory_cached_resources().back().request_destination);
+  EXPECT_EQ(subframe->GetFrameTreeNodeId(),
+            memory_cached_resources().back().frame_tree_node_id);
+}
+
+TEST_P(MetricsWebContentsObserverTest,
+       DidLoadResourceFromMemoryCache_UntrackedSchemeIgnored) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+
+  GURL untracked_url("custom://www.example.com/script.js");
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), untracked_url,
+      "application/javascript", network::mojom::RequestDestination::kScript);
+
+  EXPECT_TRUE(memory_cached_resources().empty());
+}
+
+TEST_P(MetricsWebContentsObserverTest,
+       DidLoadResourceFromMemoryCache_InvalidUrlIgnored) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+
+  // Empty GURL.
+  GURL empty_url;
+  ASSERT_FALSE(empty_url.is_valid());
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), empty_url,
+      "application/javascript", network::mojom::RequestDestination::kScript);
+
+  // Invalid GURL.
+  GURL invalid_url("http://google.com:12three45");
+  ASSERT_FALSE(invalid_url.is_valid());
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), invalid_url,
+      "application/javascript", network::mojom::RequestDestination::kScript);
+
+  EXPECT_TRUE(memory_cached_resources().empty());
+}
+
+TEST_P(MetricsWebContentsObserverTest,
+       DidLoadResourceFromMemoryCache_MultipleSequentialResources) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+
+  GURL script_url("https://www.google.com/test.js");
+  observer()->DidLoadResourceFromMemoryCache(
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc b/components/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc
index 8aa7ee3f..8e41205 100644
--- a/components/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc
+++ b/components/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc
@@ -273,6 +273,10 @@
     return embedder_interface_->loaded_resources();
   }
 
+  const std::vector<MemoryResourceLoadInfo>& memory_cached_resources() const {
+    return embedder_interface_->memory_cached_resources();
+  }
+
  protected:
   MetricsWebContentsObserver* observer() {
     return MetricsWebContentsObserver::FromWebContents(web_contents());
@@ -1075,6 +1079,143 @@
   EXPECT_TRUE(loaded_resources().empty());
 }
 
+TEST_P(MetricsWebContentsObserverTest, DidLoadResourceFromMemoryCache) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+  GURL memory_cached_url("https://www.google.com/test.js");
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), memory_cached_url,
+      "application/javascript", network::mojom::RequestDestination::kScript);
+
+  ASSERT_EQ(1u, memory_cached_resources().size());
+  EXPECT_EQ(url::SchemeHostPort(memory_cached_url),
+            memory_cached_resources().back().url);
+  EXPECT_EQ(network::mojom::RequestDestination::kScript,
+            memory_cached_resources().back().request_destination);
+  EXPECT_EQ(web_contents()->GetPrimaryMainFrame()->GetFrameTreeNodeId(),
+            memory_cached_resources().back().frame_tree_node_id);
+}
+
+TEST_P(MetricsWebContentsObserverTest,
+       DidLoadResourceFromMemoryCache_ResourceFromOtherRFHIgnored) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+
+  content::RenderFrameHost* old_rfh = web_contents()->GetPrimaryMainFrame();
+  content::LeaveInPendingDeletionState(old_rfh);
+
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl2));
+
+  ASSERT_FALSE(old_rfh->IsActive());
+  GURL other_url("https://www.google.com/test.js");
+  observer()->DidLoadResourceFromMemoryCache(
+      old_rfh, other_url, "application/javascript",
+      network::mojom::RequestDestination::kScript);
+
+  EXPECT_TRUE(memory_cached_resources().empty());
+}
+
+TEST_P(MetricsWebContentsObserverTest,
+       DidLoadResourceFromMemoryCache_Subframe) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+
+  content::RenderFrameHostTester* rfh_tester =
+      content::RenderFrameHostTester::For(main_rfh());
+  content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
+  subframe = content::NavigationSimulator::NavigateAndCommitFromDocument(
+      GURL(kDefaultTestUrl2), subframe);
+
+  GURL memory_cached_url("https://www.google.com/subframe_script.js");
+  observer()->DidLoadResourceFromMemoryCache(
+      subframe, memory_cached_url, "application/javascript",
+      network::mojom::RequestDestination::kScript);
+
+  ASSERT_EQ(1u, memory_cached_resources().size());
+  EXPECT_EQ(url::SchemeHostPort(memory_cached_url),
+            memory_cached_resources().back().url);
+  EXPECT_EQ(network::mojom::RequestDestination::kScript,
+            memory_cached_resources().back().request_destination);
+  EXPECT_EQ(subframe->GetFrameTreeNodeId(),
+            memory_cached_resources().back().frame_tree_node_id);
+}
+
+TEST_P(MetricsWebContentsObserverTest,
+       DidLoadResourceFromMemoryCache_UntrackedSchemeIgnored) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+
+  GURL untracked_url("custom://www.example.com/script.js");
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), untracked_url,
+      "application/javascript", network::mojom::RequestDestination::kScript);
+
+  EXPECT_TRUE(memory_cached_resources().empty());
+}
+
+TEST_P(MetricsWebContentsObserverTest,
+       DidLoadResourceFromMemoryCache_InvalidUrlIgnored) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+
+  // Empty GURL.
+  GURL empty_url;
+  ASSERT_FALSE(empty_url.is_valid());
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), empty_url,
+      "application/javascript", network::mojom::RequestDestination::kScript);
+
+  // Invalid GURL.
+  GURL invalid_url("http://google.com:12three45");
+  ASSERT_FALSE(invalid_url.is_valid());
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), invalid_url,
+      "application/javascript", network::mojom::RequestDestination::kScript);
+
+  EXPECT_TRUE(memory_cached_resources().empty());
+}
+
+TEST_P(MetricsWebContentsObserverTest,
+       DidLoadResourceFromMemoryCache_MultipleSequentialResources) {
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), GURL(kDefaultTestUrl));
+
+  GURL script_url("https://www.google.com/test.js");
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), script_url,
+      "application/javascript", network::mojom::RequestDestination::kScript);
+
+  GURL style_url("https://www.google.com/test.css");
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), style_url, "text/css",
+      network::mojom::RequestDestination::kStyle);
+
+  GURL image_url("https://www.google.com/test.png");
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), image_url, "image/png",
+      network::mojom::RequestDestination::kImage);
+
+  GURL font_url("https://www.google.com/test.woff2");
+  observer()->DidLoadResourceFromMemoryCache(
+      web_contents()->GetPrimaryMainFrame(), font_url, "font/woff2",
+      network::mojom::RequestDestination::kFont);
+
+  ASSERT_EQ(4u, memory_cached_resources().size());
+  EXPECT_EQ(url::SchemeHostPort(script_url), memory_cached_resources()[0].url);
+  EXPECT_EQ(network::mojom::RequestDestination::kScript,
+            memory_cached_resources()[0].request_destination);
+  EXPECT_EQ(url::SchemeHostPort(style_url), memory_cached_resources()[1].url);
+  EXPECT_EQ(network::mojom::RequestDestination::kStyle,
+            memory_cached_resources()[1].request_destination);
+  EXPECT_EQ(url::SchemeHostPort(image_url), memory_cached_resources()[2].url);
+  EXPECT_EQ(network::mojom::RequestDestination::kImage,
+            memory_cached_resources()[2].request_destination);
+  EXPECT_EQ(url::SchemeHostPort(font_url), memory_cached_resources()[3].url);
+  EXPECT_EQ(network::mojom::RequestDestination::kFont,
+            memory_cached_resources()[3].request_destination);
+}
+
 TEST_P(MetricsWebContentsObserverTest, RecordFeatureUsage) {
   content::NavigationSimulator::NavigateAndCommitFromBrowser(
       web_contents(), GURL(kDefaultTestUrl));
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.