CVE-2026-87429
Overview
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
WebContentsObservercallback that fires when a subresource is served from that in-renderer memory cache, carrying theurl,mime_type, andnetwork::mojom::RequestDestination. - `MemoryResourceLoadInfo`
- the new struct added by the patch that packages a memory-cache load’s
url(url::SchemeHostPort),request_destination, andframe_tree_node_idfor 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.
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
- 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-rendererMemoryCache. - Re-request the cached subresource
A subsequent request for that resource is satisfied entirely from the in-renderer cache, firing
WebContentsObserver::DidLoadResourceFromMemoryCachebut neverOnLoadedResource(). - Observer never sees the load
Because
MetricsWebContentsObserverdid not overrideDidLoadResourceFromMemoryCache,PageLoadTrackerand itsPageLoadMetricsObservers 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). - 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
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
| Function | Change | Notes |
|---|---|---|
ifcomponents/page_load_metrics/browser/page_load_metrics_forward_observer.cc |
modified | |
forcomponents/page_load_metrics/browser/page_load_tracker.cc |
modified | |
TEST_Pcomponents/page_load_metrics/browser/metrics_web_contents_observer_unittest.cc |
modified |
Files Changed
components/page_load_metrics/browser/metrics_web_contents_observer.hcomponents/page_load_metrics/browser/page_load_metrics_observer.hcomponents/page_load_metrics/browser/page_load_metrics_forward_observer.hcomponents/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.cccomponents/page_load_metrics/browser/observers/assert_page_load_metrics_observer.hcomponents/page_load_metrics/browser/page_load_tracker.hcomponents/page_load_metrics/browser/metrics_web_contents_observer.cccomponents/page_load_metrics/browser/page_load_metrics_observer_interface.hcomponents/page_load_metrics/browser/page_load_tracker.cccomponents/page_load_metrics/browser/metrics_web_contents_observer_unittest.cccomponents/page_load_metrics/browser/test_metrics_web_contents_observer_embedder.hcomponents/page_load_metrics/browser/page_load_metrics_forward_observer.cc
Audit Directions
- Parallel notification pathsWherever 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 hooksAudit 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 instrumentationReview other cache layers (HTTP cache, bfcache, ServiceWorker
CacheStorage) for load types that satisfy requests without emitting the standard resource-load notifications observers rely on.
Patch
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)
Regression Test / PoC
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));