0

Obtain and store per-origin salts in the VisitedLinkReader

This CL obtains the per-origin salts sent via navigation commit_params, routes them to the correct VisitedLinkState, and stores them in the correct process' VisitedLinkReader. All per-origin salts for a given process can be found in the VisitedLinkReader's <origin,salt> map. These salts are necessary for hashing and determining if a link is :visited.

Context CL: https://chromium-review.googlesource.com/c/chromium/src/+/5353594
Design Doc: https://docs.google.com/document/d/19qYPkfPNkPnmVcZ6jjl7jR8JPKY6j4rTjfoFRHll2IQ/edit?tab=t.0

Change-Id: I7fd089ea3fdf752644b0bf1b0339ca147620c7ff
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5540573
Commit-Queue: Kyra Seevers <kyraseevers@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1309904}
This commit is contained in:
Kyra
2024-06-04 13:25:26 +00:00
committed by Chromium LUCI CQ
parent af40077ad0
commit de57137aec
19 changed files with 186 additions and 7 deletions

@ -228,6 +228,13 @@ bool AwContentRendererClient::IsLinkVisited(uint64_t link_hash) {
return visited_link_reader_->IsVisited(link_hash);
}
// Android WebView does not support partitioned :visited links. Since per-origin
// salts are only used in the partitioned hashtable, AndroidWebView clients do
// not need to take any action if a per-origin salt is received.
void AwContentRendererClient::AddOrUpdateVisitedLinkSalt(
const url::Origin& origin,
uint64_t salt) {}
void AwContentRendererClient::RunScriptsAtDocumentStart(
content::RenderFrame* render_frame) {
js_injection::JsCommunication* communication =

@ -54,6 +54,8 @@ class AwContentRendererClient : public content::ContentRendererClient,
std::string* error_html) override;
uint64_t VisitedLinkHash(std::string_view canonical_url) override;
bool IsLinkVisited(uint64_t link_hash) override;
void AddOrUpdateVisitedLinkSalt(const url::Origin& origin,
uint64_t salt) override;
void RunScriptsAtDocumentStart(content::RenderFrame* render_frame) override;
std::unique_ptr<media::KeySystemSupportRegistration> GetSupportedKeySystems(
content::RenderFrame* render_frame,

@ -1488,6 +1488,12 @@ bool ChromeContentRendererClient::IsLinkVisited(uint64_t link_hash) {
return chrome_observer_->visited_link_reader()->IsVisited(link_hash);
}
void ChromeContentRendererClient::AddOrUpdateVisitedLinkSalt(
const url::Origin& origin,
uint64_t salt) {
return chrome_observer_->visited_link_reader()->AddOrUpdateSalt(origin, salt);
}
std::unique_ptr<blink::WebPrescientNetworking>
ChromeContentRendererClient::CreatePrescientNetworking(
content::RenderFrame* render_frame) {

@ -156,6 +156,8 @@ class ChromeContentRendererClient
bool IsPrefetchOnly(content::RenderFrame* render_frame) override;
uint64_t VisitedLinkHash(std::string_view canonical_url) override;
bool IsLinkVisited(uint64_t link_hash) override;
void AddOrUpdateVisitedLinkSalt(const url::Origin& origin,
uint64_t salt) override;
std::unique_ptr<blink::WebPrescientNetworking> CreatePrescientNetworking(
content::RenderFrame* render_frame) override;
bool IsExternalPepperPlugin(const std::string& module_name) override;

@ -32,6 +32,13 @@ class VisitedLinkReader : public VisitedLinkCommon,
void(mojo::PendingReceiver<mojom::VisitedLinkNotificationSink>)>
GetBindCallback();
// Mutator for `salts_`. Called by Documents which received
// per-origin salts from their navigation commit params. This function may
// only be called on the UI (main) thread.
void AddOrUpdateSalt(const url::Origin& origin, uint64_t salt) {
salts_[origin] = salt;
}
// mojom::VisitedLinkNotificationSink overrides.
void UpdateVisitedLinks(
base::ReadOnlySharedMemoryRegion table_region) override;
@ -52,6 +59,12 @@ class VisitedLinkReader : public VisitedLinkCommon,
void Bind(mojo::PendingReceiver<mojom::VisitedLinkNotificationSink> receiver);
// Contains the per-origin salts required to hash every :visited link relevant
// to this RenderProcess. Queries to the hashtable stored within
// `table_mapping_` MUST provide the salt that corresponds to that link's
// origin, otherwise :visited status cannot be determined.
std::map<url::Origin, uint64_t> salts_;
base::ReadOnlySharedMemoryMapping table_mapping_;
mojo::Receiver<mojom::VisitedLinkNotificationSink> receiver_{this};

@ -73,6 +73,15 @@ bool PpapiBlinkPlatformImpl::IsLinkVisited(uint64_t link_hash) {
return false;
}
// PPAPI does not support partitioned :visited links. Since per-origin
// salts are only used in the partitioned :visited link hashtable, PPAPI clients
// do not need to take any action if a per-origin salt is received.
void PpapiBlinkPlatformImpl::AddOrUpdateVisitedLinkSalt(
const url::Origin& origin,
uint64_t salt) {
NOTREACHED();
}
blink::WebString PpapiBlinkPlatformImpl::DefaultLocale() {
return blink::WebString::FromUTF8("en");
}

@ -31,6 +31,8 @@ class PpapiBlinkPlatformImpl : public BlinkPlatformImpl {
blink::WebSandboxSupport* GetSandboxSupport() override;
uint64_t VisitedLinkHash(std::string_view canonical_url) override;
bool IsLinkVisited(uint64_t link_hash) override;
void AddOrUpdateVisitedLinkSalt(const url::Origin& origin,
uint64_t salt) override;
blink::WebString DefaultLocale() override;
private:

@ -167,6 +167,10 @@ bool ContentRendererClient::IsLinkVisited(uint64_t link_hash) {
return false;
}
void ContentRendererClient::AddOrUpdateVisitedLinkSalt(
const url::Origin& origin,
uint64_t salt) {}
std::unique_ptr<blink::WebPrescientNetworking>
ContentRendererClient::CreatePrescientNetworking(RenderFrame* render_frame) {
return nullptr;

@ -270,6 +270,8 @@ class CONTENT_EXPORT ContentRendererClient {
// See blink::Platform.
virtual uint64_t VisitedLinkHash(std::string_view canonical_url);
virtual bool IsLinkVisited(uint64_t link_hash);
virtual void AddOrUpdateVisitedLinkSalt(const url::Origin& origin,
uint64_t salt);
// Creates a WebPrescientNetworking instance for |render_frame|. The returned
// instance is owned by the frame. May return null.

@ -2968,6 +2968,7 @@ void RenderFrameImpl::CommitNavigationWithParams(
navigation_params->load_with_storage_access =
commit_params->load_with_storage_access;
navigation_params->visited_link_salt = commit_params->visited_link_salt;
if (!container_info) {
// An empty network provider will always be created since it is expected in

@ -290,6 +290,12 @@ bool RendererBlinkPlatformImpl::IsLinkVisited(uint64_t link_hash) {
return GetContentClient()->renderer()->IsLinkVisited(link_hash);
}
void RendererBlinkPlatformImpl::AddOrUpdateVisitedLinkSalt(
const url::Origin& origin,
uint64_t salt) {
GetContentClient()->renderer()->AddOrUpdateVisitedLinkSalt(origin, salt);
}
blink::WebString RendererBlinkPlatformImpl::UserAgent() {
auto* render_thread = RenderThreadImpl::current();
// RenderThreadImpl is null in some tests.

@ -81,6 +81,8 @@ class CONTENT_EXPORT RendererBlinkPlatformImpl : public BlinkPlatformImpl {
virtual bool sandboxEnabled();
uint64_t VisitedLinkHash(std::string_view canonical_url) override;
bool IsLinkVisited(uint64_t linkHash) override;
void AddOrUpdateVisitedLinkSalt(const url::Origin& origin,
uint64_t salt) override;
blink::WebString UserAgent() override;
blink::UserAgentMetadata UserAgentMetadata() override;
bool IsRedirectSafe(const GURL& from_url, const GURL& to_url) override;

@ -229,6 +229,13 @@ class BLINK_PLATFORM_EXPORT Platform {
// hash must have been generated by calling VisitedLinkHash().
virtual bool IsLinkVisited(uint64_t link_hash) { return false; }
// Each render process has an associated VisitedLinkReader, where all
// per-origin salts for the process are stored. Documents that receive a salt
// via the VisitedLinkNavigationThrottle should notify the VisitedLinkReader
// so its value can be stored in its canonical `salts_` map.
virtual void AddOrUpdateVisitedLinkSalt(const url::Origin& origin,
uint64_t salt) {}
// Keep it the same as ImageDecoder::kNoDecodedImageByteLimit
static const size_t kNoDecodedImageByteLimit = static_cast<size_t>(-1);

@ -551,6 +551,22 @@ struct BLINK_EXPORT WebNavigationParams {
// The cookie deprecation label for cookie deprecation facilitated testing.
WebString cookie_deprecation_label;
// The :visited link hashtable is stored in shared memory and contains salted
// hashes for all visits. Each salt corresponds to a unique origin, and
// renderer processes are only informed of salts that correspond to their
// origins. As a result, any given renderer process can only
// learn about visits relevant to origins for which it has the salt.
//
// Here we store the salt corresponding to this navigation's origin to
// be committed. It will allow the renderer process that commits this
// navigation to learn about visits hashed with this salt. If the :visited
// link hashtable is not yet initialized (or the feature is disabled), the
// salt value will not be set here. Instead, PartitionedVisitedLinkWriter will
// send the salt values to the renderer (specifically to VisitedLinkReader via
// the VisitedLinkNotificationSink interface) after the :visited link
// hashtable is initialized.
std::optional<uint64_t> visited_link_salt;
};
} // namespace blink

@ -31,8 +31,11 @@
#include "third_party/blink/renderer/core/dom/visited_link_state.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/execution_context/security_context.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_anchor_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/svg/svg_element.h"
@ -109,6 +112,21 @@ void VisitedLinkState::InvalidateStyleForLink(LinkHash link_hash) {
InvalidateStyleForLinkRecursively(*GetDocument().firstChild(), link_hash);
}
void VisitedLinkState::UpdateSalt(uint64_t visited_link_salt) {
// Obtain the SecurityOrigin for our Document.
// NOTE: for all Documents which have a valid VisitedLinkState, we should not
// ever encounter an invalid `frame`, `context`, or `security_origin`.
const LocalFrame* frame = GetDocument().GetFrame();
DCHECK(frame);
const SecurityContext* context = frame->GetSecurityContext();
DCHECK(context);
const SecurityOrigin* security_origin = context->GetSecurityOrigin();
DCHECK(security_origin);
// Inform VisitedLinkReader in our corresponding process of new salt value.
Platform::Current()->AddOrUpdateVisitedLinkSalt(
security_origin->ToUrlOrigin(), visited_link_salt);
}
EInsideLink VisitedLinkState::DetermineLinkStateSlowCase(
const Element& element) {
DCHECK(element.IsLink());

@ -47,6 +47,11 @@ class VisitedLinkState final : public GarbageCollected<VisitedLinkState> {
void InvalidateStyleForAllLinks(bool invalidate_visited_link_hashes);
void InvalidateStyleForLink(LinkHash);
// Allows Documents to set or update the per-origin salt associated with their
// VisitedLinkState. This function informs the VisitedLinkReader in the
// corresponding process of the new salt value.
void UpdateSalt(uint64_t visited_link_salt);
EInsideLink DetermineLinkState(const Element& element) {
if (element.IsLink())
return DetermineLinkStateSlowCase(element);

@ -81,6 +81,7 @@
#include "third_party/blink/renderer/core/dom/document_parser.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/scriptable_document_parser.h"
#include "third_party/blink/renderer/core/dom/visited_link_state.h"
#include "third_party/blink/renderer/core/dom/weak_identifier_map.h"
#include "third_party/blink/renderer/core/execution_context/agent.h"
#include "third_party/blink/renderer/core/execution_context/security_context_init.h"
@ -257,6 +258,7 @@ struct SameSizeAsDocumentLoader
bool grant_load_local_resources;
std::optional<blink::mojom::FetchCacheMode> force_fetch_cache_mode;
FramePolicy frame_policy;
std::optional<uint64_t> visited_link_salt;
Member<LocalFrame> frame;
Member<HistoryItem> history_item;
Member<DocumentParser> parser;
@ -494,6 +496,7 @@ DocumentLoader::DocumentLoader(
grant_load_local_resources_(params_->grant_load_local_resources),
force_fetch_cache_mode_(params_->force_fetch_cache_mode),
frame_policy_(params_->frame_policy.value_or(FramePolicy())),
visited_link_salt_(params_->visited_link_salt),
frame_(frame),
// For back/forward navigations, the browser passed a history item to use
// at commit time in |params_|. Set it as the current history item of this
@ -717,6 +720,7 @@ DocumentLoader::CreateWebNavigationParamsToCloneDocument() {
params->load_with_storage_access = load_with_storage_access_;
params->modified_runtime_features = modified_runtime_features_;
params->cookie_deprecation_label = cookie_deprecation_label_;
params->visited_link_salt = visited_link_salt_;
params->content_settings = content_settings_->Clone();
return params;
}
@ -2842,6 +2846,16 @@ void DocumentLoader::CommitNavigation() {
document->SetBaseURLOverride(main_resource_url);
}
// For any navigations which have a per-origin salt, we need to notify the
// resulting `document`. The `visited_link_salt_` allows the `document` to
// hash and identify which links should be styled as :visited. Without the
// salt, the hashtable is unreadable to the Document.
if (visited_link_salt_.has_value() &&
base::FeatureList::IsEnabled(
blink::features::kPartitionVisitedLinkDatabase)) {
document->GetVisitedLinkState().UpdateSalt(visited_link_salt_.value());
}
// The navigation API is not initialized on the initial about:blank document
// or opaque-origin documents.
if (commit_reason_ != CommitReason::kInitialization &&

@ -659,6 +659,7 @@ class CORE_EXPORT DocumentLoader : public GarbageCollected<DocumentLoader>,
const bool grant_load_local_resources_ = false;
const std::optional<blink::mojom::FetchCacheMode> force_fetch_cache_mode_;
const FramePolicy frame_policy_;
std::optional<uint64_t> visited_link_salt_;
Member<LocalFrame> frame_;

@ -26,6 +26,7 @@
#include "third_party/blink/renderer/platform/loader/fetch/url_loader/url_loader_client.h"
#include "third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.h"
#include "third_party/blink/renderer/platform/storage/blink_storage_key.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/url_loader_mock_factory.h"
#include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
@ -101,15 +102,47 @@ class BodyLoaderTestDelegate : public URLLoaderTestDelegate {
StaticDataNavigationBodyLoader* body_loader_raw_;
};
// To test the abiltity to obtain and store the per-origin salt used in
// partitioning visited links, we need to override the Platform::Current() used
// in this test. Our platform will obtain and store the per-origin salt values
// locally in `salts_`.
class VisitedLinkSaltPlatform : public TestingPlatformSupport {
public:
// Override which stores our per-origin salts locally.
void AddOrUpdateVisitedLinkSalt(const url::Origin& origin,
uint64_t salt) override {
salts_[origin] = salt;
}
// Test cases can query whether we obtained a salt for a specific origin.
std::optional<uint64_t> GetVisitedLinkSaltForOrigin(
const url::Origin& origin) {
auto it = salts_.find(origin);
if (it != salts_.end()) {
return it->second;
}
// We do not have a corresponding salt for this origin.
return std::nullopt;
}
private:
std::map<url::Origin, uint64_t> salts_;
};
class DocumentLoaderTest : public testing::TestWithParam<bool> {
protected:
void SetUp() override {
if (IsThirdPartyStoragePartitioningEnabled()) {
scoped_feature_list_.InitAndEnableFeature(
net::features::kThirdPartyStoragePartitioning);
if (GetParam()) {
// Enabled the features.
scoped_feature_list_.InitWithFeatures(
{net::features::kThirdPartyStoragePartitioning,
blink::features::kPartitionVisitedLinkDatabase},
{});
} else {
scoped_feature_list_.InitAndDisableFeature(
net::features::kThirdPartyStoragePartitioning);
// Disables the features.
scoped_feature_list_.InitWithFeatures(
{}, {net::features::kThirdPartyStoragePartitioning,
blink::features::kPartitionVisitedLinkDatabase});
}
web_view_helper_.Initialize();
@ -152,8 +185,6 @@ class DocumentLoaderTest : public testing::TestWithParam<bool> {
url_test_helpers::UnregisterAllURLsAndClearMemoryCache();
}
bool IsThirdPartyStoragePartitioningEnabled() const { return GetParam(); }
class ScopedLoaderDelegate {
public:
explicit ScopedLoaderDelegate(URLLoaderTestDelegate* delegate) {
@ -164,6 +195,7 @@ class DocumentLoaderTest : public testing::TestWithParam<bool> {
WebLocalFrameImpl* MainFrame() { return web_view_helper_.LocalMainFrame(); }
ScopedTestingPlatformSupport<VisitedLinkSaltPlatform> platform_;
test::TaskEnvironment task_environment_;
frame_test_helpers::WebViewHelper web_view_helper_;
base::test::ScopedFeatureList scoped_feature_list_;
@ -850,5 +882,35 @@ TEST_P(DocumentLoaderTest, EmbeddedCredentialsNavigation) {
}
}
TEST_P(DocumentLoaderTest, VisitedLinkSalt) {
// Generate the constants.
const uint64_t kSalt = base::RandUint64();
const KURL& kUrl = KURL(NullURL(), "https://www.example.com/foo.html");
// Load a blank slate.
WebViewImpl* web_view_impl =
web_view_helper_.InitializeAndLoad("about:blank");
// Create params for the URL we will navigate to next.
std::unique_ptr<WebNavigationParams> params =
WebNavigationParams::CreateWithEmptyHTMLForTesting(kUrl);
params->visited_link_salt = kSalt;
// Perform the navigation and provide an empty vector for visited link state.
LocalFrame* local_frame =
To<LocalFrame>(web_view_impl->GetPage()->MainFrame());
local_frame->Loader().CommitNavigation(std::move(params), nullptr);
// Check if the platform was notified of our salt.
std::optional<uint64_t> result_salt =
platform_->GetVisitedLinkSaltForOrigin(url::Origin::Create(GURL(kUrl)));
ASSERT_EQ(result_salt.has_value(),
base::FeatureList::IsEnabled(
blink::features::kPartitionVisitedLinkDatabase));
if (result_salt.has_value()) {
EXPECT_EQ(result_salt.value(), kSalt);
}
}
} // namespace
} // namespace blink