CVE-2026-113037
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/url_request/url_request_http_job.cc |
modified | |
TEST_Fnet/url_request/url_request_http_job_unittest.cc |
modified |
Files Changed
net/url_request/url_request_http_job.ccnet/url_request/url_request_http_job.hnet/http/http_transaction_test_util.hnet/url_request/url_request_http_job_unittest.cc
Patch
From 1985cb7b30ce45730451a3f23fb23cb5e4b6ceb1 Mon Sep 17 00:00:00 2001
From: Adam Rice <ricea@chromium.org>
Date: Thu, 17 Sep 2026 00:42:45 -0700
Subject: [PATCH] Call URLRequestHttpJob::NotifyConnectedCallback via WeakPtr
net::URLRequestHttpJob::NotifyConnectedCallback was called via an
unretained `this` pointer. However, in some cases it could be called
after the job had been destroyed. Call it via a WeakPtr instead.
BUG=560939312
Change-Id: I1c289ec5797d97892ff51bde4866755a709cb458
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8412191
Commit-Queue: Adam Rice <ricea@chromium.org>
Reviewed-by: Nidhi Jaju <nidhijaju@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699580}
---
diff --git a/net/http/http_transaction_test_util.h b/net/http/http_transaction_test_util.h
index 24c6b81..28b1a54c 100644
--- a/net/http/http_transaction_test_util.h
+++ b/net/http/http_transaction_test_util.h
@@ -264,6 +264,10 @@
RequestPriority priority() const { return priority_; }
+ const ConnectedCallback& connected_callback() const {
+ return connected_callback_;
+ }
+
base::WeakPtr<MockNetworkTransaction> AsWeakPtr() {
return weak_factory_.GetWeakPtr();
}
diff --git a/net/url_request/url_request_http_job.cc b/net/url_request/url_request_http_job.cc
index f93d02b8..fc2c9a8 100644
--- a/net/url_request/url_request_http_job.cc
+++ b/net/url_request/url_request_http_job.cc
@@ -558,9 +558,13 @@
}
int URLRequestHttpJob::NotifyConnectedCallback(
+ base::WeakPtr<URLRequestHttpJob> job,
const TransportInfo& info,
CompletionOnceCallback callback) {
- return URLRequestJob::NotifyConnected(info, std::move(callback));
+ if (!job) {
+ return ERR_ABORTED;
+ }
+ return job->NotifyConnected(info, std::move(callback));
}
PrivacyMode URLRequestHttpJob::DeterminePrivacyMode() const {
@@ -747,8 +751,9 @@
}
if (rv == OK) {
- transaction_->SetConnectedCallback(base::BindRepeating(
- &URLRequestHttpJob::NotifyConnectedCallback, base::Unretained(this)));
+ transaction_->SetConnectedCallback(
+ base::BindRepeating(&URLRequestHttpJob::NotifyConnectedCallback,
+ weak_factory_.GetWeakPtr()));
transaction_->SetRequestHeadersCallback(request_headers_callback_);
transaction_->SetEarlyResponseHeadersCallback(
early_response_headers_callback_);
diff --git a/net/url_request/url_request_http_job.h b/net/url_request/url_request_http_job.h
index 566ff74..8bbc3e8 100644
--- a/net/url_request/url_request_http_job.h
+++ b/net/url_request/url_request_http_job.h
@@ -158,9 +158,11 @@
const std::optional<HttpRequestHeaders>& headers);
// This just forwards the call to URLRequestJob::NotifyConnected().
// We need it because that method is protected and cannot be bound in a
- // callback in this class.
- int NotifyConnectedCallback(const TransportInfo& info,
- CompletionOnceCallback callback);
+ // callback in this class. It is static because base::BindRepeating only
+ // supports binding WeakPtr directly to methods with a void return type.
+ static int NotifyConnectedCallback(base::WeakPtr<URLRequestHttpJob> job,
+ const TransportInfo& info,
+ CompletionOnceCallback callback);
void RestartTransaction();
#if BUILDFLAG(ENABLE_DEVICE_BOUND_SESSIONS)
diff --git a/net/url_request/url_request_http_job_unittest.cc b/net/url_request/url_request_http_job_unittest.cc
index 1c61450..f6afb1d1 100644
--- a/net/url_request/url_request_http_job_unittest.cc
+++ b/net/url_request/url_request_http_job_unittest.cc
@@ -1194,6 +1194,18 @@
EXPECT_EQ(HIGHEST, network_layer().last_transaction()->priority());
}
+TEST_F(URLRequestHttpJobTest, ConnectedCallbackAfterDestruction) {
+ req_->Start();
+ ASSERT_TRUE(network_layer().last_transaction());
+ HttpTransaction::ConnectedCallback callback =
+ network_layer().last_transaction()->connected_callback();
+ ASSERT_TRUE(callback);
+
+ req_.reset();
+
+ EXPECT_EQ(ERR_ABORTED, callback.Run(TransportInfo(), base::DoNothing()));
+}
+
TEST_F(URLRequestHttpJobTest, HSTSInternalRedirectTest) {
// Setup HSTS state.
context_->transport_security_state()->AddHSTS(
Regression Test / PoC
diff --git a/net/url_request/url_request_http_job_unittest.cc b/net/url_request/url_request_http_job_unittest.cc
index 1c61450..f6afb1d1 100644
--- a/net/url_request/url_request_http_job_unittest.cc
+++ b/net/url_request/url_request_http_job_unittest.cc
@@ -1194,6 +1194,18 @@
EXPECT_EQ(HIGHEST, network_layer().last_transaction()->priority());
}
+TEST_F(URLRequestHttpJobTest, ConnectedCallbackAfterDestruction) {
+ req_->Start();
+ ASSERT_TRUE(network_layer().last_transaction());
+ HttpTransaction::ConnectedCallback callback =
+ network_layer().last_transaction()->connected_callback();
+ ASSERT_TRUE(callback);
+
+ req_.reset();
+
+ EXPECT_EQ(ERR_ABORTED, callback.Run(TransportInfo(), base::DoNothing()));
+}
+
TEST_F(URLRequestHttpJobTest, HSTSInternalRedirectTest) {
// Setup HSTS state.
context_->transport_security_state()->AddHSTS(
Original Bug Report
Use-after-free in HttpNetworkTransaction::ResumeAfterConnected when a LocalNetworkAccess permission reply arrives after the 240s WebSocket handshake-timeout teardown
VULNERABILITY DETAILS
UAF in HttpNetworkTransaction::ResumeAfterConnected when an LNA permission reply arrives after the 240s WebSocket handshake-timeout teardown. Network service, heap-use-after-free WRITE. Line refs against ToT (checkout 2026-09-13).
- The connect continuation is stashed with an Unretained transaction. When the handshake’s transport completes:
- network::WebSocket parks it behind an LNA ask guarded by ONLY a WeakPtr<WebSocket>: The guard protects the WebSocket, but the callback captures an Unretained HttpNetworkTransaction — a different object, different lifetime. Nothing invalidates the continuation when the transaction dies.
- The 240s timer frees the transaction synchronously:
// net/websockets/websocket_stream.cc:338-340
void OnTimeout() { url_request_->CancelWithError(ERR_TIMED_OUT); }
// chain: CancelWithError -> URLRequestHttpJob::Kill ->
// net/url_request/url_request_http_job.cc:637-650
void URLRequestHttpJob::DestroyTransaction() {
...
transaction_.reset(); // <-- HttpNetworkTransaction FREED
}
- The WebSocket dies one task later — Kill only posts completion: // net/url_request/url_request_job.cc:606-612
if (notify_done) {
TaskRunner(request_->priority())
->PostTask(FROM_HERE, base::BindOnce(&URLRequestJob::NotifyDone, ...));
}
NotifyDone → … → ~WebSocket (where WeakPtrs expire) runs in that later task. Between (3) and (4) the transaction is freed but weak_self is still valid — a reply landing there passes its only guard:
Root cause: the late-reply guard checks the lifetime of the wrong object; the mojo-held closure outlives the transaction because neither CancelWithError, Kill, nor DestroyTransaction invalidates the pending connected_callback_ stash.
Bisect
Introduced by: 9798186fd45a5 (2025-09-04, initial LNA check for WebSockets — added the WeakPtr-guarded lambda + ERR_IO_PENDING); default-reachable since 53bd4f687e11 (2026-03-19). Still present at ToT. The guard protects the WebSocket, but the callback captures an Unretained HttpNetworkTransaction — a different object, different lifetime. Nothing invalidates the continuation when the transaction dies.
VERSION
Chrome Version: Head latest asan chrome reelase Operating System: Linux
REPRODUCTION CASE
Follow the exact steps below to repro the bug. This requires a bit of setup for the server.
1 — Certs (2-day validity):
openssl ecparam -name prime256v1 -genkey -noout -out key.pem
openssl req -new -x509 -key key.pem -out cert.pem -days 2 -subj "/CN=test" \
-addext "subjectAltName=IP:172.28.0.2,DNS:test"
2 — Start the LAN-box container:
docker network create --subnet 172.28.0.0/24 lnanet
docker build -t lna-target .
docker run -d --name lna-target --net lnanet --ip 172.28.0.2 \
--cap-add=NET_ADMIN --sysctl net.ipv4.tcp_synack_retries=127 lna-target
3 — One host sysctl (determinism knob only — default 6 abandons the 240s-delayed connect at ~127s; restore to 6 after):
sudo sysctl -w net.ipv4.tcp_syn_retries=8
4 — Launch 2-3 chromes at the self-driving page:
CHROME=$PWD/out/asan/chrome
for N in 1 2 3; do
ASAN_OPTIONS="detect_leaks=0:detect_odr_violation=0:abort_on_error=0" \
$CHROME --user-data-dir=/tmp/lna-v$N --headless=new --no-first-run --disable-gpu \
--ignore-certificate-errors --deny-permission-prompts \
--ip-address-space-overrides=172.28.0.2:8443=public \
--enable-features=LocalNetworkAccessChecks,LocalNetworkAccessChecksWebSockets \
--enable-logging=stderr --log-net-log=/tmp/netlog_v$N.json \
https://172.28.0.2:8443/vrp.html > /tmp/chromeV$N.log 2>&1 &
done
Type of crash: ASAN UAF Write
Reporter credit: HoneyBee