Low watch CVSS 9.6 chrome UAF 🔧 Commit mapped

Overview

Low
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DevTools
DescriptionUse after free in DevTools
ComponentDevTools
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker516965176
Fix commit7aafca3e8c40 (chromium/src) +242/-48
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-08-04 (public git)
Disclosed2026-09-08
Patch gap35 days — fix public in source before the advisory

Background

`DevToolsManagerDelegate`
The embedder-facing interface that DevTools protocol handlers query for browser contexts (GetBrowserContexts(), GetDefaultBrowserContext(), and now GetBrowserContext(context_id)).
`BrowserContext` (OTR profile)
A browser context created on demand via Target.createBrowserContext; DevTools can request its automatic teardown via disposeOnDetach.
`disposeOnDetach` / `dispose_on_detach_context_ids_`
A set of context ids that must be destroyed when the controlling DevTools session detaches.
`base::WeakPtr`
A non-owning pointer that becomes null when its target is destroyed, so a caller can detect a freed object instead of dereferencing a dangling raw pointer.

Root Cause Analysis

GetBrowserContexts() returned a std::vector<content::BrowserContext*> of raw pointers, and the DevTools target_handler.cc detach path (the dispose_on_detach_context_ids_ loop; its enclosing method name is not shown in the @@ hunk) iterated that vector while calling delegate->DisposeBrowserContext(context, ...) on entries inside the loop. Disposing a browser context synchronously destroys the underlying OTR profile, so continuing to iterate and dereference the raw pointers held by the snapshot vector (e.g. context->UniqueId()) touched freed memory — a use-after-free reachable when many disposeOnDetach contexts are torn down together on detach.

The patch removes this pattern: it moves the id set into a local (auto context_ids = std::move(dispose_on_detach_context_ids_)), clears the member, then looks each context up freshly by id with the new GetBrowserContext(id) and only disposes it if the lookup is non-null. In parallel, GetBrowserContexts() was changed to return std::vector<base::WeakPtr<BrowserContext>> so remaining callers must null-check before use, and browser_handler.cc/target_handler.cc enumeration sites were rewritten to skip freed (null) weak pointers or to resolve a single context by id rather than scanning a raw-pointer list. This works because contexts are no longer dereferenced from a stale snapshot across a destroying call; each access is re-validated against live state.

Key insight
The core mistake was iterating a snapshot vector of raw BrowserContext* while destroying those very contexts inside the loop, leaving dangling pointers; the fix replaces per-iteration pointer retention with id-based re-lookup (GetBrowserContext) and makes GetBrowserContexts() hand out WeakPtrs that callers must null-check.

Attack Path

  1. Attach a browser-level DevTools session Via the DevTools/CDP protocol (e.g. Target.attachToBrowserTarget, as the regression test does), obtain access to the Target domain.
  2. Create many disposeOnDetach contexts Call Target.createBrowserContext({disposeOnDetach: true}) repeatedly (the test creates 64+), populating dispose_on_detach_context_ids_.
  3. Trigger mass disposal on detach Detach the session; the detach handler iterates GetBrowserContexts() and calls DisposeBrowserContext() on each matching context, destroying OTR profiles mid-iteration.
  4. Dangling dereference Subsequent loop iterations dereference now-freed BrowserContext* pointers (context->UniqueId()), producing the use-after-free in the browser process.
  5. Exploitation Turning this UAF into a controlled read/write or code execution is not demonstrated by the patch (inferred).

Impact Assessment

An actor with DevTools/CDP protocol access to the browser target can free browser-context objects mid-iteration and cause a use-after-free in the browser process by creating and then disposing many disposeOnDetach contexts. The advisory classifies this as a use-after-free in DevTools; no exploited-in-the-wild status is stated. The precondition is access to the DevTools browser target (remote debugging or an in-page context granted the protocol via exposeDevToolsProtocol, as in the test); any escalation of the UAF to memory corruption or code execution is (inferred) and not shown by the patch.

Changed Functions

FunctionChangeNotes
GetBrowserContexts
chrome/browser/devtools/chrome_devtools_manager_delegate.cc
modified
GetBrowserContexts
chrome/browser/devtools/devtools_browser_context_manager.cc
modified
if
content/browser/devtools/protocol/browser_handler.cc
modified
for
content/browser/devtools/protocol/target_handler.cc
modified
if
content/browser/devtools/protocol/target_handler.cc
modified
GetBrowserContexts
content/public/browser/devtools_manager_delegate.cc
modified
GetBrowserContexts
headless/lib/browser/headless_devtools_manager_delegate.cc
modified
GetDefaultBrowserContext
headless/lib/browser/headless_devtools_manager_delegate.cc
modified
if
headless/lib/browser/headless_devtools_manager_delegate.cc
modified

Files Changed

  • components/headless/test/data/protocol/shared/get-browser-contexts.js
  • chrome/browser/devtools/chrome_devtools_manager_delegate.h
  • chrome/browser/devtools/devtools_browser_context_manager.cc
  • headless/lib/browser/headless_devtools_manager_delegate.h
  • headless/lib/browser/headless_devtools_manager_delegate.cc
  • headless/test/data/protocol/sanity/context-disposal.js
  • chrome/browser/headless/test/headless_mode_protocol_browsertest.cc
  • headless/test/headless_protocol_browsertest.cc
  • headless/test/data/protocol/sanity/context-disposal-expected.txt
  • content/browser/devtools/protocol/browser_handler.cc
  • chrome/browser/devtools/devtools_browser_context_manager.h
  • components/headless/test/data/protocol/shared/get-browser-contexts-expected.txt
  • content/public/browser/devtools_manager_delegate.h
  • content/public/browser/devtools_manager_delegate.cc
  • chrome/browser/devtools/chrome_devtools_manager_delegate.cc
  • fuchsia_web/webengine/browser/web_engine_content_browser_client.cc
  • content/browser/devtools/protocol/target_handler.cc

Audit Directions

  • Destroy-while-iterating over container snapshots
    Look for loops that hold raw pointers from a getter and call a destroying method (Dispose*, Remove*, Close*) on elements inside the loop, where the underlying object is freed before later dereferences.
  • Raw-pointer getters for lifetime-fragile objects
    Audit APIs returning std::vector<T*> of profile/context/host objects and prefer id-based re-lookup or WeakPtr returns, checking that all callers null-check before use.
  • DevTools protocol teardown paths
    Review disposeOnDetach, detach, and shutdown handlers in DevTools/CDP handlers for re-entrancy where disposing one object mutates the collection or invalidates pointers still referenced later in the same operation.
From 7aafca3e8c40a59540cd2bbc4dc7af446024d0a8 Mon Sep 17 00:00:00 2001
From: Andrey Kosyakov <[email protected]>
Date: Tue, 04 Aug 2026 14:36:33 -0700
Subject: [PATCH] Rework browser context getters of DevToolsManagerDelegate

- introduce GetBrowserContext(contextId), use it in most places we were
    iterating list of browser contexts to find a context;
- make sure GetBrowserContexts() returns WeakPtrs, not raw ptrs, just to
    safeguard possible future usages;
- add a test for the previously present UaF when iterating browser contexts while destroying them.

Fixed: 516965176
Change-Id: I346c0f74b8024218151827d966f140e9f5ee92d6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8145402
Auto-Submit: Andrey Kosyakov <[email protected]>
Reviewed-by: Dmitry Gozman <[email protected]>
Commit-Queue: Andrey Kosyakov <[email protected]>
Reviewed-by: Sergey Ulanov <[email protected]>
Reviewed-by: Alex Rudenko <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1673682}
---
diff --git a/chrome/browser/devtools/chrome_devtools_manager_delegate.cc b/chrome/browser/devtools/chrome_devtools_manager_delegate.cc
index c9f49cd..3e7c8a2 100644
--- a/chrome/browser/devtools/chrome_devtools_manager_delegate.cc
+++ b/chrome/browser/devtools/chrome_devtools_manager_delegate.cc
@@ -446,7 +446,7 @@
                    params.navigated_or_inserted_contents);
 }
 
-std::vector<content::BrowserContext*>
+std::vector<base::WeakPtr<content::BrowserContext>>
 ChromeDevToolsManagerDelegate::GetBrowserContexts() {
   return DevToolsBrowserContextManager::GetInstance().GetBrowserContexts();
 }
@@ -457,6 +457,12 @@
       .GetDefaultBrowserContext();
 }
 
+content::BrowserContext* ChromeDevToolsManagerDelegate::GetBrowserContext(
+    const std::string& context_id) {
+  return DevToolsBrowserContextManager::GetInstance().GetProfileById(
+      context_id);
+}
+
 content::BrowserContext* ChromeDevToolsManagerDelegate::CreateBrowserContext() {
   return DevToolsBrowserContextManager::GetInstance().CreateBrowserContext();
 }
diff --git a/chrome/browser/devtools/chrome_devtools_manager_delegate.h b/chrome/browser/devtools/chrome_devtools_manager_delegate.h
index a48b3a9eb..5a1fb50 100644
--- a/chrome/browser/devtools/chrome_devtools_manager_delegate.h
+++ b/chrome/browser/devtools/chrome_devtools_manager_delegate.h
@@ -46,8 +46,11 @@
   // Resets |device_manager_|.
   void ResetAndroidDeviceManagerForTesting();
 
-  std::vector<content::BrowserContext*> GetBrowserContexts() override;
+  std::vector<base::WeakPtr<content::BrowserContext>> GetBrowserContexts()
+      override;
   content::BrowserContext* GetDefaultBrowserContext() override;
+  content::BrowserContext* GetBrowserContext(
+      const std::string& context_id) override;
 
   // Closes browser soon, not in the current task.
   static void CloseBrowserSoon();
diff --git a/chrome/browser/devtools/devtools_browser_context_manager.cc b/chrome/browser/devtools/devtools_browser_context_manager.cc
index fd8128c..7b3a276 100644
--- a/chrome/browser/devtools/devtools_browser_context_manager.cc
+++ b/chrome/browser/devtools/devtools_browser_context_manager.cc
@@ -68,11 +68,11 @@
   return otr_profile;
 }
 
-std::vector<content::BrowserContext*>
+std::vector<base::WeakPtr<content::BrowserContext>>
 DevToolsBrowserContextManager::GetBrowserContexts() {
-  std::vector<content::BrowserContext*> result;
+  std::vector<base::WeakPtr<content::BrowserContext>> result;
   for (const auto& profile_pair : otr_profiles_)
-    result.push_back(profile_pair.second);
+    result.push_back(profile_pair.second->GetWeakPtr());
   return result;
 }
 
diff --git a/chrome/browser/devtools/devtools_browser_context_manager.h b/chrome/browser/devtools/devtools_browser_context_manager.h
index cd2383a..1898b7ee 100644
--- a/chrome/browser/devtools/devtools_browser_context_manager.h
+++ b/chrome/browser/devtools/devtools_browser_context_manager.h
@@ -29,7 +29,7 @@
       const DevToolsBrowserContextManager&) = delete;
 
   Profile* GetProfileById(const std::string& browser_context_id);
-  std::vector<content::BrowserContext*> GetBrowserContexts();
+  std::vector<base::WeakPtr<content::BrowserContext>> GetBrowserContexts();
   content::BrowserContext* GetDefaultBrowserContext();
   content::BrowserContext* CreateBrowserContext();
   void DisposeBrowserContext(
diff --git a/content/browser/devtools/protocol/browser_handler.cc b/content/browser/devtools/protocol/browser_handler.cc
index 41e483e..d027adc1 100644
--- a/content/browser/devtools/protocol/browser_handler.cc
+++ b/content/browser/devtools/protocol/browser_handler.cc
@@ -370,21 +370,18 @@
         "Browser context management is not supported.");
   if (!browser_context_id.has_value()) {
     *browser_context = delegate->GetDefaultBrowserContext();
-    if (*browser_context == nullptr)
+    if (!*browser_context) {
       return Response::ServerError(
           "Browser context management is not supported.");
+    }
     return Response::Success();
   }
-
-  std::string context_id = browser_context_id.value();
-  for (auto* context : delegate->GetBrowserContexts()) {
-    if (context->UniqueId() == context_id) {
-      *browser_context = context;
-      return Response::Success();
-    }
+  *browser_context = delegate->GetBrowserContext(browser_context_id.value());
+  if (!*browser_context) {
+    return Response::InvalidParams("Failed to find browser context for id " +
+                                   browser_context_id.value());
   }
-  return Response::InvalidParams("Failed to find browser context for id " +
-                                 context_id);
+  return Response::Success();
 }
 
 // static
diff --git a/content/browser/devtools/protocol/target_handler.cc b/content/browser/devtools/protocol/target_handler.cc
index f20a1a8..5d6b074 100644
--- a/content/browser/devtools/protocol/target_handler.cc
+++ b/content/browser/devtools/protocol/target_handler.cc
@@ -804,14 +804,15 @@
   }
 
   if (dispose_on_detach_context_ids_.size()) {
-    for (auto* context : delegate->GetBrowserContexts()) {
-      if (!dispose_on_detach_context_ids_.contains(context->UniqueId())) {
-        continue;
-      }
-      delegate->DisposeBrowserContext(context, base::DoNothing());
-    }
+    auto context_ids = std::move(dispose_on_detach_context_ids_);
     dispose_on_detach_context_ids_.clear();
+    for (const std::string& id : context_ids) {
+      if (auto* context = delegate->GetBrowserContext(id)) {
+        delegate->DisposeBrowserContext(context, base::DoNothing());
+      }
+    }
   }
+
   contexts_with_overridden_proxy_.clear();
   return Response::Success();
 }
@@ -1611,16 +1612,19 @@
     return Response::ServerError(
         "Browser context management is not supported.");
   }
-  std::vector<BrowserContext*> contexts = delegate->GetBrowserContexts();
+  std::vector<base::WeakPtr<BrowserContext>> contexts =
+      delegate->GetBrowserContexts();
   *browser_context_ids = std::make_unique<protocol::Array<protocol::String>>();
-  for (auto* context : contexts) {
-    (*browser_context_ids)->emplace_back(context->UniqueId());
+  for (const auto& context : contexts) {
+    if (context) {
+      (*browser_context_ids)->emplace_back(context->UniqueId());
+    }
   }
 
-  BrowserContext* default_context = delegate->GetDefaultBrowserContext();
-  if (default_context) {
+  if (BrowserContext* default_context = delegate->GetDefaultBrowserContext()) {
     *default_browser_context_id = default_context->UniqueId();
   }
+
   return Response::Success();
 }
 
@@ -1638,17 +1642,21 @@
         Response::ServerError("Browser context management is not supported."));
     return;
   }
-  std::vector<BrowserContext*> contexts = delegate->GetBrowserContexts();
-  auto context_it =
-      std::ranges::find(contexts, context_id, &BrowserContext::UniqueId);
-  if (context_it == contexts.end()) {
+  BrowserContext* context = delegate->GetBrowserContext(context_id);
+  if (!context) {
     callback->sendFailure(
         Response::ServerError("Failed to find context with id " + context_id));
     return;
   }
+  if (context == delegate->GetDefaultBrowserContext()) {
+    callback->sendFailure(
+        Response::ServerError("Cannot dispose default browser context."));
+    return;
+  }
   dispose_on_detach_context_ids_.erase(context_id);
   delegate->DisposeBrowserContext(
-      *context_it,
+      context,
+
       base::BindOnce(
           [](std::unique_ptr<DisposeBrowserContextCallback> callback,
              bool success, const std::string& error) {
diff --git a/content/public/browser/devtools_manager_delegate.cc b/content/public/browser/devtools_manager_delegate.cc
index 8257dd01..db9be765 100644
--- a/content/public/browser/devtools_manager_delegate.cc
+++ b/content/public/browser/devtools_manager_delegate.cc
@@ -3,7 +3,9 @@
 // found in the LICENSE file.
 
 #include "content/public/browser/devtools_manager_delegate.h"
+
 #include "base/values.h"
+#include "content/public/browser/browser_context.h"
 #include "content/public/browser/devtools_agent_host.h"
 
 namespace content {
@@ -74,14 +76,20 @@
   return nullptr;
 }
 
-std::vector<BrowserContext*> DevToolsManagerDelegate::GetBrowserContexts() {
-  return std::vector<BrowserContext*>();
+std::vector<base::WeakPtr<BrowserContext>>
+DevToolsManagerDelegate::GetBrowserContexts() {
+  return std::vector<base::WeakPtr<BrowserContext>>();
 }
 
 BrowserContext* DevToolsManagerDelegate::GetDefaultBrowserContext() {
   return nullptr;
 }
 
+BrowserContext* DevToolsManagerDelegate::GetBrowserContext(
+    const std::string& context_id) {
+  return nullptr;
+}
+
 BrowserContext* DevToolsManagerDelegate::CreateBrowserContext() {
   return nullptr;
 }
diff --git a/content/public/browser/devtools_manager_delegate.h b/content/public/browser/devtools_manager_delegate.h
index f08399e..069a7b4 100644
--- a/content/public/browser/devtools_manager_delegate.h
+++ b/content/public/browser/devtools_manager_delegate.h
@@ -11,6 +11,7 @@
 
 #include "base/containers/span.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/memory/weak_ptr.h"
 #include "content/common/content_export.h"
 #include "content/public/browser/devtools_agent_host.h"
 #include "url/gurl.h"
@@ -112,7 +113,8 @@
   enum TargetType { kFrame, kTab };
 
   // Returns all targets embedder would like to report as debuggable remotely.
-  virtual DevToolsAgentHost::List RemoteDebuggingTargets(TargetType target_type);
+  virtual DevToolsAgentHost::List RemoteDebuggingTargets(
+      TargetType target_type);
 
   // Creates new inspectable target given the |url|.
   // |new_window| is currently only used on Android - Desktop platforms handle
@@ -122,12 +124,19 @@
   virtual scoped_refptr<DevToolsAgentHost>
   CreateNewTarget(const GURL& url, TargetType target_type, bool new_window);
 
-  // Get all live browser contexts created by CreateBrowserContext() method.
-  virtual std::vector<BrowserContext*> GetBrowserContexts();
+  // Returns the list of browser contexts exposed to browser automation. This
+  // MUST include contexts previously created via CreateBrowserContext(), but
+  // embedder may choose to omit some contexts that were not created via
+  // DevTools. In particular, default contexts are not exposed by some
+  // embedders at the moment, but this may eventually be revisited.
+  virtual std::vector<base::WeakPtr<BrowserContext>> GetBrowserContexts();
 
   // Get default browser context. May return null if not supported.
   virtual BrowserContext* GetDefaultBrowserContext();
 
+  // Get browser context by its unique id. May return null if not found.
+  virtual BrowserContext* GetBrowserContext(const std::string& context_id);
+
   // Create new browser context. May return null if not supported or not
   // possible. Delegate must take ownership of the created browser context, and
   // may destroy it at will.
diff --git a/fuchsia_web/webengine/browser/web_engine_content_browser_client.cc b/fuchsia_web/webengine/browser/web_engine_content_browser_client.cc
index ddd6ade..868d979 100644
--- a/fuchsia_web/webengine/browser/web_engine_content_browser_client.cc
+++ b/fuchsia_web/webengine/browser/web_engine_content_browser_client.cc
@@ -94,13 +94,31 @@
   DevToolsManagerDelegate& operator=(const DevToolsManagerDelegate&) = delete;
 
   // content::DevToolsManagerDelegate implementation.
-  std::vector<content::BrowserContext*> GetBrowserContexts() override {
-    return main_parts_->browser_contexts();
+  std::vector<base::WeakPtr<content::BrowserContext>> GetBrowserContexts()
+      override {
+    std::vector<base::WeakPtr<content::BrowserContext>> contexts;
+    for (auto* context : main_parts_->browser_contexts()) {
+      contexts.push_back(context->GetWeakPtr());
+    }
+    return contexts;
   }
+
   content::BrowserContext* GetDefaultBrowserContext() override {
-    std::vector<content::BrowserContext*> contexts = GetBrowserContexts();
+    std::vector<content::BrowserContext*> contexts =
+        main_parts_->browser_contexts();
     return contexts.empty() ? nullptr : contexts.front();
   }
+
+  content::BrowserContext* GetBrowserContext(
+      const std::string& context_id) override {
+    for (auto* context : main_parts_->browser_contexts()) {
+      if (context->UniqueId() == context_id) {
+        return context;
+      }
+    }
+    return nullptr;
+  }
+
   content::DevToolsAgentHost::List RemoteDebuggingTargets(
       DevToolsManagerDelegate::TargetType target_type) override {
     LOG_IF(WARNING, target_type != DevToolsManagerDelegate::kFrame)
diff --git a/headless/lib/browser/headless_devtools_manager_delegate.cc b/headless/lib/browser/headless_devtools_manager_delegate.cc
index fabd8dfd..271340f6 100644
--- a/headless/lib/browser/headless_devtools_manager_delegate.cc
+++ b/headless/lib/browser/headless_devtools_manager_delegate.cc
@@ -69,22 +69,35 @@
   sessions_.erase(channel);
 }
 
-std::vector<content::BrowserContext*>
+std::vector<base::WeakPtr<content::BrowserContext>>
 HeadlessDevToolsManagerDelegate::GetBrowserContexts() {
-  std::vector<content::BrowserContext*> contexts;
+  std::vector<base::WeakPtr<content::BrowserContext>> contexts;
   if (!browser_)
     return contexts;
   for (auto* context : browser_->GetAllBrowserContexts()) {
-    if (context != browser_->GetDefaultBrowserContext())
-      contexts.push_back(HeadlessBrowserContextImpl::From(context));
+    if (context != browser_->GetDefaultBrowserContext()) {
+      contexts.push_back(
+          HeadlessBrowserContextImpl::From(context)->GetWeakPtr());
+    }
   }
   return contexts;
 }
+
 content::BrowserContext*
 HeadlessDevToolsManagerDelegate::GetDefaultBrowserContext() {
-  return browser_ ? HeadlessBrowserContextImpl::From(
-                        browser_->GetDefaultBrowserContext())
-                  : nullptr;
+  if (!browser_) {
+    return nullptr;
+  }
+  return HeadlessBrowserContextImpl::From(browser_->GetDefaultBrowserContext());
+}
+
+content::BrowserContext* HeadlessDevToolsManagerDelegate::GetBrowserContext(
+    const std::string& context_id) {
+  if (!browser_) {
+    return nullptr;
+  }
+  return HeadlessBrowserContextImpl::From(
+      browser_->GetBrowserContextForId(context_id));
 }
 
 content::BrowserContext*
diff --git a/headless/lib/browser/headless_devtools_manager_delegate.h b/headless/lib/browser/headless_devtools_manager_delegate.h
index 7b079aa..48dc01bd 100644
--- a/headless/lib/browser/headless_devtools_manager_delegate.h
+++ b/headless/lib/browser/headless_devtools_manager_delegate.h
@@ -42,9 +42,13 @@
   void ClientDetached(
       content::DevToolsAgentHostClientChannel* channel) override;
 
-  std::vector<content::BrowserContext*> GetBrowserContexts() override;
+  std::vector<base::WeakPtr<content::BrowserContext>> GetBrowserContexts()
+      override;
   content::BrowserContext* GetDefaultBrowserContext() override;
+  content::BrowserContext* GetBrowserContext(
+      const std::string& context_id) override;
   content::BrowserContext* CreateBrowserContext() override;
+
   void DisposeBrowserContext(content::BrowserContext* context,
                              DisposeCallback callback) override;
 
diff --git a/chrome/browser/headless/test/headless_mode_protocol_browsertest.cc b/chrome/browser/headless/test/headless_mode_protocol_browsertest.cc
index 5b85610..48249af 100644
--- a/chrome/browser/headless/test/headless_mode_protocol_browsertest.cc
+++ b/chrome/browser/headless/test/headless_mode_protocol_browsertest.cc
@@ -523,4 +523,7 @@
 HEADLESS_MODE_PROTOCOL_TEST(NormalWindowHasOpener,
                             "shared/normal-window-has-opener.js")
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/headless/test/headless_mode_protocol_browsertest.cc b/chrome/browser/headless/test/headless_mode_protocol_browsertest.cc
index 5b85610..48249af 100644
--- a/chrome/browser/headless/test/headless_mode_protocol_browsertest.cc
+++ b/chrome/browser/headless/test/headless_mode_protocol_browsertest.cc
@@ -523,4 +523,7 @@
 HEADLESS_MODE_PROTOCOL_TEST(NormalWindowHasOpener,
                             "shared/normal-window-has-opener.js")
 
+HEADLESS_MODE_PROTOCOL_TEST(GetBrowserContexts,
+                            "shared/get-browser-contexts.js")
+
 }  // namespace headless
diff --git a/components/headless/test/data/protocol/shared/get-browser-contexts-expected.txt b/components/headless/test/data/protocol/shared/get-browser-contexts-expected.txt
new file mode 100644
index 0000000..2784d0c
--- /dev/null
+++ b/components/headless/test/data/protocol/shared/get-browser-contexts-expected.txt
@@ -0,0 +1,11 @@
+Tests Target.getBrowserContexts
+Initial browserContextIds count: 0
+Has defaultBrowserContextId: true
+browserContextIds count after create: 1
+Contains created context: true
+browserContextIds count after dispose: 0
+Attempting to dispose default browser context...
+Error disposing default context: {
+    code : -32000
+    message : Cannot dispose default browser context.
+}
\ No newline at end of file
diff --git a/components/headless/test/data/protocol/shared/get-browser-contexts.js b/components/headless/test/data/protocol/shared/get-browser-contexts.js
new file mode 100644
index 0000000..d6e337a4
--- /dev/null
+++ b/components/headless/test/data/protocol/shared/get-browser-contexts.js
@@ -0,0 +1,44 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  await testRunner.startBlank('Tests Target.getBrowserContexts');
+
+  const browserSession = await testRunner.attachFullBrowserSession();
+  const bp = browserSession.protocol;
+
+  const {result: res1} = await bp.Target.getBrowserContexts();
+  testRunner.log(
+      `Initial browserContextIds count: ${res1.browserContextIds.length}`);
+  testRunner.log(`Has defaultBrowserContextId: ${
+      typeof res1.defaultBrowserContextId === 'string' &&
+      res1.defaultBrowserContextId.length > 0}`);
+
+  const {result: {browserContextId}} = await bp.Target.createBrowserContext();
+
+  const {result: res2} = await bp.Target.getBrowserContexts();
+  testRunner.log(
+      `browserContextIds count after create: ${res2.browserContextIds.length}`);
+  testRunner.log(`Contains created context: ${
+      res2.browserContextIds.includes(browserContextId)}`);
+
+  await bp.Target.disposeBrowserContext({browserContextId});
+
+  const {result: res3} = await bp.Target.getBrowserContexts();
+  testRunner.log(`browserContextIds count after dispose: ${
+      res3.browserContextIds.length}`);
+
+  testRunner.log('Attempting to dispose default browser context...');
+  const disposeDefaultRes = await bp.Target.disposeBrowserContext({
+    browserContextId: res1.defaultBrowserContextId,
+  });
+  const error = disposeDefaultRes.error;
+  if (error && error.message) {
+    error.message = error.message.replace(
+        res1.defaultBrowserContextId, '<defaultBrowserContextId>');
+  }
+  testRunner.log(error, 'Error disposing default context:');
+
+  testRunner.completeTest();
+});
diff --git a/headless/test/data/protocol/sanity/context-disposal-expected.txt b/headless/test/data/protocol/sanity/context-disposal-expected.txt
new file mode 100644
index 0000000..11bbc95b
--- /dev/null
+++ b/headless/test/data/protocol/sanity/context-disposal-expected.txt
@@ -0,0 +1,4 @@
+Tests context disposal on detach from page exposing DevTools protocol.
+Browser contexts count before detach: 65
+Detaching browser session...
+Browser contexts count after detach: 0
diff --git a/headless/test/data/protocol/sanity/context-disposal.js b/headless/test/data/protocol/sanity/context-disposal.js
new file mode 100644
index 0000000..b924333
--- /dev/null
+++ b/headless/test/data/protocol/sanity/context-disposal.js
@@ -0,0 +1,62 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  testRunner.log(
+      'Tests context disposal on detach from page exposing DevTools protocol.');
+
+  // Create extra browser session.
+  const {result: {sessionId: browserSessionId}} =
+      await testRunner.browserP().Target.attachToBrowserTarget({});
+  const browserSession = testRunner.createSessionFor(browserSessionId);
+  const bp = browserSession.protocol;
+
+  const {result: {browserContextId}} =
+      await bp.Target.createBrowserContext({disposeOnDetach: true});
+
+  const {result: {targetId}} = await bp.Target.createTarget({
+    browserContextId,
+    url: 'about:blank',
+  });
+  const {result: {sessionId}} =
+      await bp.Target.attachToTarget({targetId, flatten: true});
+  const session = testRunner.createSessionFor(sessionId);
+
+  await bp.Target.exposeDevToolsProtocol({targetId, bindingName: 'cdp'});
+
+  const testFrameworkURL =
+      new URL(
+          '/resources/inspector-protocol-test-subtarget.html', location.href)
+          .href;
+  await session.navigate(testFrameworkURL);
+
+  await session.evaluateAsync(async () => {
+    const testRunner = new TestRunner(
+        '', '', DevToolsAPI._log, () => {}, DevToolsAPI._fetch, {});
+    const bp = testRunner.browserP();
+    const promises = [];
+    for (let i = 0; i < 64; i++) {
+      promises.push(bp.Target.createBrowserContext({disposeOnDetach: true}));
+    }
+    await Promise.all(promises);
+  });
+
+  const contextsBefore =
+      (await testRunner.browserP().Target.getBrowserContexts())
+          .result.browserContextIds;
+  testRunner.log(
+      `Browser contexts count before detach: ${contextsBefore.length}`);
+
+  testRunner.log('Detaching browser session...');
+  await testRunner.browserP().Target.detachFromTarget(
+      {sessionId: browserSessionId});
+
+  const contextsAfter =
+      (await testRunner.browserP().Target.getBrowserContexts())
+          .result.browserContextIds;
+  testRunner.log(
+      `Browser contexts count after detach: ${contextsAfter.length}\n`);
+
+  testRunner.completeTest();
+});
diff --git a/headless/test/headless_protocol_browsertest.cc b/headless/test/headless_protocol_browsertest.cc
index d99612a..e4b74e3 100644
--- a/headless/test/headless_protocol_browsertest.cc
+++ b/headless/test/headless_protocol_browsertest.cc
@@ -728,6 +728,8 @@
 
 HEADLESS_PROTOCOL_TEST(BlockNewWebContents, "sanity/block-new-web-contents.js")
 
+HEADLESS_PROTOCOL_TEST(ContextDisposal, "sanity/context-disposal.js")
+
 HEADLESS_PROTOCOL_TEST(ScreenRotationSecondaryScreen,
                        "sanity/screen-rotation-secondary-screen.js")
 
@@ -848,4 +850,6 @@
 HEADLESS_PROTOCOL_TEST(GetCanvasContextWebGL,
                        "shared/get-canvas-context-webgl.js")
 
+HEADLESS_PROTOCOL_TEST(GetBrowserContexts, "shared/get-browser-contexts.js")
+
 }  // namespace headless
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.