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 <[email protected]> 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 <[email protected]> Commit-Queue: Keita Suzuki <[email protected]> Reviewed-by: Takashi Toyoshima <[email protected]> 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));