CVE-2026-95354
Overview
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
URLLoaderFactoryand a reconnector so it can perform network fetches. - `AsyncCertNetFetcherURLLoader`
- The object owned via
impl_insideCertNetFetcherURLLoaderthat performs the actual network fetches over the suppliedURLLoaderFactory. - `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.
CHECK so the impl_ overwrite can no longer occur.Attack Path
- Obtain the service interface
A compromised or malicious client that holds a
mojom::CertVerifierServiceremote drives theEnableNetworkAccessIPC intoCertVerifierServiceImpl. - First initialization
The client calls
EnableNetworkAccess, which populates theCertNetFetcherURLLoader’simpl_with a liveAsyncCertNetFetcherURLLoader. - Duplicate call overwrites state
The client calls
EnableNetworkAccessa second time; before the patch,SetURLLoaderFactoryAndReconnectorreassignsimpl_(theDCHECKbeing a no-op in release), freeing the prior fetcher object. - Use-after-free
Outstanding references to the destroyed fetcher are subsequently dereferenced during certificate-related network activity, yielding a use-after-free
(inferred). - Exploitation
The freed-then-reused allocation is groomed to gain a memory-corruption primitive within the verifier
(inferred).
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifservices/cert_verifier/cert_verifier_service.cc |
modified | |
TEST_Fservices/cert_verifier/cert_verifier_service_unittest.cc |
modified |
Files Changed
services/cert_verifier/cert_verifier_service.ccservices/cert_verifier/BUILD.gnservices/cert_verifier/cert_verifier_service_unittest.ccservices/cert_verifier/cert_verifier_service.hservices/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.ccservices/cert_verifier/cert_net_url_loader/cert_net_fetcher_url_loader.hservices/network/public/mojom/cert_verifier_service.mojom
Audit Directions
- Set-once mojo methodsAudit other mojo interface methods that install or replace owned objects (
impl_, factories, reconnectors) for missing call-once guards and reliance onDCHECKinstead ofCHECK/ReportBadMessage. - DCHECK-guarded invariants on IPC pathsSearch for
DCHECKassertions 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 plumbingReview 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.
Patch
From f27de9c360a07d62328c0444a4c337ca0d4d331c Mon Sep 17 00:00:00 2001 From: David Benjamin <[email protected]> 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 <[email protected]> Reviewed-by: Giovanni Ortuno Urquidi <[email protected]> Reviewed-by: Matt Mueller <[email protected]> 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",
Regression Test / PoC
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
Original Bug Report
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.ccservices/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();
}
auto* job = job_.get();extracts the raw native address.job_ = nullptr;drops the final BRP refcount to 0. PartitionAlloc immediately moves the memory block from quarantine to the active thread cache freelist.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:
- Add a replay guard in
CertVerifierServiceImpl::EnableNetworkAccessto prevent re-initialization if a fetcher is already active. - Ensure
AsyncCertNetFetcherURLLoader::~AsyncCertNetFetcherURLLoaderexplicitly cancels or detaches all active jobs before destruction, soRequestCores are properly notified. - 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::EnableNetworkAccessVerification: Codebase investigation verified thatCertVerifierServiceImpl::EnableNetworkAccess(services/cert_verifier/cert_verifier_service.cc:129) runs on the browser UI thread (viacontent/browser/network_service_instance_impl.cc:834). The method only checksif (cert_net_fetcher_)before callingSetURLLoaderFactoryAndReconnectorunconditionally. There is no call-once guard.DCHECKOptimization Verification: Codebase investigation confirmed thatDCHECK(requests_.empty());inJob::~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 activeJobinstances to destruct silently.JobClass Layout Verification: Codebase investigation confirmed thatJobdoes 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 /
JobSetMap Verification:JobSet(cert_net_fetcher_url_loader.cc:127) is typed asstd::map<Job*, std::unique_ptr<Job>, JobComparator>;. Destruction ofAsyncCertNetFetcherURLLoader::jobs_destroys theunique_ptr, deleting theJobfrom memory. Thus,RequestCore::job_(araw_ptr) is definitively the final pointer keeping theJobmemory quarantined by BRP. Thejob_ = nullptrassignment drops the final refcount to 0 before thejob->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.