| // Copyright 2021 The Chromium Authors | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | #include "base/files/file_path.h" | 
 | #include "base/strings/stringprintf.h" | 
 | #include "base/strings/to_string.h" | 
 | #include "chrome/browser/extensions/extension_browsertest.h" | 
 | #include "chrome/browser/profiles/profile.h" | 
 | #include "content/public/browser/render_frame_host.h" | 
 | #include "content/public/browser/render_process_host.h" | 
 | #include "content/public/browser/web_contents.h" | 
 | #include "content/public/test/browser_test.h" | 
 | #include "content/public/test/browser_test_utils.h" | 
 | #include "content/public/test/test_navigation_observer.h" | 
 | #include "extensions/browser/extension_host.h" | 
 | #include "extensions/browser/process_manager.h" | 
 | #include "extensions/browser/process_map.h" | 
 | #include "extensions/buildflags/buildflags.h" | 
 | #include "extensions/common/constants.h" | 
 | #include "extensions/common/features/feature.h" | 
 | #include "extensions/common/mojom/context_type.mojom.h" | 
 | #include "extensions/test/extension_test_message_listener.h" | 
 | #include "extensions/test/result_catcher.h" | 
 | #include "extensions/test/test_extension_dir.h" | 
 | #include "net/dns/mock_host_resolver.h" | 
 | #include "testing/gmock/include/gmock/gmock.h" | 
 |  | 
 | #if BUILDFLAG(ENABLE_EXTENSIONS) | 
 | #include "chrome/browser/ui/browser.h" | 
 | #include "chrome/test/base/ui_test_utils.h" | 
 | #endif | 
 |  | 
 | static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); | 
 |  | 
 | namespace extensions { | 
 | namespace { | 
 |  | 
 | using net::ERR_BLOCKED_BY_CLIENT; | 
 |  | 
 | void RestrictProcessCount() { | 
 |   // Set the maximum number of processes to 1.  This is a soft limit that | 
 |   // we're allowed to exceed if processes *must* not share, which is the case | 
 |   // for cross-origin-isolated contexts vs non-cross-origin-isolated | 
 |   // contexts. | 
 |   content::RenderProcessHost::SetMaxRendererProcessCount(1); | 
 | } | 
 |  | 
 | class CrossOriginIsolationTest : public ExtensionBrowserTest { | 
 |  public: | 
 |   CrossOriginIsolationTest() = default; | 
 |   ~CrossOriginIsolationTest() override = default; | 
 |   CrossOriginIsolationTest(const CrossOriginIsolationTest&) = delete; | 
 |   CrossOriginIsolationTest& operator=(const CrossOriginIsolationTest&) = delete; | 
 |  | 
 |   void SetUpOnMainThread() override { | 
 |     ExtensionBrowserTest::SetUpOnMainThread(); | 
 |     host_resolver()->AddRule("*", "127.0.0.1"); | 
 |     ASSERT_TRUE(embedded_test_server()->Start()); | 
 |   } | 
 |  | 
 |   struct Options { | 
 |     const char* coep_value = nullptr; | 
 |     const char* coop_value = nullptr; | 
 |     bool use_service_worker = false; | 
 |     const char* background_script = ""; | 
 |     const char* test_js = ""; | 
 |     bool is_platform_app = false; | 
 |   }; | 
 |   using ExtensionBrowserTest::LoadExtension; | 
 |   const Extension* LoadExtension(TestExtensionDir& dir, | 
 |                                  const Options& options) { | 
 |     CHECK(options.coep_value); | 
 |     CHECK(options.coop_value); | 
 |     CHECK(!options.is_platform_app || !options.use_service_worker) | 
 |         << "Platform apps cannot use 'service_worker' key."; | 
 | #if !BUILDFLAG(ENABLE_PLATFORM_APPS) | 
 |     CHECK(!options.is_platform_app) << "Platform apps are not supported."; | 
 | #endif | 
 |  | 
 |     static constexpr char kManifestTemplate[] = R"( | 
 |       { | 
 |         %s, | 
 |         %s | 
 |         "manifest_version": 2, | 
 |         "name": "CrossOriginIsolation", | 
 |         "version": "1.1", | 
 |         "cross_origin_embedder_policy": { | 
 |           "value": "%s" | 
 |         }, | 
 |         "cross_origin_opener_policy": { | 
 |           "value": "%s" | 
 |         }, | 
 |         "permissions": ["http://foo.test:*/*"] | 
 |       } | 
 |     )"; | 
 |  | 
 |     const char* background_script = nullptr; | 
 |     const char* extension_only_keys = R"( | 
 |       "web_accessible_resources": ["test.html"], | 
 |     )"; | 
 |     if (options.is_platform_app) { | 
 |       background_script = R"( | 
 |         "app": { | 
 |           "background": { | 
 |             "scripts": ["background.js"] | 
 |           } | 
 |         } | 
 |       )"; | 
 |       extension_only_keys = ""; | 
 |     } else if (options.use_service_worker) { | 
 |       background_script = R"( | 
 |         "background" : { | 
 |           "service_worker": "background.js" | 
 |         } | 
 |       )"; | 
 |     } else { | 
 |       background_script = R"( | 
 |         "background": { | 
 |           "scripts": ["background.js"] | 
 |         } | 
 |       )"; | 
 |     } | 
 |  | 
 |     CHECK(background_script); | 
 |     std::string manifest = base::StringPrintf( | 
 |         kManifestTemplate, background_script, extension_only_keys, | 
 |         options.coep_value, options.coop_value); | 
 |     dir.WriteManifest(manifest); | 
 |     dir.WriteFile(FILE_PATH_LITERAL("background.js"), | 
 |                   options.background_script); | 
 |     dir.WriteFile(FILE_PATH_LITERAL("test.html"), | 
 |                   "<script src='test.js'></script>"); | 
 |     dir.WriteFile(FILE_PATH_LITERAL("test.js"), options.test_js); | 
 |     return LoadExtension(dir.UnpackedPath()); | 
 |   } | 
 |  | 
 |   bool IsCrossOriginIsolated(content::RenderFrameHost* host) { | 
 |     return content::EvalJs(host, "crossOriginIsolated").ExtractBool(); | 
 |   } | 
 |  | 
 |   content::RenderFrameHost* GetBackgroundRenderFrameHost( | 
 |       const Extension& extension) { | 
 |     ExtensionHost* host = | 
 |         ProcessManager::Get(profile())->GetBackgroundHostForExtension( | 
 |             extension.id()); | 
 |     return host ? host->main_frame_host() : nullptr; | 
 |   } | 
 | }; | 
 |  | 
 | // Tests that extensions can opt into cross origin isolation. | 
 | IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest, CrossOriginIsolation) { | 
 |   RestrictProcessCount(); | 
 |  | 
 |   TestExtensionDir coi_test_dir; | 
 |   const Extension* coi_extension = LoadExtension( | 
 |       coi_test_dir, | 
 |       {.coep_value = "require-corp", .coop_value = "same-origin"}); | 
 |   ASSERT_TRUE(coi_extension); | 
 |   content::RenderFrameHost* coi_background_render_frame_host = | 
 |       GetBackgroundRenderFrameHost(*coi_extension); | 
 |   ASSERT_TRUE(coi_background_render_frame_host); | 
 |   EXPECT_TRUE(IsCrossOriginIsolated(coi_background_render_frame_host)); | 
 |  | 
 |   TestExtensionDir non_coi_test_dir; | 
 |   const Extension* non_coi_extension = | 
 |       LoadExtension(non_coi_test_dir, | 
 |                     {.coep_value = "unsafe-none", .coop_value = "same-origin"}); | 
 |   ASSERT_TRUE(non_coi_extension); | 
 |   content::RenderFrameHost* non_coi_background_render_frame_host = | 
 |       GetBackgroundRenderFrameHost(*non_coi_extension); | 
 |   ASSERT_TRUE(non_coi_background_render_frame_host); | 
 |   EXPECT_FALSE(IsCrossOriginIsolated(non_coi_background_render_frame_host)); | 
 |  | 
 |   // A cross-origin-isolated extension should not share a process with a | 
 |   // non-cross-origin-isolated one. | 
 |   EXPECT_NE(coi_background_render_frame_host->GetProcess(), | 
 |             non_coi_background_render_frame_host->GetProcess()); | 
 | } | 
 |  | 
 | // Tests the interaction of Cross-Origin-Embedder-Policy with extension host | 
 | // permissions. See crbug.com/1246109. | 
 | IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest, | 
 |                        CrossOriginEmbedderPolicy_HostPermissions) { | 
 |   TestExtensionDir test_dir_1; | 
 |   const Extension* coep_strict_extension = LoadExtension( | 
 |       test_dir_1, {.coep_value = "require-corp", .coop_value = "unsafe-none"}); | 
 |   ASSERT_TRUE(coep_strict_extension); | 
 |   content::RenderFrameHost* coep_strict_background_render_frame_host = | 
 |       GetBackgroundRenderFrameHost(*coep_strict_extension); | 
 |   ASSERT_TRUE(coep_strict_background_render_frame_host); | 
 |  | 
 |   TestExtensionDir test_dir_2; | 
 |   const Extension* coep_lax_extension = LoadExtension( | 
 |       test_dir_2, {.coep_value = "unsafe-none", .coop_value = "unsafe-none"}); | 
 |   ASSERT_TRUE(coep_lax_extension); | 
 |   content::RenderFrameHost* coep_lax_background_render_frame_host = | 
 |       GetBackgroundRenderFrameHost(*coep_lax_extension); | 
 |   ASSERT_TRUE(coep_lax_background_render_frame_host); | 
 |  | 
 |   auto test_image_load = [](content::RenderFrameHost* render_frame_host, | 
 |                             const GURL& image_url) -> std::string { | 
 |     static constexpr char kScript[] = R"( | 
 |       (() => { | 
 |         let img = document.createElement('img'); | 
 |         return new Promise(resolve => { | 
 |           img.addEventListener('load', () => { | 
 |             resolve('Success'); | 
 |           }); | 
 |           img.addEventListener('error', (e) => { | 
 |             resolve('Load failed'); | 
 |           }); | 
 |           img.src = $1; | 
 |           document.body.appendChild(img); | 
 |         }); | 
 |       })(); | 
 |     )"; | 
 |  | 
 |     return content::EvalJs(render_frame_host, | 
 |                            content::JsReplace(kScript, image_url)) | 
 |         .ExtractString(); | 
 |   }; | 
 |  | 
 |   GURL image_url_with_host_permissions = | 
 |       embedded_test_server()->GetURL("foo.test", "/load_image/image.png"); | 
 |   GURL image_url_without_host_permissions = | 
 |       embedded_test_server()->GetURL("bar.test", "/load_image/image.png"); | 
 |  | 
 |   // Allowed since cross origin embedding is allowed unless COEP: require-corp. | 
 |   EXPECT_EQ("Success", test_image_load(coep_lax_background_render_frame_host, | 
 |                                        image_url_with_host_permissions)); | 
 |   EXPECT_EQ("Success", test_image_load(coep_lax_background_render_frame_host, | 
 |                                        image_url_without_host_permissions)); | 
 |  | 
 |   // Disallowed due to COEP: require-corp. | 
 |   // TODO(crbug.com/40789023): Should host permissions override behavior here? | 
 |   EXPECT_EQ("Load failed", | 
 |             test_image_load(coep_strict_background_render_frame_host, | 
 |                             image_url_with_host_permissions)); | 
 |   EXPECT_EQ("Load failed", | 
 |             test_image_load(coep_strict_background_render_frame_host, | 
 |                             image_url_without_host_permissions)); | 
 | } | 
 |  | 
 | #if BUILDFLAG(ENABLE_PLATFORM_APPS) | 
 | // Tests that platform apps can opt into cross origin isolation. | 
 | IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest, | 
 |                        CrossOriginIsolation_PlatformApps) { | 
 |   RestrictProcessCount(); | 
 |  | 
 |   TestExtensionDir coi_test_dir; | 
 |   const Extension* coi_app = | 
 |       LoadExtension(coi_test_dir, {.coep_value = "require-corp", | 
 |                                    .coop_value = "same-origin", | 
 |                                    .is_platform_app = true}); | 
 |   ASSERT_TRUE(coi_app); | 
 |   ASSERT_TRUE(coi_app->is_platform_app()); | 
 |   content::RenderFrameHost* coi_app_background_render_frame_host = | 
 |       GetBackgroundRenderFrameHost(*coi_app); | 
 |   ASSERT_TRUE(coi_app_background_render_frame_host); | 
 |   EXPECT_TRUE(IsCrossOriginIsolated(coi_app_background_render_frame_host)); | 
 |  | 
 |   TestExtensionDir non_coi_test_dir; | 
 |   const Extension* non_coi_extension = | 
 |       LoadExtension(non_coi_test_dir, | 
 |                     {.coep_value = "unsafe-none", .coop_value = "same-origin"}); | 
 |   ASSERT_TRUE(non_coi_extension); | 
 |   content::RenderFrameHost* non_coi_background_render_frame_host = | 
 |       GetBackgroundRenderFrameHost(*non_coi_extension); | 
 |   ASSERT_TRUE(non_coi_background_render_frame_host); | 
 |   EXPECT_FALSE(IsCrossOriginIsolated(non_coi_background_render_frame_host)); | 
 |  | 
 |   // A cross-origin-isolated platform app should not share a process with a | 
 |   // non-cross-origin-isolated extension. | 
 |   EXPECT_NE(coi_app_background_render_frame_host->GetProcess(), | 
 |             non_coi_background_render_frame_host->GetProcess()); | 
 | } | 
 | #endif  // BUILDFLAG(ENABLE_PLATFORM_APPS) | 
 |  | 
 | // Tests that a web accessible frame from a cross origin isolated extension is | 
 | // not cross origin isolated. | 
 | IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest, WebAccessibleFrame) { | 
 |   RestrictProcessCount(); | 
 |  | 
 |   TestExtensionDir coi_test_dir; | 
 |   const Extension* coi_extension = LoadExtension( | 
 |       coi_test_dir, | 
 |       {.coep_value = "require-corp", .coop_value = "same-origin"}); | 
 |   ASSERT_TRUE(coi_extension); | 
 |   content::RenderFrameHost* coi_background_render_frame_host = | 
 |       GetBackgroundRenderFrameHost(*coi_extension); | 
 |   ASSERT_TRUE(coi_background_render_frame_host); | 
 |   EXPECT_TRUE(IsCrossOriginIsolated(coi_background_render_frame_host)); | 
 |  | 
 |   GURL extension_test_url = coi_extension->GetResourceURL("test.html"); | 
 |   ASSERT_TRUE( | 
 |       content::NavigateToURL(GetActiveWebContents(), extension_test_url)); | 
 |   content::WebContents* web_contents = GetActiveWebContents(); | 
 |   EXPECT_TRUE(IsCrossOriginIsolated(web_contents->GetPrimaryMainFrame())); | 
 |   EXPECT_EQ(web_contents->GetPrimaryMainFrame()->GetProcess(), | 
 |             coi_background_render_frame_host->GetProcess()); | 
 |  | 
 |   // Load test.html as a web accessible resource inside a web frame. | 
 |   ASSERT_TRUE(content::NavigateToURL( | 
 |       GetActiveWebContents(), | 
 |       embedded_test_server()->GetURL("/iframe_blank.html"))); | 
 |   ASSERT_TRUE( | 
 |       content::NavigateIframeToURL(web_contents, "test", extension_test_url)); | 
 |  | 
 |   content::RenderFrameHost* extension_iframe = | 
 |       content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0); | 
 |   ASSERT_TRUE(extension_iframe); | 
 |   EXPECT_EQ(extension_test_url, extension_iframe->GetLastCommittedURL()); | 
 |  | 
 |   // The extension iframe is embedded within a web frame and won't be cross | 
 |   // origin isolated. It should also not share a process with the extension's | 
 |   // cross origin isolated context, nor with the web frame it's embedded in. | 
 |   EXPECT_FALSE(IsCrossOriginIsolated(extension_iframe)); | 
 |   EXPECT_NE(extension_iframe->GetProcess(), | 
 |             coi_background_render_frame_host->GetProcess()); | 
 |   EXPECT_NE(extension_iframe->GetProcess(), | 
 |             web_contents->GetPrimaryMainFrame()->GetProcess()); | 
 |  | 
 |   // Check ProcessManager APIs to ensure they work correctly for the case where | 
 |   // an extension has multiple processes for the same profile. | 
 |   { | 
 |     ProcessManager* process_manager = ProcessManager::Get(profile()); | 
 |     ASSERT_TRUE(process_manager); | 
 |     std::set<content::RenderFrameHost*> extension_hosts = | 
 |         process_manager->GetRenderFrameHostsForExtension(coi_extension->id()); | 
 |     EXPECT_THAT(extension_hosts, | 
 |                 ::testing::UnorderedElementsAre( | 
 |                     coi_background_render_frame_host, extension_iframe)); | 
 |  | 
 |     EXPECT_EQ(coi_extension, process_manager->GetExtensionForRenderFrameHost( | 
 |                                  coi_background_render_frame_host)); | 
 |     EXPECT_EQ(coi_extension, process_manager->GetExtensionForRenderFrameHost( | 
 |                                  extension_iframe)); | 
 |   } | 
 |  | 
 |   // Check ProcessMap APIs to ensure they work correctly for the case where an | 
 |   // extension has multiple processes for the same profile. | 
 |   { | 
 |     ProcessMap* process_map = ProcessMap::Get(profile()); | 
 |     ASSERT_TRUE(process_map); | 
 |     EXPECT_TRUE(process_map->Contains( | 
 |         coi_extension->id(), | 
 |         coi_background_render_frame_host->GetProcess()->GetDeprecatedID())); | 
 |     EXPECT_TRUE(process_map->Contains( | 
 |         coi_extension->id(), | 
 |         extension_iframe->GetProcess()->GetDeprecatedID())); | 
 |  | 
 |     GURL* url = nullptr; | 
 |     EXPECT_EQ( | 
 |         mojom::ContextType::kPrivilegedExtension, | 
 |         process_map->GetMostLikelyContextType( | 
 |             coi_extension, | 
 |             coi_background_render_frame_host->GetProcess()->GetDeprecatedID(), | 
 |             url)); | 
 |     EXPECT_EQ(mojom::ContextType::kPrivilegedExtension, | 
 |               process_map->GetMostLikelyContextType( | 
 |                   coi_extension, | 
 |                   extension_iframe->GetProcess()->GetDeprecatedID(), url)); | 
 |   } | 
 |  | 
 |   // Ensure both cross-origin-isolated and non-cross-origin-isolated extension | 
 |   // contexts inherit extension's cross-origin privileges. | 
 |   { | 
 |     auto execute_fetch = [](content::RenderFrameHost* host, const GURL& url) { | 
 |       static constexpr char kScript[] = R"( | 
 |         fetch('%s') | 
 |           .then(response => response.text()) | 
 |           .catch(err => "Fetch error: " + err); | 
 |       )"; | 
 |       std::string script = base::StringPrintf(kScript, url.spec().c_str()); | 
 |       return content::EvalJs(host, script).ExtractString(); | 
 |     }; | 
 |     // Sanity check that fetching a url the extension doesn't have access to, | 
 |     // leads to a fetch error. | 
 |     const char* kPath = "/extensions/test_file.txt"; | 
 |     GURL disallowed_url = embedded_test_server()->GetURL("bar.test", kPath); | 
 |     EXPECT_THAT(execute_fetch(coi_background_render_frame_host, disallowed_url), | 
 |                 ::testing::HasSubstr("Fetch error:")); | 
 |  | 
 |     GURL allowed_url = embedded_test_server()->GetURL("foo.test", kPath); | 
 |     EXPECT_EQ("Hello!", | 
 |               execute_fetch(coi_background_render_frame_host, allowed_url)); | 
 |     EXPECT_EQ("Hello!", execute_fetch(extension_iframe, allowed_url)); | 
 |   } | 
 |  | 
 |   // Finally make some extension API calls to ensure both cross-origin-isolated | 
 |   // and non-cross-origin-isolated extension contexts are considered | 
 |   // "privileged". Use chrome.extension.isAllowedIncognitoAccess because it is | 
 |   // supported on desktop Android. | 
 |   { | 
 |     auto verify_is_privileged_context = [](content::RenderFrameHost* host) { | 
 |       const char* kScript = R"( | 
 |         new Promise(resolve => { | 
 |           chrome.extension.isAllowedIncognitoAccess(allowed => { | 
 |             resolve(allowed); | 
 |           }); | 
 |         }); | 
 |       )"; | 
 |       EXPECT_EQ(false, content::EvalJs(host, kScript)); | 
 |     }; | 
 |  | 
 |     { | 
 |       SCOPED_TRACE( | 
 |           "Verifying coi extension background is a privileged context."); | 
 |       verify_is_privileged_context(coi_background_render_frame_host); | 
 |     } | 
 |     { | 
 |       SCOPED_TRACE( | 
 |           "Verifying non-coi extension iframe is a privileged context."); | 
 |       verify_is_privileged_context(extension_iframe); | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | // Test that an extension service worker for a cross origin isolated extension | 
 | // is not cross origin isolated. See crbug.com/1131404. | 
 | IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest, ServiceWorker) { | 
 |   RestrictProcessCount(); | 
 |  | 
 |   static constexpr char kServiceWorkerScript[] = R"( | 
 |     const readyMessage = crossOriginIsolated ? | 
 |         'crossOriginIsolated' : 'notCrossOriginIsolated'; | 
 |     chrome.test.sendMessage(readyMessage, () => {}); | 
 |   )"; | 
 |  | 
 |   ExtensionTestMessageListener ready_listener("notCrossOriginIsolated"); | 
 |   TestExtensionDir coi_test_dir; | 
 |   const Extension* coi_extension = | 
 |       LoadExtension(coi_test_dir, {.coep_value = "require-corp", | 
 |                                    .coop_value = "same-origin", | 
 |                                    .use_service_worker = true, | 
 |                                    .background_script = kServiceWorkerScript}); | 
 |   ASSERT_TRUE(coi_extension); | 
 |   EXPECT_TRUE(ready_listener.WaitUntilSatisfied()); | 
 |  | 
 |   GURL extension_test_url = coi_extension->GetResourceURL("test.html"); | 
 |   ASSERT_TRUE( | 
 |       content::NavigateToURL(GetActiveWebContents(), extension_test_url)); | 
 |   content::RenderFrameHost* extension_tab = | 
 |       content::ConvertToRenderFrameHost(GetActiveWebContents()); | 
 |   ASSERT_TRUE(extension_tab); | 
 |  | 
 |   // The service worker should be active since it's waiting for a response to | 
 |   // chrome.test.sendMessage call. | 
 |   std::vector<WorkerId> service_workers = | 
 |       ProcessManager::Get(profile())->GetServiceWorkersForExtension( | 
 |           coi_extension->id()); | 
 |   ASSERT_EQ(1u, service_workers.size()); | 
 |  | 
 |   // Sanity checking that the service worker (non-cross-origin-isolated) and the | 
 |   // extension tab (cross-origin-isolated) don't share a process. | 
 |   content::RenderProcessHost* service_worker_process = | 
 |       content::RenderProcessHost::FromID(service_workers[0].render_process_id); | 
 |   ASSERT_TRUE(service_worker_process); | 
 |   EXPECT_NE(service_worker_process, extension_tab->GetProcess()); | 
 |  | 
 |   // Check ProcessMap APIs to ensure they work correctly for the case where an | 
 |   // extension has multiple processes for the same profile. | 
 |   ProcessMap* process_map = ProcessMap::Get(profile()); | 
 |   ASSERT_TRUE(process_map); | 
 |   EXPECT_TRUE(process_map->Contains( | 
 |       coi_extension->id(), extension_tab->GetProcess()->GetDeprecatedID())); | 
 |   EXPECT_TRUE(process_map->Contains(coi_extension->id(), | 
 |                                     service_worker_process->GetDeprecatedID())); | 
 |  | 
 |   GURL* url = nullptr; | 
 |   EXPECT_EQ( | 
 |       mojom::ContextType::kPrivilegedExtension, | 
 |       process_map->GetMostLikelyContextType( | 
 |           coi_extension, extension_tab->GetProcess()->GetDeprecatedID(), url)); | 
 |   EXPECT_EQ(mojom::ContextType::kPrivilegedExtension, | 
 |             process_map->GetMostLikelyContextType( | 
 |                 coi_extension, service_worker_process->GetDeprecatedID(), url)); | 
 | } | 
 |  | 
 | // Tests certain extension APIs which retrieve in-process extension windows. | 
 | // Test these for a cross origin isolated extension with non-cross origin | 
 | // isolated contexts. | 
 | IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest, | 
 |                        WebAccessibleFrame_WindowApis) { | 
 |   TestExtensionDir coi_test_dir; | 
 |   const Extension* coi_extension = LoadExtension( | 
 |       coi_test_dir, | 
 |       {.coep_value = "require-corp", .coop_value = "same-origin"}); | 
 |   ASSERT_TRUE(coi_extension); | 
 |   content::RenderFrameHost* coi_background_render_frame_host = | 
 |       GetBackgroundRenderFrameHost(*coi_extension); | 
 |   ASSERT_TRUE(coi_background_render_frame_host); | 
 |  | 
 |   GURL extension_test_url = coi_extension->GetResourceURL("test.html"); | 
 |   content::WebContents* web_contents = GetActiveWebContents(); | 
 |   ASSERT_TRUE(NavigateToURL( | 
 |       web_contents, embedded_test_server()->GetURL("/iframe_blank.html"))); | 
 |   ASSERT_TRUE( | 
 |       content::NavigateIframeToURL(web_contents, "test", extension_test_url)); | 
 |   content::RenderFrameHost* extension_iframe = | 
 |       content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0); | 
 |   ASSERT_TRUE(extension_iframe); | 
 |  | 
 |   content::RenderFrameHost* extension_tab = | 
 |       NavigateToURLInNewTab(extension_test_url); | 
 |   ASSERT_TRUE(extension_tab); | 
 |  | 
 |   // getBackgroundPage API. | 
 |   { | 
 |     auto test_get_background_page = [](content::RenderFrameHost* host, | 
 |                                        bool expect_background_page) { | 
 |       static constexpr char kScript[] = R"( | 
 |         const expectBackgroundPage = %s; | 
 |         const hasBackgroundPage = !!chrome.extension.getBackgroundPage(); | 
 |         hasBackgroundPage === expectBackgroundPage; | 
 |       )"; | 
 |       EXPECT_EQ(true, content::EvalJs( | 
 |                           host, base::StringPrintf( | 
 |                                     kScript, | 
 |                                     base::ToString(expect_background_page)))); | 
 |     }; | 
 |  | 
 |     test_get_background_page(coi_background_render_frame_host, true); | 
 |     test_get_background_page(extension_tab, true); | 
 |  | 
 |     // The extension iframe should be non-cross origin isolated and hence in a | 
 |     // different process than the extension background page. Since the API can | 
 |     // only retrieve the background page if it's in the same process, | 
 |     // getBackgroundPage should return null here. | 
 |     test_get_background_page(extension_iframe, false); | 
 |   } | 
 |  | 
 |   // getViews API. | 
 |   { | 
 |     auto verify_get_tabs = [](content::RenderFrameHost* host, | 
 |                               int num_tabs_expected) { | 
 |       static constexpr char kScript[] = R"( | 
 |         const numTabsExpected = %d; | 
 |         const tabs = chrome.extension.getViews({type: 'tab'}); | 
 |         tabs.length === numTabsExpected; | 
 |       )"; | 
 |       EXPECT_EQ(true, content::EvalJs(host, base::StringPrintf( | 
 |                                                 kScript, num_tabs_expected))); | 
 |     }; | 
 |  | 
 |     verify_get_tabs(coi_background_render_frame_host, 1); | 
 |     verify_get_tabs(extension_tab, 1); | 
 |  | 
 |     // The extension iframe should be non-cross origin isolated and hence in a | 
 |     // different process than the background page. Since the API can only | 
 |     // retrieve windows in the same process, no windows will be returned. | 
 |     verify_get_tabs(extension_iframe, 0); | 
 |   } | 
 | } | 
 |  | 
 | // Tests extension messaging between cross origin isolated and | 
 | // non-cross-origin-isolated frames of an extension. | 
 | IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest, ExtensionMessaging_Frames) { | 
 |   RestrictProcessCount(); | 
 |  | 
 |   static constexpr char kTestJs[] = R"( | 
 |       function inIframe () { | 
 |         try { | 
 |           // Accessing `window.top` may raise an error due to the same origin | 
 |           // policy. | 
 |           return window.self !== window.top; | 
 |         } catch (e) { | 
 |           return true; | 
 |         } | 
 |       } | 
 |  | 
 |       chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | 
 |         if (message !== 'hello') { | 
 |           sendResponse('Unexpected message in test script ' + message); | 
 |           return; | 
 |         } | 
 |  | 
 |         if (inIframe()) | 
 |           sendResponse('ack-from-iframe'); | 
 |         else | 
 |           sendResponse('ack-from-tab'); | 
 |       }); | 
 |   )"; | 
 |  | 
 |   TestExtensionDir coi_test_dir; | 
 |   const Extension* coi_extension = | 
 |       LoadExtension(coi_test_dir, {.coep_value = "require-corp", | 
 |                                    .coop_value = "same-origin", | 
 |                                    .test_js = kTestJs}); | 
 |   ASSERT_TRUE(coi_extension); | 
 |  | 
 |   content::WebContents* web_contents = GetActiveWebContents(); | 
 |   ASSERT_TRUE(NavigateToURL( | 
 |       web_contents, embedded_test_server()->GetURL("/iframe_blank.html"))); | 
 |  | 
 |   GURL extension_test_url = coi_extension->GetResourceURL("test.html"); | 
 |   ASSERT_TRUE( | 
 |       content::NavigateIframeToURL(web_contents, "test", extension_test_url)); | 
 |   content::RenderFrameHost* extension_iframe = | 
 |       content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0); | 
 |   ASSERT_TRUE(extension_iframe); | 
 |  | 
 |   content::RenderFrameHost* extension_tab = | 
 |       NavigateToURLInNewTab(extension_test_url); | 
 |   ASSERT_TRUE(extension_tab); | 
 |  | 
 |   // `extension_iframe` and `extension_tab` should not share a process as they | 
 |   // are non-cross-origin-isolated and cross-origin-isolated respectively. | 
 |   EXPECT_NE(extension_iframe->GetProcess(), extension_tab->GetProcess()); | 
 |  | 
 |   // However they should be able to use extension messaging to communicate. | 
 |   auto test_messaging = [](content::RenderFrameHost* source, | 
 |                            content::RenderFrameHost* destination, | 
 |                            const char* expected_response) { | 
 |     static constexpr char kScript[] = R"( | 
 |       chrome.runtime.sendMessage('hello', response => { | 
 |         chrome.test.assertNoLastError(); | 
 |         chrome.test.assertEq($1, response); | 
 |         chrome.test.succeed(); | 
 |       }); | 
 |     )"; | 
 |  | 
 |     ResultCatcher catcher; | 
 |     ASSERT_TRUE(content::ExecJs( | 
 |         source, content::JsReplace(kScript, expected_response))); | 
 |     EXPECT_TRUE(catcher.GetNextResult()) << catcher.message(); | 
 |   }; | 
 |  | 
 |   { | 
 |     SCOPED_TRACE("Message from iframe to tab."); | 
 |     test_messaging(extension_iframe, extension_tab, "ack-from-tab"); | 
 |   } | 
 |  | 
 |   { | 
 |     SCOPED_TRACE("Message from tab to iframe."); | 
 |     test_messaging(extension_tab, extension_iframe, "ack-from-iframe"); | 
 |   } | 
 | } | 
 |  | 
 | // Tests extension messaging between a cross origin isolated extension frame and | 
 | // the extension service worker which is not cross origin isolated (and hence in | 
 | // a different process). | 
 | IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest, | 
 |                        ExtensionMessaging_ServiceWorker) { | 
 |   RestrictProcessCount(); | 
 |  | 
 |   static constexpr char kTestJs[] = R"( | 
 |     chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | 
 |       console.log('message received'); | 
 |       if (message !== 'hello-from-service-worker') { | 
 |         sendResponse('Invalid message received by tab ' + message); | 
 |         return; | 
 |       } | 
 |  | 
 |       sendResponse('ack-from-tab'); | 
 |     }); | 
 |   )"; | 
 |  | 
 |   static constexpr char kServiceWorkerScript[] = R"( | 
 |     chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | 
 |       if (message !== 'hello-from-tab') { | 
 |         sendResponse('Invalid message received by service worker ' + message); | 
 |         return; | 
 |       } | 
 |  | 
 |       sendResponse('ack-from-service-worker'); | 
 |     }); | 
 |  | 
 |     chrome.test.sendMessage('ready', () => { | 
 |       chrome.runtime.sendMessage( | 
 |           'hello-from-service-worker', response => { | 
 |             chrome.test.assertNoLastError(); | 
 |             chrome.test.assertEq('ack-from-tab', response); | 
 |             chrome.test.succeed(); | 
 |           }); | 
 |     }); | 
 |   )"; | 
 |  | 
 |   ExtensionTestMessageListener ready_listener("ready", | 
 |                                               ReplyBehavior::kWillReply); | 
 |   TestExtensionDir coi_test_dir; | 
 |   const Extension* coi_extension = | 
 |       LoadExtension(coi_test_dir, {.coep_value = "require-corp", | 
 |                                    .coop_value = "same-origin", | 
 |                                    .use_service_worker = true, | 
 |                                    .background_script = kServiceWorkerScript, | 
 |                                    .test_js = kTestJs}); | 
 |   ASSERT_TRUE(coi_extension); | 
 |   EXPECT_TRUE(ready_listener.WaitUntilSatisfied()); | 
 |  | 
 |   GURL extension_test_url = coi_extension->GetResourceURL("test.html"); | 
 |   content::WebContents* web_contents = GetActiveWebContents(); | 
 |   ASSERT_TRUE(NavigateToURL(web_contents, extension_test_url)); | 
 |   content::RenderFrameHost* extension_tab = web_contents->GetPrimaryMainFrame(); | 
 |   ASSERT_TRUE(extension_tab); | 
 |  | 
 |   { | 
 |     SCOPED_TRACE("Message from service worker to tab."); | 
 |     ResultCatcher catcher; | 
 |     ready_listener.Reply(""); | 
 |     EXPECT_TRUE(catcher.GetNextResult()) << catcher.message(); | 
 |   } | 
 |  | 
 |   { | 
 |     SCOPED_TRACE("Message from tab to service worker."); | 
 |     static constexpr char kScript[] = R"( | 
 |       chrome.runtime.sendMessage('hello-from-tab', response => { | 
 |         chrome.test.assertNoLastError(); | 
 |         chrome.test.assertEq('ack-from-service-worker', response); | 
 |         chrome.test.succeed(); | 
 |       }); | 
 |     )"; | 
 |     ResultCatcher catcher; | 
 |     ASSERT_TRUE(content::ExecJs(extension_tab, kScript)); | 
 |     EXPECT_TRUE(catcher.GetNextResult()) << catcher.message(); | 
 |   } | 
 | } | 
 |  | 
 | // Verify extension resource access if it's in an iframe. Regression test for | 
 | // crbug.com/1343610. | 
 | IN_PROC_BROWSER_TEST_F(ExtensionBrowserTest, ExtensionResourceInIframe) { | 
 |   EXPECT_TRUE(embedded_test_server()->Start()); | 
 |  | 
 |   // Load an extension that has one web accessible resource. | 
 |   TestExtensionDir extension_dir; | 
 |   static constexpr char kManifestStub[] = R"({ | 
 |     "name": "Test", | 
 |     "version": "0.1", | 
 |     "manifest_version": 3, | 
 |     "web_accessible_resources": [ | 
 |       { | 
 |         "resources": [ "accessible_resource.html" ], | 
 |         "matches": [ "<all_urls>" ] | 
 |       } | 
 |     ] | 
 |   })"; | 
 |   extension_dir.WriteManifest(kManifestStub); | 
 |   extension_dir.WriteFile(FILE_PATH_LITERAL("accessible_resource.html"), ""); | 
 |   extension_dir.WriteFile(FILE_PATH_LITERAL("inaccessible_resource.html"), ""); | 
 |   const Extension* extension = LoadExtension(extension_dir.UnpackedPath()); | 
 |   EXPECT_TRUE(extension); | 
 |  | 
 |   // Allow navigation from a web frame to a web accessible resource. | 
 |   { | 
 |     // Navigate the main frame with a renderer initiated navigation to a blank | 
 |     // web page. This should succeed. | 
 |     const GURL gurl = embedded_test_server()->GetURL("/iframe_blank.html"); | 
 |     content::WebContents* web_contents = GetActiveWebContents(); | 
 |     EXPECT_TRUE(content::NavigateToURL(web_contents, gurl)); | 
 |     content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame(); | 
 |     content::RenderFrameHost* iframe = content::ChildFrameAt(main_frame, 0); | 
 |     EXPECT_TRUE(iframe); | 
 |  | 
 |     // Navigate the iframe with a renderer initiated navigation to a web | 
 |     // accessible resource. This should succeed. | 
 |     GURL target = extension->GetResourceURL("accessible_resource.html"); | 
 |     content::TestNavigationObserver nav_observer(web_contents); | 
 |     EXPECT_TRUE(content::NavigateIframeToURL(web_contents, "test", target)); | 
 |     nav_observer.Wait(); | 
 |     EXPECT_TRUE(nav_observer.last_navigation_succeeded()); | 
 |     EXPECT_EQ(net::OK, nav_observer.last_net_error_code()); | 
 |     iframe = content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0); | 
 |     EXPECT_EQ(target, iframe->GetLastCommittedURL()); | 
 |   } | 
 |  | 
 |   // Prevent navigation from a web frame to a non-web accessible resource. | 
 |   { | 
 |     GURL invalid_request_url = GURL(kExtensionInvalidRequestURL); | 
 |  | 
 |     // Navigate the main frame with a renderer initiated navigation to a blank | 
 |     // web page. This should succeed. | 
 |     const GURL gurl = embedded_test_server()->GetURL("/iframe_blank.html"); | 
 |     content::WebContents* web_contents = GetActiveWebContents(); | 
 |     EXPECT_TRUE(content::NavigateToURL(web_contents, gurl)); | 
 |     content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame(); | 
 |     content::RenderFrameHost* iframe = content::ChildFrameAt(main_frame, 0); | 
 |     EXPECT_TRUE(iframe); | 
 |  | 
 |     // Navigate the iframe with a renderer initiated navigation to an extension | 
 |     // resource that isn't a web accessible resource. This should be blocked. | 
 |     GURL target = extension->GetResourceURL("inaccessible_resource.html"); | 
 |     content::TestNavigationObserver nav_observer(web_contents); | 
 |     EXPECT_TRUE(content::NavigateIframeToURL(web_contents, "test", target)); | 
 |     nav_observer.Wait(); | 
 |     EXPECT_FALSE(nav_observer.last_navigation_succeeded()); | 
 | #if BUILDFLAG(ENABLE_EXTENSIONS) | 
 |     // TODO(crbug.com/442905487): Figure out why this doesn't work on Android. | 
 |     // The navigation is correctly blocked, but the error code is different and | 
 |     // the iframe pointer is invalidated. | 
 |     EXPECT_EQ(ERR_BLOCKED_BY_CLIENT, nav_observer.last_net_error_code()); | 
 | #endif | 
 |     iframe = content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0); | 
 |     EXPECT_EQ(invalid_request_url, iframe->GetLastCommittedURL()); | 
 |  | 
 |     // Navigate the iframe with a browser initiated navigation to an extension | 
 |     // resource. This should be blocked because the origin is not opaque, as | 
 |     // it's embedded in a web context. | 
 |     content::TestNavigationObserver reload_observer(web_contents); | 
 |     EXPECT_TRUE(iframe->Reload()); | 
 |     reload_observer.Wait(); | 
 | #if BUILDFLAG(ENABLE_EXTENSIONS) | 
 |     // TODO(crbug.com/442905487): Figure out why this doesn't work on Android. | 
 |     // The navigation is correctly blocked, but the error code is different. | 
 |     EXPECT_EQ(ERR_BLOCKED_BY_CLIENT, reload_observer.last_net_error_code()); | 
 | #endif | 
 |     iframe = content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0); | 
 |     EXPECT_FALSE(reload_observer.last_navigation_succeeded()); | 
 |     EXPECT_EQ(invalid_request_url, iframe->GetLastCommittedURL()); | 
 |  | 
 | #if BUILDFLAG(ENABLE_EXTENSIONS) | 
 |     // Verify iframe browser initiated navigation (to test real UI behavior). | 
 |     // TODO(https://crbug.com/442905487): Port to desktop Android when we have a | 
 |     // cross-platform replacement for NavigateParams. | 
 |     iframe = content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0); | 
 |     content::TestNavigationObserver browser_initiated_observer(target); | 
 |     NavigateParams params(browser(), target, ui::PAGE_TRANSITION_RELOAD); | 
 |     params.frame_tree_node_id = iframe->GetFrameTreeNodeId(); | 
 |     params.is_renderer_initiated = false; | 
 |     params.initiator_origin = embedded_test_server()->GetOrigin(); | 
 |     browser_initiated_observer.WatchExistingWebContents(); | 
 |     ui_test_utils::NavigateToURL(¶ms); | 
 |     browser_initiated_observer.Wait(); | 
 |     EXPECT_EQ(ERR_BLOCKED_BY_CLIENT, | 
 |               browser_initiated_observer.last_net_error_code()); | 
 |     EXPECT_FALSE(browser_initiated_observer.last_navigation_succeeded()); | 
 |     iframe = content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0); | 
 |     EXPECT_EQ(target, iframe->GetLastCommittedURL()); | 
 | #endif  // BUILDFLAG(ENABLE_EXTENSIONS) | 
 |   } | 
 | } | 
 |  | 
 | }  // namespace | 
 | }  // namespace extensions |