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 <[email protected]> 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 <[email protected]> Reviewed-by: Nidhi Jaju <[email protected]> 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