0

Reland: Inherit service worker controller for srcdoc iframe

This is a reland of http://crrev.com/c/6054691 with changes from
http://crrev.com/c/6081248 for fixing the DCHECK/DwC from the original
change.

Same origin srcdoc iframes should be controlled by the same service
worker that controls the parent frame.

To achieve this, expanded ServiceWorkerClient::InheritControllerFrom to
also support srcdoc iframes and used it to setup service worker client
for srcdoc frames when NavigationRequests for them start.

To allow these clients to show up in clients.matchAll, we update same
origin check in GetWindowClients to use GetLastCommittedOrigin instead
of GetLastCommittedURL from the frames.

As the url for srcdoc iframe is about:srcdoc, when we use origin of that
url for access check, it will fail and hit DCHECK/DwC. So, we switch to
use its creator url, which is captured as url for scope match, when
performing access checks.

As the srcdoc frame test in about-blank-replacement.https.html now
passes, updated expectation to reflect that.

Also added wpt tests to ensure that service worker features work as
expected in srcdoc iframes.

The new behavior is behind the ServiceWorkerSrcdocSupport feature which
is enabled by default.

Bug: 41411856,382895651,382895896
Change-Id: I5d7e96b4838c666842315fce6795a9221e19f61b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6085871
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Commit-Queue: Liang Zhao <lzhao@microsoft.com>
Reviewed-by: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1408026}
This commit is contained in:
Liang Zhao
2025-01-17 10:34:24 -08:00
committed by Chromium LUCI CQ
parent 8a5e6552a9
commit 5b299cef9f
18 changed files with 349 additions and 61 deletions

@ -5867,9 +5867,52 @@ void NavigationRequest::OnWillCommitWithoutUrlLoaderChecksComplete(
// Cases with a UrlLoader are handled in OnStartChecksComplete.
MaybeDispatchNavigateEventForCrossDocumentTraversal();
InheritServiceWorkerControllerFromParentIfNeeded();
CommitNavigation();
}
void NavigationRequest::InheritServiceWorkerControllerFromParentIfNeeded() {
if (!base::FeatureList::IsEnabled(features::kServiceWorkerSrcdocSupport)) {
return;
}
CHECK(!loader_);
CHECK(!service_worker_handle_);
RenderFrameHostImpl* parent = frame_tree_node()->parent();
if (!parent || !GetURL().IsAboutSrcdoc()) {
return;
}
base::WeakPtr<ServiceWorkerClient> parent_service_worker_client =
parent->GetLastCommittedServiceWorkerClient();
if (!parent_service_worker_client) {
return;
}
if ((frame_tree_node_->pending_frame_policy().sandbox_flags &
network::mojom::WebSandboxFlags::kOrigin) ==
network::mojom::WebSandboxFlags::kOrigin) {
return;
}
StoragePartition* partition = GetStoragePartitionWithCurrentSiteInfo();
auto* service_worker_context = static_cast<ServiceWorkerContextWrapper*>(
partition->GetServiceWorkerContext());
// As ServiceWorkerMainResourceHandle is not used for intercepting the srcdoc
// iframe main resource, the fetch event client id is not used. Use empty
// string as fetch_event_client_id when creating it.
service_worker_handle_ = std::make_unique<ServiceWorkerMainResourceHandle>(
service_worker_context, base::DoNothing(),
/*fetch_event_client_id=*/std::string(), parent_service_worker_client);
service_worker_handle_->set_service_worker_client(
service_worker_context->context()
->service_worker_client_owner()
.CreateServiceWorkerClientForWindow(
IsSecureFrame(frame_tree_node_->parent()),
frame_tree_node_->frame_tree_node_id()));
service_worker_handle_->service_worker_client()->InheritControllerFrom(
*parent_service_worker_client, net::SimplifyUrlForRequest(GetURL()));
}
void NavigationRequest::RunCommitDeferringConditions() {
commit_deferrer_->RegisterDeferringConditions(*this);
commit_deferrer_->ProcessChecks();

@ -2148,6 +2148,13 @@ class CONTENT_EXPORT NavigationRequest
// contexts or if DocumentIsolationPolicy is not supported.
void SanitizeDocumentIsolationPolicyHeader();
// Sets up service worker client info to inherit controller from the parent
// frame if it is a same origin srcdoc iframe.
// This method creates a ServiceWorkerClient associated with the navigating
// frame. It should not be called when NavigationURLLoader is used as that
// would also create ServiceWorkerClient and cause conflict.
void InheritServiceWorkerControllerFromParentIfNeeded();
// Never null. The pointee node owns this navigation request instance.
// This field is not a raw_ptr because of incompatibilities with tracing
// (TRACE_EVENT*), perfetto::TracedDictionary::Add and gmock/EXPECT_THAT.

@ -849,7 +849,7 @@ void ServiceWorkerClient::OnEnterBackForwardCache() {
static_cast<int32_t>(GetClientType()));
SCOPED_CRASH_KEY_BOOL("SWC_OnEBFC", "is_execution_ready",
is_execution_ready());
SCOPED_CRASH_KEY_BOOL("SWC_OnEBFC", "is_blob_url",
SCOPED_CRASH_KEY_BOOL("SWC_OnEBFC", "is_blob_or_about_url",
url() != GetUrlForScopeMatch());
SCOPED_CRASH_KEY_BOOL("SWC_OnEBFC", "is_inherited", is_inherited());
CHECK(!controller_->BFCacheContainsControllee(client_uuid()));
@ -864,7 +864,7 @@ void ServiceWorkerClient::OnRestoreFromBackForwardCache() {
// TODO(crbug.com/330928087): remove check when this issue resolved.
SCOPED_CRASH_KEY_BOOL("SWC_OnRFBFC", "is_in_bfcache",
is_in_back_forward_cache_);
SCOPED_CRASH_KEY_BOOL("SWC_OnRFBFC", "is_blob_url",
SCOPED_CRASH_KEY_BOOL("SWC_OnRFBFC", "is_blob_or_about_url",
url() != GetUrlForScopeMatch());
SCOPED_CRASH_KEY_BOOL("SWC_OnRFBFC", "is_inherited", is_inherited());
if (controller_) {
@ -1007,7 +1007,7 @@ void ServiceWorkerClient::UpdateController(bool notify_controllerchange) {
static_cast<int32_t>(GetClientType()));
SCOPED_CRASH_KEY_BOOL("SWV_RCFBCM", "is_execution_ready",
is_execution_ready());
SCOPED_CRASH_KEY_BOOL("SWV_RCFBCM", "is_blob_url",
SCOPED_CRASH_KEY_BOOL("SWV_RCFBCM", "is_blob_or_about_url",
url() != GetUrlForScopeMatch());
SCOPED_CRASH_KEY_BOOL("SWV_RCFBCM", "is_inherited", is_inherited());
CHECK(!version->BFCacheContainsControllee(client_uuid()));
@ -1082,26 +1082,39 @@ void ServiceWorkerClient::CheckControllerConsistency(bool should_crash) const {
#endif // DCHECK_IS_ON()
const GURL& ServiceWorkerClient::GetUrlForScopeMatch() const {
if (!scope_match_url_for_blob_client_.is_empty()) {
return scope_match_url_for_blob_client_;
if (!scope_match_url_for_client_.is_empty()) {
return scope_match_url_for_client_;
}
return url_;
}
void ServiceWorkerClient::InheritControllerFrom(
ServiceWorkerClient& creator_host,
const GURL& blob_url) {
const GURL& client_url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(base::FeatureList::IsEnabled(kSharedWorkerBlobURLFix) ||
blink::mojom::ServiceWorkerClientType::kDedicatedWorker ==
GetClientType());
DCHECK(blob_url.SchemeIsBlob());
DCHECK(GetClientType() ==
blink::mojom::ServiceWorkerClientType::kDedicatedWorker ||
(base::FeatureList::IsEnabled(kSharedWorkerBlobURLFix) &&
GetClientType() ==
blink::mojom::ServiceWorkerClientType::kSharedWorker) ||
(base::FeatureList::IsEnabled(features::kServiceWorkerSrcdocSupport) &&
GetClientType() == blink::mojom::ServiceWorkerClientType::kWindow &&
client_url.IsAboutSrcdoc()));
// Only expect srcdoc url or blob url of same origin as creator for
// client_url.
DCHECK((client_url.SchemeIsBlob() &&
url::Origin::Create(client_url)
.IsSameOriginWith(creator_host.key().origin())) ||
(base::FeatureList::IsEnabled(features::kServiceWorkerSrcdocSupport) &&
client_url.IsAboutSrcdoc()));
UpdateUrls(blob_url, creator_host.top_frame_origin(), creator_host.key());
// Let `scope_match_url_for_blob_client_` be the creator's url for scope match
// Let `scope_match_url_for_client_` be the creator's url for scope match
// because a client should be handled by the service worker of its creator.
scope_match_url_for_blob_client_ = creator_host.GetUrlForScopeMatch();
// Update it before UpdateUrls so that CheckOnUpdateUrls inside UpdateUrls
// checks with the updated GetUrlForScopeMatch().
scope_match_url_for_client_ = creator_host.GetUrlForScopeMatch();
UpdateUrls(client_url, creator_host.top_frame_origin(), creator_host.key());
// Inherit the controller of the creator.
if (creator_host.controller_registration()) {

@ -312,16 +312,16 @@ class CONTENT_EXPORT ServiceWorkerClient final
// For service worker clients. Returns the URL that is used for scope matching
// algorithm. This can be different from url() in the case of blob URL
// workers. In that case, url() may be like "blob://https://a.test" and the
// scope matching URL is "https://a.test", inherited from the parent container
// host.
// workers or srcdoc/about:blank iframes. In that case, url() may be like
// "blob://https://a.test" or "about:srcdoc" and the scope matching URL is
// "https://a.test", inherited from the parent container host.
const GURL& GetUrlForScopeMatch() const;
// For service worker clients that are dedicated workers. Inherits the
// controller of the creator document or worker. Used when the client was
// created with a blob URL.
// For service worker clients that are dedicated workers and srcdoc iframe.
// Inherits the controller of the creator document or worker. Used when the
// client was created with a blob, about:srcdoc or about:blank URL.
void InheritControllerFrom(ServiceWorkerClient& creator_host,
const GURL& blob_url);
const GURL& client_url);
void SetContainerReady();
@ -504,8 +504,9 @@ class CONTENT_EXPORT ServiceWorkerClient final
std::optional<ServiceWorkerClientInfo> client_info_;
// The URL used for service worker scope matching. It is empty except in the
// case of a service worker client with a blob URL.
GURL scope_match_url_for_blob_client_;
// case of a service worker client with a blob, about:blank or about:srcdoc
// URL.
GURL scope_match_url_for_client_;
// Become true if the container is inherited by other container.
bool is_inherited_ = false;

@ -335,6 +335,8 @@ void GetWindowClients(
return;
}
const url::Origin controller_origin =
url::Origin::Create(controller->script_url());
for (const auto& it : clients_info) {
blink::mojom::ServiceWorkerClientInfoPtr info =
GetWindowClientInfo(std::get<0>(it), std::get<1>(it), std::get<2>(it));
@ -347,12 +349,15 @@ void GetWindowClients(
continue;
DCHECK(!info->client_uuid.empty());
// We can get info for a frame that was navigating end ended up with a
// TODO(crbug.com/385901567): Investigate/clarify the intention of this
// check.
// We can get info for a frame that was navigating and ended up with a
// different URL than expected. In such case, we should make sure to not
// expose cross-origin WindowClient.
if (info->url.DeprecatedGetOriginAsURL() !=
controller->script_url().DeprecatedGetOriginAsURL())
auto* rfh = RenderFrameHostImpl::FromID(std::get<0>(it));
if (!controller_origin.IsSameOriginWith(rfh->GetLastCommittedOrigin())) {
continue;
}
clients.push_back(std::move(info));
}

@ -122,7 +122,7 @@ void ServiceWorkerContainerHostForClient::Register(
return;
}
std::vector<GURL> urls = {url(), options->scope, script_url};
std::vector<GURL> urls = {url_for_access_check(), options->scope, script_url};
if (!service_worker_security_utils::AllOriginsMatchAndCanAccessServiceWorkers(
urls)) {
mojo::ReportBadMessage(ServiceWorkerConsts::kBadMessageImproperOrigins);
@ -135,7 +135,8 @@ void ServiceWorkerContainerHostForClient::Register(
}
if (!service_worker_security_utils::
OriginCanRegisterServiceWorkerFromJavascript(url())) {
OriginCanRegisterServiceWorkerFromJavascript(
url_for_access_check())) {
mojo::ReportBadMessage(ServiceWorkerConsts::kBadMessageImproperOrigins);
// ReportBadMessage() will terminate the renderer process, but Mojo
// complains if the callback is not run. Just run it with nonsense
@ -197,7 +198,7 @@ void ServiceWorkerContainerHostForClient::GetRegistration(
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CanServeContainerHostMethods(
&callback, url(), GURL(),
&callback, url_for_access_check(), GURL(),
ServiceWorkerConsts::kServiceWorkerGetRegistrationErrorPrefix,
nullptr)) {
return;
@ -238,7 +239,7 @@ void ServiceWorkerContainerHostForClient::GetRegistrations(
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CanServeContainerHostMethods(
&callback, url(), GURL(),
&callback, url_for_access_check(), GURL(),
ServiceWorkerConsts::kServiceWorkerGetRegistrationsErrorPrefix,
std::nullopt)) {
return;
@ -770,6 +771,11 @@ const GURL& ServiceWorkerContainerHostForClient::url() const {
return service_worker_client().url();
}
const GURL& ServiceWorkerContainerHostForClient::url_for_access_check() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return service_worker_client().GetUrlForScopeMatch();
}
const base::WeakPtr<ServiceWorkerContextCore>&
ServiceWorkerContainerHostForServiceWorker::context() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@ -787,6 +793,12 @@ const GURL& ServiceWorkerContainerHostForServiceWorker::url() const {
return url_;
}
const GURL& ServiceWorkerContainerHostForServiceWorker::url_for_access_check()
const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return url_;
}
ServiceWorkerHost*
ServiceWorkerContainerHostForServiceWorker::service_worker_host() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@ -1027,7 +1039,7 @@ bool ServiceWorkerContainerHostForClient::IsValidGetRegistrationMessage(
*out_error = ServiceWorkerConsts::kBadMessageInvalidURL;
return false;
}
std::vector<GURL> urls = {url(), client_url};
std::vector<GURL> urls = {url_for_access_check(), client_url};
if (!service_worker_security_utils::AllOriginsMatchAndCanAccessServiceWorkers(
urls)) {
*out_error = ServiceWorkerConsts::kBadMessageImproperOrigins;
@ -1044,7 +1056,7 @@ bool ServiceWorkerContainerHostForClient::IsValidGetRegistrationsMessage(
*out_error = ServiceWorkerConsts::kBadMessageFromNonWindow;
return false;
}
if (!OriginCanAccessServiceWorkers(url())) {
if (!OriginCanAccessServiceWorkers(url_for_access_check())) {
*out_error = ServiceWorkerConsts::kBadMessageImproperOrigins;
return false;
}

@ -175,6 +175,11 @@ class CONTENT_EXPORT ServiceWorkerContainerHost
// The URL of this context.
virtual const GURL& url() const = 0;
// The url to use for access check, the same url as the one used for scope
// match. This is needed for srcdoc iframes where url() is "about:srcdoc" and
// url_for_access_check() is the parent client's URL that matches the service
// worker's origin.
virtual const GURL& url_for_access_check() const = 0;
// Calls ContentBrowserClient::AllowServiceWorker(). Returns true if content
// settings allows service workers to run at |scope|. If this container is for
@ -304,6 +309,7 @@ class CONTENT_EXPORT ServiceWorkerContainerHostForClient final
const base::WeakPtr<ServiceWorkerContextCore>& context() const override;
base::WeakPtr<ServiceWorkerContainerHost> AsWeakPtr() override;
const GURL& url() const override;
const GURL& url_for_access_check() const override;
bool AllowServiceWorker(const GURL& scope, const GURL& script_url) override;
void DispatchExtendableMessageEvent(
scoped_refptr<ServiceWorkerVersion> version,
@ -470,6 +476,7 @@ class CONTENT_EXPORT ServiceWorkerContainerHostForServiceWorker final
const base::WeakPtr<ServiceWorkerContextCore>& context() const override;
base::WeakPtr<ServiceWorkerContainerHost> AsWeakPtr() override;
const GURL& url() const override;
const GURL& url_for_access_check() const override;
bool AllowServiceWorker(const GURL& scope, const GURL& script_url) override;
void DispatchExtendableMessageEvent(
scoped_refptr<ServiceWorkerVersion> version,

@ -520,7 +520,8 @@ void ServiceWorkerContextCore::OnClientDestroyed(
service_worker_client.container_host()
? service_worker_client.container_host()->ukm_source_id()
: ukm::kInvalidSourceId,
service_worker_client.url(), service_worker_client.GetClientType());
service_worker_client.GetUrlForScopeMatch(),
service_worker_client.GetClientType());
}
void ServiceWorkerClientOwner::DestroyServiceWorkerClient(
@ -698,7 +699,8 @@ void ServiceWorkerContextCore::NotifyClientIsExecutionReady(
observer_list_->Notify(
FROM_HERE, &ServiceWorkerContextCoreObserver::OnClientIsExecutionReady,
service_worker_client.container_host()->ukm_source_id(),
service_worker_client.url(), service_worker_client.GetClientType());
service_worker_client.GetUrlForScopeMatch(),
service_worker_client.GetClientType());
}
bool ServiceWorkerContextCore::MaybeHasRegistrationForStorageKey(

@ -21,7 +21,8 @@ ServiceWorkerObjectHost::ServiceWorkerObjectHost(
scoped_refptr<ServiceWorkerVersion> version)
: context_(context),
container_host_(container_host),
container_origin_(url::Origin::Create(container_host_->url())),
container_origin_(
url::Origin::Create(container_host_->url_for_access_check())),
version_(std::move(version)) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(context_ && container_host_ && version_);
@ -88,7 +89,8 @@ void ServiceWorkerObjectHost::DispatchExtendableMessageEvent(
std::move(callback).Run(blink::ServiceWorkerStatusCode::kErrorAbort);
return;
}
DCHECK_EQ(container_origin_, url::Origin::Create(container_host_->url()));
DCHECK_EQ(container_origin_,
url::Origin::Create(container_host_->url_for_access_check()));
// As we don't track tasks between workers and renderers, we can nullify the
// message's parent task ID.

@ -347,11 +347,12 @@ bool ServiceWorkerRegistrationObjectHost::CanServeRegistrationObjectHostMethods(
return false;
}
std::vector<GURL> urls = {container_host_->url(), registration_->scope()};
std::vector<GURL> urls = {container_host_->url_for_access_check(),
registration_->scope()};
if (!service_worker_security_utils::AllOriginsMatchAndCanAccessServiceWorkers(
urls)) {
SCOPED_CRASH_KEY_STRING256("SWROH_CSROHM", "host_url",
container_host_->url().spec());
container_host_->url_for_access_check().spec());
SCOPED_CRASH_KEY_STRING256("SWROH_CSROHM", "reg_scope",
registration_->scope().spec());
receivers_.ReportBadMessage(

@ -1695,7 +1695,7 @@ void ServiceWorkerVersion::GetClient(const std::string& client_uuid,
context_->service_worker_client_owner().GetServiceWorkerClientByClientID(
client_uuid);
if (!service_worker_client ||
service_worker_client->url().DeprecatedGetOriginAsURL() !=
service_worker_client->GetUrlForScopeMatch().DeprecatedGetOriginAsURL() !=
script_url_.DeprecatedGetOriginAsURL()) {
// The promise will be resolved to 'undefined'.
// Note that we don't BadMessage here since Clients#get() can be passed an
@ -1795,7 +1795,7 @@ void ServiceWorkerVersion::PostMessageToClient(
}
}
if (service_worker_client->url().DeprecatedGetOriginAsURL() !=
if (service_worker_client->GetUrlForScopeMatch().DeprecatedGetOriginAsURL() !=
script_url_.DeprecatedGetOriginAsURL()) {
associated_interface_receiver_.ReportBadMessage(
"Received Client#postMessage() request for a cross-origin client.");
@ -1851,7 +1851,7 @@ void ServiceWorkerVersion::FocusClient(const std::string& client_uuid,
std::move(callback).Run(nullptr /* client */);
return;
}
if (service_worker_client->url().DeprecatedGetOriginAsURL() !=
if (service_worker_client->GetUrlForScopeMatch().DeprecatedGetOriginAsURL() !=
script_url_.DeprecatedGetOriginAsURL()) {
associated_interface_receiver_.ReportBadMessage(
"Received WindowClient#focus() request for a cross-origin client.");
@ -1908,7 +1908,7 @@ void ServiceWorkerVersion::NavigateClient(const std::string& client_uuid,
std::string("The client was not found."));
return;
}
if (service_worker_client->url().DeprecatedGetOriginAsURL() !=
if (service_worker_client->GetUrlForScopeMatch().DeprecatedGetOriginAsURL() !=
script_url_.DeprecatedGetOriginAsURL()) {
associated_interface_receiver_.ReportBadMessage(
"Received WindowClient#navigate() request for a cross-origin client.");

@ -379,20 +379,6 @@ BASE_FEATURE(kServiceWorkerAvoidMainThreadForInitialization,
#endif
);
// (crbug.com/1371756): When enabled, the static routing API starts
// ServiceWorker when the routing result of a main resource request was network
// fallback.
BASE_FEATURE(kServiceWorkerStaticRouterStartServiceWorker,
"ServiceWorkerStaticRouterStartServiceWorker",
base::FEATURE_ENABLED_BY_DEFAULT);
// (crbug.com/340949948): Killswitch for the fix to address the ServiceWorker
// main and subreosurce loader lifetime issue, which introduces fetch() failure
// in the sw fetch handler.
BASE_FEATURE(kServiceWorkerStaticRouterRaceRequestFix,
"kServiceWorkerStaticRouterRaceRequestFix",
base::FEATURE_ENABLED_BY_DEFAULT);
// The set of ServiceWorker to bypass while making navigation request.
// They are represented by a comma separated list of HEX encoded SHA256 hash of
// the ServiceWorker's scripts.
@ -407,6 +393,26 @@ const base::FeatureParam<std::string>
&kServiceWorkerBypassFetchHandlerHashStrings,
"script_checksum_to_bypass", ""};
// (crbug.com/41411856): When enabled, the srcdoc iframes are controlled by the
// same service worker that controls their parent.
BASE_FEATURE(kServiceWorkerSrcdocSupport,
"ServiceWorkerSrcdocSupport",
base::FEATURE_DISABLED_BY_DEFAULT);
// (crbug.com/340949948): Killswitch for the fix to address the ServiceWorker
// main and subreosurce loader lifetime issue, which introduces fetch() failure
// in the sw fetch handler.
BASE_FEATURE(kServiceWorkerStaticRouterRaceRequestFix,
"kServiceWorkerStaticRouterRaceRequestFix",
base::FEATURE_ENABLED_BY_DEFAULT);
// (crbug.com/1371756): When enabled, the static routing API starts
// ServiceWorker when the routing result of a main resource request was network
// fallback.
BASE_FEATURE(kServiceWorkerStaticRouterStartServiceWorker,
"ServiceWorkerStaticRouterStartServiceWorker",
base::FEATURE_ENABLED_BY_DEFAULT);
// Enables skipping the early call to CommitPending when navigating away from a
// crashed frame.
BASE_FEATURE(kSkipEarlyCommitPendingForCrashedFrame,

@ -88,13 +88,14 @@ CONTENT_EXPORT BASE_DECLARE_FEATURE(kRemoveRendererProcessLimit);
CONTENT_EXPORT BASE_DECLARE_FEATURE(kSendBeaconThrowForBlobWithNonSimpleType);
CONTENT_EXPORT BASE_DECLARE_FEATURE(
kServiceWorkerAvoidMainThreadForInitialization);
CONTENT_EXPORT BASE_DECLARE_FEATURE(
kServiceWorkerStaticRouterStartServiceWorker);
CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerStaticRouterRaceRequestFix);
CONTENT_EXPORT BASE_DECLARE_FEATURE(
kServiceWorkerBypassFetchHandlerHashStrings);
CONTENT_EXPORT extern const base::FeatureParam<std::string>
kServiceWorkerBypassFetchHandlerBypassedHashStrings;
CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerSrcdocSupport);
CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerStaticRouterRaceRequestFix);
CONTENT_EXPORT BASE_DECLARE_FEATURE(
kServiceWorkerStaticRouterStartServiceWorker);
CONTENT_EXPORT BASE_DECLARE_FEATURE(kSkipEarlyCommitPendingForCrashedFrame);
#if BUILDFLAG(IS_MAC)
CONTENT_EXPORT BASE_DECLARE_FEATURE(kTextInputClient);

@ -21965,6 +21965,27 @@
]
}
],
"ServiceWorkerSrcdocSupport": [
{
"platforms": [
"android",
"android_webview",
"chromeos",
"fuchsia",
"linux",
"mac",
"windows"
],
"experiments": [
{
"name": "Enabled",
"enable_features": [
"ServiceWorkerSrcdocSupport"
]
}
]
}
],
"ServiceWorkerStaticRouterRaceNetworkRequestPerformanceImprovement": [
{
"platforms": [

@ -7,8 +7,6 @@ This is a testharness.js-based test.
assert_false: result: failure: could not find about:blank client expected false got true
[FAIL] Simple about:blank is controlled and is exposed to clients.matchAll().
promise_test: Unhandled rejection with value: object "TypeError: Cannot read properties of null (reading 'scriptURL')"
[FAIL] Nested about:srcdoc is controlled and is exposed to clients.matchAll().
promise_test: Unhandled rejection with value: object "TypeError: Cannot read properties of null (reading 'scriptURL')"
[FAIL] Dynamic about:blank is controlled and is exposed to clients.matchAll().
promise_test: Unhandled rejection with value: object "TypeError: Cannot read properties of null (reading 'scriptURL')"
Harness: the test ran to completion.

@ -0,0 +1,11 @@
self.addEventListener('message', event => {
event.source.postMessage('passed');
});
self.addEventListener('fetch', event => {
let url = new URL(event.request.url);
if (!url.searchParams.get('test_resource')) {
return;
}
event.respondWith(new Response('passed'));
});

@ -0,0 +1,92 @@
<!doctype html>
<html>
<body>
<iframe id="srcdocFrame" srcdoc="
<script>
function reportTestResult(result) {
top.postMessage({ type: 'TEST_RESULT', result: result }, '*');
}
async function postMessageToController() {
let controller = navigator.serviceWorker.controller;
if (!controller) {
reportTestResult('no navigator.serviceWorker.controller');
}
try {
controller.postMessage('test');
} catch (e) {
reportTestResult('Unexpected Error ' + e.name + ' : ' + e.message);
}
}
async function getServiceWorkerRegistration(scope) {
try {
let reg = await navigator.serviceWorker.getRegistration(scope);
if (!reg) {
reportTestResult('no regsitration for ' + scope);
}
if (reg.scope !== scope) {
reportTestResult('regsitration scope does not match');
}
if (!reg.active) {
reportTestResult('regsitration.active is not valid' + reg.active);
}
reportTestResult('passed');
} catch (e) {
reportTestResult('Unexpected Error ' + e.name + ' : ' + e.message);
}
}
function addSrcdocIframeWithSandbox(sandbox) {
let frame = document.createElement('iframe');
frame.sandbox = sandbox;
frame.srcdoc = `
<script>
function reportTestResult(result) {
top.postMessage({ type: 'TEST_RESULT', result: result }, '*');
}
window.onload = function onLoad() {
try {
let controller = navigator.serviceWorker.controller;
reportTestResult(controller ? 'HasController' : 'NoController');
} catch (e) {
reportTestResult((e.name == 'SecurityError') ?
'NoController' : 'UnexpectedError:' + e.message);
}
}
<\/script>`;
document.body.appendChild(frame);
}
function addSandboxedSrcdocFrame() {
addSrcdocIframeWithSandbox('allow-scripts');
}
function addSameOriginSandboxedSrcdocFrame() {
addSrcdocIframeWithSandbox('allow-scripts allow-same-origin');
}
async function fetchResource() {
let response = await fetch('?test_resource=1');
reportTestResult(await response.text());
}
window.navigator.serviceWorker.addEventListener(
'message', function onMsg(evt) {
// Forward the message as test result.
reportTestResult(evt.data);
});
</script>
"></iframe>
<script>
// Helper routine to make it slightly easier for our parent to find
// the srcdoc frame.
function srcdocFrame() {
return document.getElementById('srcdocFrame').contentWindow;
}
</script>
</body>
</html>

@ -0,0 +1,66 @@
<!DOCTYPE html>
<title>Service Worker: srcdoc frame handling</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/test-helpers.sub.js"></script>
<body>
<script>
// This test attempts to verify various service worker behaviors in
// srcdoc iframe.
function runTestInSrcDocFrame(testFunc, testFuncParam) {
return new Promise((resolve, reject) => {
window.addEventListener('message', function onMsg(evt) {
if (evt.data.type !== 'TEST_RESULT') {
return;
}
window.removeEventListener('message', onMsg);
resolve(evt.data.result);
});
testFunc(testFuncParam);
});
}
async function doAsyncTest(t, testFuncName, expectedResult) {
const worker = 'resources/srcdoc-iframe-worker.js';
const scope = 'resources/srcdoc-iframe.html';
let reg = await service_worker_unregister_and_register(t, worker, scope);
t.add_cleanup(() => service_worker_unregister(t, scope));
await wait_for_state(t, reg.installing, 'activated');
let frame = await with_iframe(scope);
let scopeFullUlr = frame.contentWindow.location.href;
let testFunc = frame.contentWindow.srcdocFrame()[testFuncName];
let testResult = await runTestInSrcDocFrame(testFunc, scopeFullUlr);
assert_equals(testResult, expectedResult);
frame.remove();
}
promise_test(async function(t) {
await doAsyncTest(t, 'addSandboxedSrcdocFrame', 'NoController');
}, 'nested sandboxed srcdoc frame should not inherit controller');
promise_test(async function(t) {
await doAsyncTest(t, 'addSameOriginSandboxedSrcdocFrame', 'HasController');
}, 'nested same origin sandboxed srcdoc frame should inherit controller');
promise_test(async function(t) {
await doAsyncTest(t, 'fetchResource', 'passed');
}, 'should be able to serve resource from controller for srcdoc frame');
promise_test(async function(t) {
await doAsyncTest(t, 'getServiceWorkerRegistration', 'passed');
}, 'getRegistration should work in srcdoc iframe');
promise_test(async function(t) {
await doAsyncTest(t, 'postMessageToController', 'passed');
}, 'controller.postMessage should work in srcdoc iframe');
</script>
</body>