Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebSockets
DescriptionUse after free in WebSockets
ComponentWebSockets
Sub-componentBlink>Network>WebSockets, Blink>SecurityFeature>LocalNetworkAccess
Bug ClassUAF
Tracker560939312
Fix commit1985cb7b30ce (chromium/src) +29/-6
CISA KEVNot listed
CreditedHoneyBee
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
net/url_request/url_request_http_job.cc
modified
TEST_F
net/url_request/url_request_http_job_unittest.cc
modified

Files Changed

  • net/url_request/url_request_http_job.cc
  • net/url_request/url_request_http_job.h
  • net/http/http_transaction_test_util.h
  • net/url_request/url_request_http_job_unittest.cc
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(
Loading diff…

Regression Test / PoC

shipped with the fix
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(
Loading diff…

Original Bug Report

reported by rd...@gmail.com

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).

  1. The connect continuation is stashed with an Unretained transaction. When the handshake’s transport completes:
  2. 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.
  3. 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
}
  1. 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

View on issue tracker