Chrome · BrowserTag
CVE-2026-14040
UAF in BrowserTag
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Pchrome/browser/apps/guest_view/app_view_browsertest.cc |
modified |
Files Changed
chrome/browser/apps/guest_view/app_view_browsertest.ccextensions/browser/guest_view/app_view/app_view_guest.ccextensions/browser/guest_view/app_view/app_view_guest.h
Patch
From 65c2955546bb3d4cca0d647c2511b9475b01dce7 Mon Sep 17 00:00:00 2001 From: Kevin McNee <[email protected]> Date: Mon, 11 May 2026 02:37:29 -0700 Subject: [PATCH] Prevent id collision in AppView pending embed requests The requests are stored in a global map, but the guest instance ids are scoped to a profile. We now include the profile's unique token as part of the key to prevent collisions across profiles. Fixed: 497488593 Change-Id: Ib3b54d15d0ecba1733c53ffed1bdbb4082f59b12 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7829969 Auto-Submit: Kevin McNee <[email protected]> Reviewed-by: Giovanni Pezzino <[email protected]> Commit-Queue: Giovanni Pezzino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1628447} --- diff --git a/chrome/browser/apps/guest_view/app_view_browsertest.cc b/chrome/browser/apps/guest_view/app_view_browsertest.cc index 9ce2ae4e..045d6f3 100644 --- a/chrome/browser/apps/guest_view/app_view_browsertest.cc +++ b/chrome/browser/apps/guest_view/app_view_browsertest.cc @@ -300,6 +300,46 @@ ContinueEmbedding(guest_app, true); } +IN_PROC_BROWSER_TEST_P(AppViewTest, NoCrossProfilePendingRequestCollision) { + // Existing test utilities don't handle opening apps in other profiles. For + // ease of testing, we simulate an app making an embed request in another + // profile and leaving it pending. + const base::UnguessableToken other_profile_token = + base::UnguessableToken::Create(); + const int colliding_id = 1; + extensions::AppViewGuest::AddFakePendingRequestForTesting(other_profile_token, + colliding_id); + + // Now in the original profile, embed an app normally. + const extensions::Extension* host_app = + LoadAndLaunchPlatformApp("app_view/host_app", "AppViewTest.LAUNCHED"); + const extensions::Extension* guest_app = + InstallPlatformApp("app_view/guest_app"); + + ExtensionTestMessageListener on_embed_requested_listener( + "AppViewTest.EmbedRequested"); + ASSERT_TRUE(content::ExecJs( + extensions::AppWindowRegistry::Get(browser()->profile()) + ->GetCurrentAppWindowForApp(host_app->id()) + ->web_contents(), + content::JsReplace("onAppCommand($1, $2);", "EMBED", guest_app->id()))); + ASSERT_TRUE(on_embed_requested_listener.WaitUntilSatisfied()); + EXPECT_EQ( + 2u, + extensions::AppViewGuest::GetAllRegisteredInstanceIdsForTesting().size()); + + ContinueEmbedding(guest_app, true); + + guest_view::GuestViewBase* guest = + test_guest_view_manager()->WaitForSingleGuestViewCreated(); + EXPECT_EQ(browser()->profile(), guest->browser_context()); + EXPECT_TRUE(test_guest_view_manager()->WaitUntilAttachedAndLoaded(guest)); + + EXPECT_EQ( + 1u, + extensions::AppViewGuest::GetAllRegisteredInstanceIdsForTesting().size()); +} + // Load an AppView which loads a WebView with a text field. The embedding app // calls `focus()` on the AppView. The AppView calls `focus()` on the WebView. // This should be enough to focus the content of the WebView without further diff --git a/extensions/browser/guest_view/app_view/app_view_guest.cc b/extensions/browser/guest_view/app_view/app_view_guest.cc index f51ac6c64..4a610e6 100644 --- a/extensions/browser/guest_view/app_view/app_view_guest.cc +++ b/extensions/browser/guest_view/app_view/app_view_guest.cc @@ -10,6 +10,7 @@ #include "base/functional/bind.h" #include "base/lazy_instance.h" #include "components/guest_view/browser/guest_view_manager.h" +#include "content/public/browser/browser_context.h" #include "content/public/browser/render_process_host.h" #include "content/public/common/content_features.h" #include "extensions/browser/api/app_runtime/app_runtime_api.h" @@ -53,7 +54,9 @@ ~ResponseInfo() = default; }; -using PendingResponseMap = std::map<int, std::unique_ptr<ResponseInfo>>; +using PendingResponseKey = std::pair<int, base::UnguessableToken>; +using PendingResponseMap = + std::map<PendingResponseKey, std::unique_ptr<ResponseInfo>>; base::LazyInstance<PendingResponseMap>::DestructorAtExit g_pending_response_map = LAZY_INSTANCE_INITIALIZER; @@ -72,7 +75,8 @@ const std::string& guest_extension_id, content::RenderProcessHost* guest_render_process_host) { PendingResponseMap* response_map = g_pending_response_map.Pointer(); - auto it = response_map->find(guest_instance_id); + auto it = response_map->find( + std::make_pair(guest_instance_id, browser_context->UniqueToken())); // Kill the requesting process if it is not the real guest. if (it == response_map->end()) { // The requester used an invalid |guest_instance_id|. @@ -355,10 +359,13 @@ ->enabled_extensions() .GetByID(context_info->extension_id); - g_pending_response_map.Get().insert(std::make_pair( - guest_instance_id(), + auto [it, inserted] = g_pending_response_map.Get().insert(std::make_pair( + std::make_pair(guest_instance_id(), browser_context()->UniqueToken()), std::make_unique<ResponseInfo>(extension, std::move(owned_this), std::move(callback)))); + // If there was a conflicting element, then it's unsafe to proceed as `this` + // would be destroyed. + CHECK(inserted); base::DictValue embed_request; embed_request.Set(appview::kGuestInstanceID, guest_instance_id()); @@ -384,9 +391,18 @@ std::vector<int> AppViewGuest::GetAllRegisteredInstanceIdsForTesting() { std::vector<int> instances; for (const auto& key_value : g_pending_response_map.Get()) { - instances.push_back(key_value.first); + instances.push_back(key_value.first.first); } return instances; } +void AppViewGuest::AddFakePendingRequestForTesting( + const base::UnguessableToken& profile_token, + int guest_instance_id) { + auto [it, inserted] = g_pending_response_map.Get().insert(std::make_pair( + std::make_pair(guest_instance_id, profile_token), + std::make_unique<ResponseInfo>(nullptr, nullptr, base::NullCallback()))); + CHECK(inserted); +} + } // namespace extensions diff --git a/extensions/browser/guest_view/app_view/app_view_guest.h b/extensions/browser/guest_view/app_view/app_view_guest.h index ab01e95..2128e6a 100644 --- a/extensions/browser/guest_view/app_view/app_view_guest.h +++ b/extensions/browser/guest_view/app_view/app_view_guest.h @@ -8,6 +8,7 @@ #include <memory> #include "base/containers/id_map.h" +#include "base/unguessable_token.h" #include "base/values.h" #include "components/guest_view/browser/guest_view.h" #include "extensions/browser/guest_view/app_view/app_view_guest_delegate.h" @@ -53,6 +54,10 @@ // Sets the AppDelegate for this guest. void SetAppDelegateForTest(AppDelegate* delegate); + static void AddFakePendingRequestForTesting( + const base::UnguessableToken& profile_token, + int guest_instance_id); + private: explicit AppViewGuest(content::RenderFrameHost* owner_rfh);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/apps/guest_view/app_view_browsertest.cc b/chrome/browser/apps/guest_view/app_view_browsertest.cc
index 9ce2ae4e..045d6f3 100644
--- a/chrome/browser/apps/guest_view/app_view_browsertest.cc
+++ b/chrome/browser/apps/guest_view/app_view_browsertest.cc
@@ -300,6 +300,46 @@
ContinueEmbedding(guest_app, true);
}
+IN_PROC_BROWSER_TEST_P(AppViewTest, NoCrossProfilePendingRequestCollision) {
+ // Existing test utilities don't handle opening apps in other profiles. For
+ // ease of testing, we simulate an app making an embed request in another
+ // profile and leaving it pending.
+ const base::UnguessableToken other_profile_token =
+ base::UnguessableToken::Create();
+ const int colliding_id = 1;
+ extensions::AppViewGuest::AddFakePendingRequestForTesting(other_profile_token,
+ colliding_id);
+
+ // Now in the original profile, embed an app normally.
+ const extensions::Extension* host_app =
+ LoadAndLaunchPlatformApp("app_view/host_app", "AppViewTest.LAUNCHED");
+ const extensions::Extension* guest_app =
+ InstallPlatformApp("app_view/guest_app");
+
+ ExtensionTestMessageListener on_embed_requested_listener(
+ "AppViewTest.EmbedRequested");
+ ASSERT_TRUE(content::ExecJs(
+ extensions::AppWindowRegistry::Get(browser()->profile())
+ ->GetCurrentAppWindowForApp(host_app->id())
+ ->web_contents(),
+ content::JsReplace("onAppCommand($1, $2);", "EMBED", guest_app->id())));
+ ASSERT_TRUE(on_embed_requested_listener.WaitUntilSatisfied());
+ EXPECT_EQ(
+ 2u,
+ extensions::AppViewGuest::GetAllRegisteredInstanceIdsForTesting().size());
+
+ ContinueEmbedding(guest_app, true);
+
+ guest_view::GuestViewBase* guest =
+ test_guest_view_manager()->WaitForSingleGuestViewCreated();
+ EXPECT_EQ(browser()->profile(), guest->browser_context());
+ EXPECT_TRUE(test_guest_view_manager()->WaitUntilAttachedAndLoaded(guest));
+
+ EXPECT_EQ(
+ 1u,
+ extensions::AppViewGuest::GetAllRegisteredInstanceIdsForTesting().size());
+}
+
// Load an AppView which loads a WebView with a text field. The embedding app
// calls `focus()` on the AppView. The AppView calls `focus()` on the WebView.
// This should be enough to focus the content of the WebView without further
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page