Medium watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Verifier
DescriptionUse after free in Verifier
ComponentVerifier
Sub-componentInternals>Network>Certificate>Verifier
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.
Tracker524582798
Fix commitf27de9c360a0 (chromium/src) +57/-7
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-16 (per vendor advisory)
Fix landed2026-08-26 (public git)
Disclosed2026-09-22
Patch gap27 days — fix public in source before the advisory

Background

`CertVerifierServiceImpl`
The browser’s certificate-verification service implementation that verifies TLS certificates and can be granted network access for fetching AIA/OCSP data.
`EnableNetworkAccess`
A mojo method on the cert verifier service that hands the service a URLLoaderFactory and a reconnector so it can perform network fetches.
`AsyncCertNetFetcherURLLoader`
The object owned via impl_ inside CertNetFetcherURLLoader that performs the actual network fetches over the supplied URLLoaderFactory.
`mojo::ReportBadMessage`
A mojo mechanism that flags an invalid IPC from a client and tears down the offending connection instead of trusting the message.

Root Cause Analysis

The mojo interface EnableNetworkAccess was implemented in CertVerifierServiceImpl::EnableNetworkAccess without any guard against being called more than once. On a second call, SetURLLoaderFactoryAndReconnector on the associated CertNetFetcherURLLoader would replace an already-populated impl_ (the AsyncCertNetFetcherURLLoader), and the existing DCHECK(!impl_) compiled away in release builds, so the previous fetcher state could be destroyed and reassigned while references to it were still live, producing a use-after-free.

The fix adds a persistent enable_network_access_called_ flag: the first call sets it, and any subsequent call invokes mojo::ReportBadMessage("EnableNetworkAccess was already called") and returns early, so the second reassignment never happens. It also hardens SetURLLoaderFactoryAndReconnector by upgrading DCHECK(!impl_) to CHECK(!impl_), making the single-assignment invariant enforced even in release builds.

Key insight
The single mistake was treating a set-once initialization method as idempotently re-callable; the fix enforces call-once semantics at the mojo boundary (rejecting the duplicate as a bad message) and backs it with a release-mode CHECK so the impl_ overwrite can no longer occur.

Attack Path

  1. Obtain the service interface A compromised or malicious client that holds a mojom::CertVerifierService remote drives the EnableNetworkAccess IPC into CertVerifierServiceImpl.
  2. First initialization The client calls EnableNetworkAccess, which populates the CertNetFetcherURLLoader’s impl_ with a live AsyncCertNetFetcherURLLoader.
  3. Duplicate call overwrites state The client calls EnableNetworkAccess a second time; before the patch, SetURLLoaderFactoryAndReconnector reassigns impl_ (the DCHECK being a no-op in release), freeing the prior fetcher object.
  4. Use-after-free Outstanding references to the destroyed fetcher are subsequently dereferenced during certificate-related network activity, yielding a use-after-free (inferred).
  5. Exploitation The freed-then-reused allocation is groomed to gain a memory-corruption primitive within the verifier (inferred).

Impact Assessment

An attacker who can send IPC to the certificate verifier service triggers a use-after-free in the process hosting CertVerifierServiceImpl, with the sole precondition being the ability to invoke EnableNetworkAccess twice on a held service remote. The advisory classifies this as a use-after-free in the Verifier and states no exploited-in-the-wild status; any escalation to code execution or sandbox compromise is (inferred) rather than demonstrated by the patch.

Changed Functions

FunctionChangeNotes
if
services/cert_verifier/cert_verifier_service.cc
modified
TEST_F
services/cert_verifier/cert_verifier_service_unittest.cc
modified

Files Changed

  • services/cert_verifier/cert_verifier_service.cc
  • services/cert_verifier/BUILD.gn
  • services/cert_verifier/cert_verifier_service_unittest.cc
  • services/cert_verifier/cert_verifier_service.h
  • services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc
  • services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.h
  • services/network/public/mojom/cert_verifier_service.mojom

Audit Directions

  • Set-once mojo methods
    Audit other mojo interface methods that install or replace owned objects (impl_, factories, reconnectors) for missing call-once guards and reliance on DCHECK instead of CHECK/ReportBadMessage.
  • DCHECK-guarded invariants on IPC paths
    Search for DCHECK assertions protecting single-assignment or lifetime invariants that are reachable from untrusted IPC, since these vanish in release builds and can become UAF or overwrite bugs.
  • Reinitialization of network-access plumbing
    Review services that lazily grant network access (URLLoaderFactory/reconnector handoffs) for cases where a second grant can free or dangle references held by in-flight requests.
From f27de9c360a07d62328c0444a4c337ca0d4d331c Mon Sep 17 00:00:00 2001
From: David Benjamin <davidben@chromium.org>
Date: Wed, 26 Aug 2026 14:30:56 -0700
Subject: [PATCH] Forbid duplicate calls to EnableNetworkAccess

Fixed: 524582798
Change-Id: I7fa39ccffb8af68176813a21be8c24c04719806b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8285963
Commit-Queue: David Benjamin <davidben@chromium.org>
Reviewed-by: Giovanni Ortuno Urquidi <ortuno@chromium.org>
Reviewed-by: Matt Mueller <mattm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1686700}
---
diff --git a/services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc b/services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc
index 26aa75c..8ec90db 100644
--- a/services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc
+++ b/services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc
@@ -762,7 +762,7 @@
     base::RepeatingCallback<
         void(mojo::PendingReceiver<network::mojom::URLLoaderFactory>)>
         bind_new_url_loader_factory_cb) {
-  DCHECK(!impl_);
+  CHECK(!impl_);
   impl_ = std::make_unique<AsyncCertNetFetcherURLLoader>(
       std::move(factory), std::move(bind_new_url_loader_factory_cb));
 }
diff --git a/services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.h b/services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.h
index a6de71f..a0d7483 100644
--- a/services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.h
+++ b/services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.h
@@ -37,8 +37,8 @@
   // If the other side of the |factory| remote disconnects, the
   // CertNetFetcherURLLoader will attempt to reconnect using
   // |bind_new_url_loader_factory_cb|. This must be called before ever
-  // performing a fetch. It is recommended, but not required, to provide a
-  // functional |bind_new_url_loader_factory_cb|.
+  // performing a fetch and can only be called once. It is recommended, but not
+  // required, to provide a functional |bind_new_url_loader_factory_cb|.
   void SetURLLoaderFactoryAndReconnector(
       mojo::PendingRemote<network::mojom::URLLoaderFactory> factory,
       base::RepeatingCallback<
diff --git a/services/cert_verifier/cert_verifier_service.cc b/services/cert_verifier/cert_verifier_service.cc
index 33b5276..aeff2b4fb 100644
--- a/services/cert_verifier/cert_verifier_service.cc
+++ b/services/cert_verifier/cert_verifier_service.cc
@@ -129,6 +129,12 @@
 void CertVerifierServiceImpl::EnableNetworkAccess(
     mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory,
     mojo::PendingRemote<mojom::URLLoaderFactoryConnector> reconnector) {
+  if (enable_network_access_called_) {
+    mojo::ReportBadMessage("EnableNetworkAccess was already called");
+    return;
+  }
+  enable_network_access_called_ = true;
+
   if (cert_net_fetcher_) {
     auto reconnect_cb =
         std::make_unique<mojo::Remote<mojom::URLLoaderFactoryConnector>>(
diff --git a/services/cert_verifier/cert_verifier_service.h b/services/cert_verifier/cert_verifier_service.h
index 32b7b17..b9c56965 100644
--- a/services/cert_verifier/cert_verifier_service.h
+++ b/services/cert_verifier/cert_verifier_service.h
@@ -124,6 +124,7 @@
       service_factory_impl_;
   // Will queue requests for processing until this is false.
   bool waiting_for_update_;
+  bool enable_network_access_called_ = false;
   std::vector<QueuedCertVerifyRequest> queued_requests_;
   WaitUntilNextUpdateForTestingCallback update_complete_callback_;
 };
diff --git a/services/network/public/mojom/cert_verifier_service.mojom b/services/network/public/mojom/cert_verifier_service.mojom
index 0ff067d..eb31820 100644
--- a/services/network/public/mojom/cert_verifier_service.mojom
+++ b/services/network/public/mojom/cert_verifier_service.mojom
@@ -44,8 +44,9 @@
   // |url_loader_factory| allows the CertVerifierService to connect to the
   // network for things like AIA or OCSP. |reconnector| allows the CertVerifier
   // to reconnect its URLLoaderFactory in case the network service disconnects
-  // its URLLoaderFactories without crashing. Must be called before Verify() to
-  // have an effect.
+  // its URLLoaderFactories without crashing. May only be called once, and
+  // must be called before Verify() to have an effect. Calling this more than
+  // once is invalid and will report a bad message.
   EnableNetworkAccess(
       pending_remote<network.mojom.URLLoaderFactory> url_loader_factory,
       pending_remote<URLLoaderFactoryConnector>? reconnector);
diff --git a/services/cert_verifier/cert_verifier_service_unittest.cc b/services/cert_verifier/cert_verifier_service_unittest.cc
index b195a383..51a9102 100644
--- a/services/cert_verifier/cert_verifier_service_unittest.cc
+++ b/services/cert_verifier/cert_verifier_service_unittest.cc
@@ -26,6 +26,7 @@
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
 #include "net/base/net_errors.h"
 #include "net/cert/cert_status_flags.h"
 #include "net/cert/cert_verifier.h"
@@ -37,6 +38,7 @@
 #include "net/log/net_log.h"
 #include "net/test/cert_test_util.h"
 #include "net/test/test_data_directory.h"
+#include "services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.h"
 #include "services/cert_verifier/cert_verifier_service_factory.h"
 #include "services/network/public/mojom/cert_verifier_service.mojom.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -224,7 +226,9 @@
             std::make_unique<CertVerifierServiceFactoryImpl>(
                 cv_service_factory_remote_.BindNewPipeAndPassReceiver())) {}
 
-  void CreateImpl(bool wait_for_update) {
+  void CreateImpl(
+      bool wait_for_update,
+      scoped_refptr<CertNetFetcherURLLoader> cert_net_fetcher = nullptr) {
     dummy_cv_ = new DummyCertVerifier;
     // NOTE: CertVerifierServiceImpl is self-deleting.
     auto* cvs_impl = new internal::CertVerifierServiceImpl(
@@ -232,7 +236,7 @@
         cv_service_remote_.BindNewPipeAndPassReceiver(),
         cv_service_updater_remote_.BindNewPipeAndPassReceiver(),
         cv_service_client_.BindNewPipeAndPassRemote(),
-        /*cert_net_fetcher=*/nullptr,
+        std::move(cert_net_fetcher),
         /*instance_params=*/{},
         /*wait_for_update=*/wait_for_update);
 
@@ -595,4 +599,41 @@
   ASSERT_FALSE(cv_service_req.is_completed);
 }
 
+TEST_F(CertVerifierServiceTest, EnableNetworkAccessTwiceReportsBadMessage) {
+  CreateImpl(/*wait_for_update=*/false);
+  mojo::test::BadMessageObserver bad_message_observer;
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote1;
+  std::ignore = pending_remote1.InitWithNewPipeAndPassReceiver();
+  cv_service_remote()->EnableNetworkAccess(std::move(pending_remote1),
+                                           mojo::NullRemote());
+
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote2;
+  std::ignore = pending_remote2.InitWithNewPipeAndPassReceiver();
+  cv_service_remote()->EnableNetworkAccess(std::move(pending_remote2),
+                                           mojo::NullRemote());
+
+  EXPECT_EQ("EnableNetworkAccess was already called",
+            bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(CertVerifierServiceTest,
+       EnableNetworkAccessTwiceWithFetcherReportsBadMessage) {
+  CreateImpl(/*wait_for_update=*/false,
+             base::MakeRefCounted<CertNetFetcherURLLoader>());
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote1;
+  std::ignore = pending_remote1.InitWithNewPipeAndPassReceiver();
+  cv_service_remote()->EnableNetworkAccess(std::move(pending_remote1),
+                                           mojo::NullRemote());
+
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote2;
+  std::ignore = pending_remote2.InitWithNewPipeAndPassReceiver();
+  cv_service_remote()->EnableNetworkAccess(std::move(pending_remote2),
+                                           mojo::NullRemote());
+
+  EXPECT_EQ("EnableNetworkAccess was already called",
+            bad_message_observer.WaitForBadMessage());
+}
+
 }  // namespace cert_verifier
diff --git a/services/cert_verifier/BUILD.gn b/services/cert_verifier/BUILD.gn
index 4342e3d..ebff4a03 100644
--- a/services/cert_verifier/BUILD.gn
+++ b/services/cert_verifier/BUILD.gn
@@ -52,6 +52,7 @@
     "//base/test:test_support",
     "//crypto:test_support",
     "//mojo/public/cpp/base:protobuf_support",
+    "//mojo/public/cpp/test_support:test_utils",
     "//net",
     "//net:test_support",
     "//net/cert:root_store_proto_lite",
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/cert_verifier/cert_verifier_service_unittest.cc b/services/cert_verifier/cert_verifier_service_unittest.cc
index b195a383..51a9102 100644
--- a/services/cert_verifier/cert_verifier_service_unittest.cc
+++ b/services/cert_verifier/cert_verifier_service_unittest.cc
@@ -26,6 +26,7 @@
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
 #include "net/base/net_errors.h"
 #include "net/cert/cert_status_flags.h"
 #include "net/cert/cert_verifier.h"
@@ -37,6 +38,7 @@
 #include "net/log/net_log.h"
 #include "net/test/cert_test_util.h"
 #include "net/test/test_data_directory.h"
+#include "services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.h"
 #include "services/cert_verifier/cert_verifier_service_factory.h"
 #include "services/network/public/mojom/cert_verifier_service.mojom.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -224,7 +226,9 @@
             std::make_unique<CertVerifierServiceFactoryImpl>(
                 cv_service_factory_remote_.BindNewPipeAndPassReceiver())) {}
 
-  void CreateImpl(bool wait_for_update) {
+  void CreateImpl(
+      bool wait_for_update,
+      scoped_refptr<CertNetFetcherURLLoader> cert_net_fetcher = nullptr) {
     dummy_cv_ = new DummyCertVerifier;
     // NOTE: CertVerifierServiceImpl is self-deleting.
     auto* cvs_impl = new internal::CertVerifierServiceImpl(
@@ -232,7 +236,7 @@
         cv_service_remote_.BindNewPipeAndPassReceiver(),
         cv_service_updater_remote_.BindNewPipeAndPassReceiver(),
         cv_service_client_.BindNewPipeAndPassRemote(),
-        /*cert_net_fetcher=*/nullptr,
+        std::move(cert_net_fetcher),
         /*instance_params=*/{},
         /*wait_for_update=*/wait_for_update);
 
@@ -595,4 +599,41 @@
   ASSERT_FALSE(cv_service_req.is_completed);
 }
 
+TEST_F(CertVerifierServiceTest, EnableNetworkAccessTwiceReportsBadMessage) {
+  CreateImpl(/*wait_for_update=*/false);
+  mojo::test::BadMessageObserver bad_message_observer;
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote1;
+  std::ignore = pending_remote1.InitWithNewPipeAndPassReceiver();
+  cv_service_remote()->EnableNetworkAccess(std::move(pending_remote1),
+                                           mojo::NullRemote());
+
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote2;
+  std::ignore = pending_remote2.InitWithNewPipeAndPassReceiver();
+  cv_service_remote()->EnableNetworkAccess(std::move(pending_remote2),
+                                           mojo::NullRemote());
+
+  EXPECT_EQ("EnableNetworkAccess was already called",
+            bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(CertVerifierServiceTest,
+       EnableNetworkAccessTwiceWithFetcherReportsBadMessage) {
+  CreateImpl(/*wait_for_update=*/false,
+             base::MakeRefCounted<CertNetFetcherURLLoader>());
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote1;
+  std::ignore = pending_remote1.InitWithNewPipeAndPassReceiver();
+  cv_service_remote()->EnableNetworkAccess(std::move(pending_remote1),
+                                           mojo::NullRemote());
+
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote2;
+  std::ignore = pending_remote2.InitWithNewPipeAndPassReceiver();
+  cv_service_remote()->EnableNetworkAccess(std::move(pending_remote2),
+                                           mojo::NullRemote());
+
+  EXPECT_EQ("EnableNetworkAccess was already called",
+            bad_message_observer.WaitForBadMessage());
+}
+
 }  // namespace cert_verifier
Loading diff…

Original Bug Report

reported by aw...@chromium.org

Potential browser process UAF crash via replayable CertVerifierService::EnableNetworkAccess

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A compromised network process can replay the EnableNetworkAccess Mojo IPC, destroying in-flight jobs and leaving dangling pointers in RequestCore. When the request is cancelled, a MiraclePtr-bypassing Use-After-Free occurs on the UI thread. Because the free and dereference are consecutive instructions, this deterministically crashes the browser process (DoS).

Affected files:

  • services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc
  • services/cert_verifier/cert_verifier_service.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

1. Summary of the Issue (Meant for Human Triage)

A potential Use-After-Free (UAF) vulnerability exists in the browser process within the CertNetFetcherURLLoader component. This issue can be triggered by an attacker who has compromised the sandboxed network service process.

The Mojo interface mojom::CertVerifierService::EnableNetworkAccess is hosted on the browser UI thread but lacks a replay guard. A compromised network service process holding this Mojo remote can invoke EnableNetworkAccess multiple times. In release builds of Chrome (where DCHECK macros are compiled out), doing so replaces the existing AsyncCertNetFetcherURLLoader and destroys its in-flight Job objects without notifying or detaching their corresponding RequestCore structures. This leaves any active RequestCore with a dangling raw_ptr<Job> job_ pointing to the deleted memory, quarantined by BackupRefPtr (BRP).

When a request is subsequently cancelled (e.g., due to a path building timeout), RequestCore::CancelJobOnTaskRunner is executed on the UI thread. This method executes a MiraclePtr-bypassing anti-pattern: it extracts the raw native pointer, clears the raw_ptr member variable to null (which drops the BRP refcount to 0 and immediately frees the quarantined allocation), and then dereferences the freed native pointer to call DetachRequest.

Because the free and the subsequent dereference are consecutive instructions on the same thread, PartitionAlloc handles the free synchronously via its thread cache. There is zero window for an attacker to reclaim the freed memory. The first member of the destroyed Job object is its requests_ vector (at offset 0), which PartitionAlloc immediately overwrites with an encoded freelist pointer. The subsequent call to std::ranges::find inside DetachRequest dereferences this invalid pointer and deterministically crashes the browser process. As a result, this issue strictly manifests as a browser-process Denial of Service (DoS), rather than an exploitable Sandbox Escape or Remote Code Execution.


2. Proof-of-Concept & Detailed Execution Flow

Note: Our tooling agent does not have the ability to run code, so these are the potential, verified sequence of steps an attacker would follow to trigger the vulnerability based on static analysis.

An attacker who has compromised the sandboxed network process can trigger this vulnerability through the following execution path:

Step 1: Stalling Certificate Fetches The attacker triggers certificate verification with a maliciously crafted certificate containing multiple Authority Information Access (AIA) caIssuers URIs. The Browser process initiates network fetches for these URIs, creating RequestCore and Job objects on the UI thread (services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc). The RequestCore retains a raw_ptr<Job> job_ to the Job. The attacker, acting as the URLLoaderFactory for these fetches from the compromised network service, intentionally stalls the HTTP responses to keep the Job objects alive while pushing the path builder toward its iteration limit or timeout.

Step 2: Replaying EnableNetworkAccess While the stalled Job and RequestCore objects are active, the compromised network service replays the EnableNetworkAccess Mojo IPC to CertVerifierServiceImpl.

  • File: services/cert_verifier/cert_verifier_service.cc
  • Function: CertVerifierServiceImpl::EnableNetworkAccess (lines 129-141)
void CertVerifierServiceImpl::EnableNetworkAccess(
    mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory,
    mojo::PendingRemote<mojom::URLLoaderFactoryConnector> reconnector) {
  if (cert_net_fetcher_) {
    ...
    cert_net_fetcher_->SetURLLoaderFactoryAndReconnector(std::move(url_loader_factory), ...);
  }
}

This unconditionally calls SetURLLoaderFactoryAndReconnector on the UI thread.

Step 3: Destructing the Old Fetcher and Jobs Without Notification

  • File: services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc
  • Function: CertNetFetcherURLLoader::SetURLLoaderFactoryAndReconnector (lines 760-768)
void CertNetFetcherURLLoader::SetURLLoaderFactoryAndReconnector(...) {
  DCHECK(!impl_);
  impl_ = std::make_unique<AsyncCertNetFetcherURLLoader>(...);
}

In release builds, DCHECK(!impl_) is optimized out. Reassigning impl_ destroys the previous AsyncCertNetFetcherURLLoader instance, which in turn destroys its jobs_ map (std::map<Job*, std::unique_ptr<Job>>). Destroying the unique_ptrs invokes Job::~Job() (lines 460-462):

Job::~Job() {
  DCHECK(requests_.empty());
}

Because DCHECK(requests_.empty()) is completely compiled out in release builds, the active Job objects are deleted without notifying their attached RequestCores. The RequestCore objects now hold dangling raw_ptr<Job> job_ pointers, keeping the memory quarantined via BRP.

Step 4: Cancellation and MiraclePtr Bypass On the worker thread, the path builder hits its timeout and discards the request, triggering core_->CancelJob(), which posts CancelJobOnTaskRunner to the UI thread.

  • File: services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc
  • Function: RequestCore::CancelJobOnTaskRunner (lines 444-449)
void CertNetFetcherURLLoader::RequestCore::CancelJobOnTaskRunner() {
  if (job_) {
    auto* job = job_.get();
    job_ = nullptr;
    job->DetachRequest(this);
  }
  SignalImmediateError();
}
  1. auto* job = job_.get(); extracts the raw native address.
  2. job_ = nullptr; drops the final BRP refcount to 0. PartitionAlloc immediately moves the memory block from quarantine to the active thread cache freelist.
  3. job->DetachRequest(this); dereferences the freed native pointer.

Step 5: Dereferencing Overwritten Memory

  • File: services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc
  • Function: Job::DetachRequest (lines 470-481)
void Job::DetachRequest(CertNetFetcherURLLoader::RequestCore* request) {
  std::unique_ptr<Job> delete_this;
  auto it = std::ranges::find(requests_, request);
  CHECK(it != requests_.end());
  ...

The Job class has no virtual methods or base classes, meaning its std::vector member requests_ resides exactly at offset 0. When PartitionAlloc freed the memory in the prior instruction, it overwrote offset 0 with an encoded freelist pointer. The std::ranges::find call reads this encoded pointer as the vector’s backing memory, resulting in a deterministic SIGSEGV / crash in the Browser Process.

Suggested Fix:

  1. Add a replay guard in CertVerifierServiceImpl::EnableNetworkAccess to prevent re-initialization if a fetcher is already active.
  2. Ensure AsyncCertNetFetcherURLLoader::~AsyncCertNetFetcherURLLoader explicitly cancels or detaches all active jobs before destruction, so RequestCores are properly notified.
  3. In RequestCore::CancelJobOnTaskRunner, change the order of operations to avoid the BRP bypass:
    if (job_) {
      job_->DetachRequest(this);
      job_ = nullptr;
    }
    

3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)

Prior Critic Verdict: > “The report accurately identifies a UAF in RequestCore::CancelJobOnTaskRunner that bypasses MiraclePtr due to the ‘get(), clear(), dereference’ pattern. However, as the researcher correctly notes, there is no window for reallocation because the free and the subsequent dereference are consecutive instructions on the same thread. Furthermore, the destroyed Job object has its ‘requests_’ vector at offset 0, which is immediately overwritten by PartitionAlloc’s encoded freelist pointer when truly freed. The subsequent call to std::ranges::find will reliably crash the browser process by dereferencing this invalid pointer. Even if BRP keeps the object quarantined (multiple requests attached), the vector’s backing buffer was still freed, leading to a CHECK failure or crash. Since this requires a compromised network process and strictly results in a Denial of Service, it is not exploitable for RCE or sandbox escape.”

Automated Audit Logs:

  • CertVerifierServiceImpl::EnableNetworkAccess Verification: Codebase investigation verified that CertVerifierServiceImpl::EnableNetworkAccess (services/cert_verifier/cert_verifier_service.cc:129) runs on the browser UI thread (via content/browser/network_service_instance_impl.cc:834). The method only checks if (cert_net_fetcher_) before calling SetURLLoaderFactoryAndReconnector unconditionally. There is no call-once guard.
  • DCHECK Optimization Verification: Codebase investigation confirmed that DCHECK(requests_.empty()); in Job::~Job() (services/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.cc:460-462) is compiled completely out in release builds. DCHECK_IS_ON() evaluates to false, causing the macro to expand to an optimized-out ternary expression (true ? (void)0 : ...). This guarantees no runtime overhead and zero enforcement, allowing active Job instances to destruct silently.
  • Job Class Layout Verification: Codebase investigation confirmed that Job does not inherit from any base classes and lacks virtual methods. Therefore, its first declared member, std::vector<scoped_refptr<CertNetFetcherURLLoader::RequestCore>> requests_;, is verifiably situated exactly at offset 0 of the object memory layout.
  • BRP / JobSet Map Verification: JobSet (cert_net_fetcher_url_loader.cc:127) is typed as std::map<Job*, std::unique_ptr<Job>, JobComparator>;. Destruction of AsyncCertNetFetcherURLLoader::jobs_ destroys the unique_ptr, deleting the Job from memory. Thus, RequestCore::job_ (a raw_ptr) is definitively the final pointer keeping the Job memory quarantined by BRP. The job_ = nullptr assignment drops the final refcount to 0 before the job->DetachRequest(this) dereference occurs on the subsequent instruction.

Evaluated with Chrome root at commit: 8c517fbcbb533e59ec9cedac868c8a9bdc30beb2


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker