High chrome UAF 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Proxy
DescriptionUse after free in Proxy
ComponentProxy
Bug ClassUAF
Tracker533534913
Fix commit78fb4f541c61 (chromium/src) +146/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-01

Background

`ConfiguredProxyResolutionRequest`
A per-lookup object in Chromium’s network stack that tracks one in-flight proxy resolution and holds the consumer’s completion callback.
`pending_requests_`
The set on ConfiguredProxyResolutionService holding every request currently awaiting a proxy decision, iterated when the service re-dispatches work.
PAC (Proxy Auto-Config)
A JavaScript program the browser executes to decide which proxy to use for a URL, whose runtime can be terminated mid-execution (ERR_PAC_SCRIPT_TERMINATED).
Re-entrancy
The condition where a function is invoked again on the same object before an outer invocation on that object has returned.

Root Cause Analysis

The vulnerable path is ConfiguredProxyResolutionRequest::QueryComplete, which invoked QueryDidComplete (and through it DidFinishResolvingProxy) while this was still a member of the service’s pending_requests_ set, unlinking itself only afterward. The invariant violated is that a request must be removed from pending_requests_ before the service is notified of its completion, so that the service never re-dispatches a request that is already mid-completion. When PAC execution terminated (ERR_PAC_SCRIPT_TERMINATED), DidFinishResolvingProxy reset the proxy config and re-initialized the resolver; if that re-initialization failed or completed synchronously (for example falling back to DIRECT), SetReady synchronously re-dispatched the still-listed request, re-entering QueryComplete. The re-entrant call ran the consumer callback, which could destroy the request object while the outer QueryComplete frame was still executing on that freed memory, a classic use-after-free.

The fix calls service_->RemovePendingRequest(this) before QueryDidComplete, so the re-initialization can no longer see or re-dispatch this request, and it adds a CHECK(!was_completed()) to trap any residual double-completion. The companion change in DidFinishResolvingProxy switches the guard from pending_requests_.size() > 1 to !pending_requests_.empty(), matching the new ordering where the completing request is no longer counted.

Key insight
The single mistake was notifying the service of completion before unlinking the request, leaving a live entry in pending_requests_ that a synchronous re-initialization could re-dispatch re-entrantly; the fix removes the request from pending_requests_ first, eliminating the window in which it can be re-entered and freed.

Attack Path

  1. Start concurrent resolutions An attacker’s page or a controlled proxy configuration causes two or more proxy resolutions to be in flight simultaneously through the same ConfiguredProxyResolutionService.
  2. Terminate PAC execution One request’s PAC script execution is terminated (ERR_PAC_SCRIPT_TERMINATED), driving that request into QueryComplete while it remains in pending_requests_.
  3. Force synchronous re-init DidFinishResolvingProxy resets the config and re-initializes the resolver in a way that completes synchronously (re-init fails and falls back to DIRECT), causing SetReady to re-dispatch the still-listed request.
  4. Re-enter and free The re-entrant QueryComplete runs the consumer callback, which (as real consumers like HttpStreamFactory::JobController do) destroys the request object while the outer frame still holds it.
  5. Use freed object The outer QueryComplete frame continues operating on the freed ConfiguredProxyResolutionRequest, yielding the use-after-free.

Impact Assessment

An attacker gains a use-after-free on a heap-allocated ConfiguredProxyResolutionRequest in the process that runs the network stack (the browser or network service process), which can lead to memory corruption and potentially controlled execution. Preconditions are the ability to influence proxy configuration/PAC behavior so that a PAC termination coincides with a synchronous proxy re-initialization while multiple requests are pending, and a consumer that destroys its request inside the completion callback. Severity is rated high, consistent with a UAF reachable through proxy resolution.

Changed Functions

FunctionChangeNotes
if
net/proxy_resolution/configured_proxy_resolution_service.cc
modified
SyncFailProxyResolverFactory
net/proxy_resolution/configured_proxy_resolution_service_unittest.cc
modified
async_factory_
net/proxy_resolution/configured_proxy_resolution_service_unittest.cc
modified
if
net/proxy_resolution/configured_proxy_resolution_service_unittest.cc
modified

Files Changed

  • net/proxy_resolution/configured_proxy_resolution_request.cc
  • net/proxy_resolution/configured_proxy_resolution_service.cc
  • net/proxy_resolution/configured_proxy_resolution_service_unittest.cc

Audit Directions

  • Notify-before-unlink ordering
    Flag any completion path that runs consumer callbacks or re-dispatch logic while the completing object is still registered in a container the callback can iterate or re-enter.
  • Synchronous re-init after reset
    Review resolver/config reset paths (ResetProxyConfig, ApplyProxyConfigIfAvailable, SetReady) for synchronous completions that re-dispatch pending work re-entrantly rather than posting it.
  • Callback-triggered self-destruction
    Audit consumers that reset() or delete their request object inside the completion callback, and verify no outer frame touches the object after the callback returns.
From 78fb4f541c61dccfc75aab1ec6dc985bc68a8475 Mon Sep 17 00:00:00 2001
From: Zonghan Xu <xzonghan@google.com>
Date: Tue, 25 Aug 2026 16:50:33 -0700
Subject: [PATCH] [Fortify] [CPRS] Fix UAF in ConfiguredProxyResolutionRequest::QueryComplete

When ConfiguredProxyResolutionRequest::QueryComplete completes a
request, it invoked QueryDidComplete (which calls
DidFinishResolvingProxy) before removing `this` from
`pending_requests_`.

If PAC execution terminated (e.g. ERR_PAC_SCRIPT_TERMINATED),
DidFinishResolvingProxy resets the proxy configuration and attempts
to re-initialize the proxy resolver. If re-initialization fails or
completes synchronously (such as falling back to DIRECT), SetReady()
synchronously re-dispatched the request that was still present in
`pending_requests_`.

This led to a re-entrant execution of QueryComplete, invoking the
consumer callback and destroying the request object while outer frames
were still executing, causing a use-after-free (UAF).

This CL fixes the issue by removing `this` from `pending_requests_`
before calling QueryDidComplete, and updates the `pending_requests_`
emptiness check in DidFinishResolvingProxy.

Bug: 533534913
Change-Id: I8fc6b88c8d7fc4bf69098e82e829b5e235215adc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8287885
Reviewed-by: Sebastien Lalancette <seblalancette@chromium.org>
Commit-Queue: Zonghan Xu <xzonghan@chromium.org>
Reviewed-by: mmenke <mmenke@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1686034}
---

diff --git a/net/proxy_resolution/configured_proxy_resolution_request.cc b/net/proxy_resolution/configured_proxy_resolution_request.cc
index 388275c..e42676f 100644
--- a/net/proxy_resolution/configured_proxy_resolution_request.cc
+++ b/net/proxy_resolution/configured_proxy_resolution_request.cc
@@ -216,12 +216,16 @@
 
 // Callback for when the ProxyResolver request has completed.
 void ConfiguredProxyResolutionRequest::QueryComplete(int result_code) {
+  CHECK(!was_completed());
+
+  // Remove `this` from the service before notifying it of completion to
+  // prevent re-entrant dispatch if the service re-initializes synchronously.
+  service_->RemovePendingRequest(this);
+
   result_code = QueryDidComplete(result_code);
+  service_ = nullptr;
 
   CompletionOnceCallback callback = std::move(user_callback_);
-
-  service_->RemovePendingRequest(this);
-  service_ = nullptr;
   user_callback_.Reset();
   std::move(callback).Run(result_code);
 }
diff --git a/net/proxy_resolution/configured_proxy_resolution_service.cc b/net/proxy_resolution/configured_proxy_resolution_service.cc
index 22ce137..11520ac 100644
--- a/net/proxy_resolution/configured_proxy_resolution_service.cc
+++ b/net/proxy_resolution/configured_proxy_resolution_service.cc
@@ -1340,9 +1340,9 @@
     // This can be observed because the service destructor sets
     // `in_destruction_ = true` and synchronously calls
     // QueryComplete(ERR_ABORTED) on each pending request. QueryComplete
-    // synchronously calls QueryDidComplete, which in turn calls
-    // DidFinishResolvingProxy on this service before the request removes itself
-    // from `pending_requests_` and runs the user callback.
+    // removes the request from `pending_requests_` and synchronously calls
+    // QueryDidComplete, which in turn calls DidFinishResolvingProxy on this
+    // service, and then runs the user callback.
     if (resolver_error == ERR_ABORTED && in_destruction_ && !config_) {
       result_code = ERR_MANDATORY_PROXY_CONFIGURATION_FAILED;
       net_log.EndEvent(NetLogEventType::PROXY_RESOLUTION_SERVICE);
@@ -1368,8 +1368,9 @@
 
     if (reset_config) {
       ResetProxyConfig(false);
-      if (pending_requests_.size() > 1)
+      if (!pending_requests_.empty()) {
         ApplyProxyConfigIfAvailable();
+      }
     } else if (enable_pac_runtime_backoff_ &&
                resolver_error == ERR_PAC_SCRIPT_FAILED) {
       HandlePacScriptLoadError();
diff --git a/net/proxy_resolution/configured_proxy_resolution_service_unittest.cc b/net/proxy_resolution/configured_proxy_resolution_service_unittest.cc
index b43ff3c..078ef0f 100644
--- a/net/proxy_resolution/configured_proxy_resolution_service_unittest.cc
+++ b/net/proxy_resolution/configured_proxy_resolution_service_unittest.cc
@@ -1881,6 +1881,140 @@
   EXPECT_EQ("[foopy_valid:8080]", info.proxy_chain().ToDebugString());
 }
 
+// Test what happens when the ProxyResolver fails with
+// ERR_PAC_SCRIPT_TERMINATED while multiple requests are in progress, and the
+// subsequent proxy re-initialization completes synchronously (e.g. fails and
+// falls back to DIRECT). Make sure no use-after-free occurs.
+TEST_F(ConfiguredProxyResolutionServiceTest,
+       ProxyResolverTerminatedDuringRequestWithSyncReinit) {
+  class SyncFailProxyResolverFactory : public ProxyResolverFactory {
+   public:
+    explicit SyncFailProxyResolverFactory(bool resolvers_expect_pac_bytes)
+        : ProxyResolverFactory(resolvers_expect_pac_bytes),
+          async_factory_(resolvers_expect_pac_bytes) {}
+
+    int CreateProxyResolver(const scoped_refptr<PacFileData>& pac_script,
+                            std::unique_ptr<ProxyResolver>* resolver,
+                            CompletionOnceCallback callback,
+                            std::unique_ptr<ProxyResolverFactory::Request>*
+                                request_handle) override {
+      if (synchronous_fail_) {
+        return ERR_PAC_SCRIPT_FAILED;
+      }
+      return async_factory_.CreateProxyResolver(
+          pac_script, resolver, std::move(callback), request_handle);
+    }
+
+    void set_synchronous_fail(bool synchronous_fail) {
+      synchronous_fail_ = synchronous_fail;
+    }
+
+    const MockAsyncProxyResolverFactory::RequestsList& pending_requests()
+        const {
+      return async_factory_.pending_requests();
+    }
+
+   private:
+    bool synchronous_fail_ = false;
+    MockAsyncProxyResolverFactory async_factory_;
+  };
+
+  auto config_service =
+      std::make_unique<MockProxyConfigService>("http://foopy/proxy.pac");
+
+  MockAsyncProxyResolver resolver;
+  auto resolver_factory = std::make_unique<SyncFailProxyResolverFactory>(false);
+  auto* resolver_factory_ptr = resolver_factory.get();
+
+  ConfiguredProxyResolutionService service(std::move(config_service),
+                                           std::move(resolver_factory),
+                                           mock_host_resolver_.get(), nullptr,
+                                           /*quick_check_enabled=*/true);
+
+  // Start two resolve requests.
+  GURL url1("http://www.google.com/");
+  GURL url2("https://www.google.com/");
+  ProxyInfo info1, info2;
+  std::unique_ptr<ProxyResolutionRequest> request1, request2;
+
+  int callback1_result = ERR_IO_PENDING;
+  base::RunLoop run_loop1;
+  auto callback1 = base::BindLambdaForTesting([&](int rv) {
+    callback1_result = rv;
+    // Destroy the request inside the completion callback, simulating the
+    // behavior of consumers like HttpStreamFactory::JobController.
+    request1.reset();
+    run_loop1.Quit();
+  });
+
+  int callback2_result = ERR_IO_PENDING;
+  base::RunLoop run_loop2;
+  auto callback2 = base::BindLambdaForTesting([&](int rv) {
+    callback2_result = rv;
+    request2.reset();
+    run_loop2.Quit();
+  });
+
+  // Request 1: In-flight resolve request that will encounter
+  // ERR_PAC_SCRIPT_TERMINATED and trigger proxy re-initialization.
+  int rv1 = service.ResolveProxy(url1, std::string(), NetworkAnonymizationKey(),
+                                 handles::kInvalidNetworkHandle, &info1,
+                                 std::move(callback1), &request1,
+                                 NetLogWithSource(), DEFAULT_PRIORITY);
+
+  // Request 2: Concurrent in-flight resolve request that will be paused in
+  // `pending_requests_` and restarted upon re-initialization.
+  int rv2 = service.ResolveProxy(url2, std::string(), NetworkAnonymizationKey(),
+                                 handles::kInvalidNetworkHandle, &info2,
+                                 std::move(callback2), &request2,
+                                 NetLogWithSource(), DEFAULT_PRIORITY);
+
+  // Both requests should be waiting for PAC compilation now.
+  EXPECT_THAT(rv1, IsError(ERR_IO_PENDING));
+  EXPECT_THAT(rv2, IsError(ERR_IO_PENDING));
+
+  // The service must first create a ProxyResolver by fetching and compiling
+  // the PAC script. We should have one PAC factory creation request for
+  // "http://foopy/proxy.pac".
+  ASSERT_EQ(1u, resolver_factory_ptr->pending_requests().size());
+  EXPECT_EQ(GURL("http://foopy/proxy.pac"),
+            resolver_factory_ptr->pending_requests()[0]->script_data()->url());
+
+  // Complete PAC script compilation now so the service becomes ready and
+  // dispatches `url1` and `url2` to the resolver.
+  resolver_factory_ptr->pending_requests()[0]->CompleteNowWithForwarder(
+      OK, &resolver);
+
+  // Both `url1` and `url2` are now running as in-flight jobs in the resolver.
+  JobMap jobs = GetPendingJobsForURLs(resolver, url1, url2);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/net/proxy_resolution/configured_proxy_resolution_service_unittest.cc b/net/proxy_resolution/configured_proxy_resolution_service_unittest.cc
index b43ff3c..078ef0f 100644
--- a/net/proxy_resolution/configured_proxy_resolution_service_unittest.cc
+++ b/net/proxy_resolution/configured_proxy_resolution_service_unittest.cc
@@ -1881,6 +1881,140 @@
   EXPECT_EQ("[foopy_valid:8080]", info.proxy_chain().ToDebugString());
 }
 
+// Test what happens when the ProxyResolver fails with
+// ERR_PAC_SCRIPT_TERMINATED while multiple requests are in progress, and the
+// subsequent proxy re-initialization completes synchronously (e.g. fails and
+// falls back to DIRECT). Make sure no use-after-free occurs.
+TEST_F(ConfiguredProxyResolutionServiceTest,
+       ProxyResolverTerminatedDuringRequestWithSyncReinit) {
+  class SyncFailProxyResolverFactory : public ProxyResolverFactory {
+   public:
+    explicit SyncFailProxyResolverFactory(bool resolvers_expect_pac_bytes)
+        : ProxyResolverFactory(resolvers_expect_pac_bytes),
+          async_factory_(resolvers_expect_pac_bytes) {}
+
+    int CreateProxyResolver(const scoped_refptr<PacFileData>& pac_script,
+                            std::unique_ptr<ProxyResolver>* resolver,
+                            CompletionOnceCallback callback,
+                            std::unique_ptr<ProxyResolverFactory::Request>*
+                                request_handle) override {
+      if (synchronous_fail_) {
+        return ERR_PAC_SCRIPT_FAILED;
+      }
+      return async_factory_.CreateProxyResolver(
+          pac_script, resolver, std::move(callback), request_handle);
+    }
+
+    void set_synchronous_fail(bool synchronous_fail) {
+      synchronous_fail_ = synchronous_fail;
+    }
+
+    const MockAsyncProxyResolverFactory::RequestsList& pending_requests()
+        const {
+      return async_factory_.pending_requests();
+    }
+
+   private:
+    bool synchronous_fail_ = false;
+    MockAsyncProxyResolverFactory async_factory_;
+  };
+
+  auto config_service =
+      std::make_unique<MockProxyConfigService>("http://foopy/proxy.pac");
+
+  MockAsyncProxyResolver resolver;
+  auto resolver_factory = std::make_unique<SyncFailProxyResolverFactory>(false);
+  auto* resolver_factory_ptr = resolver_factory.get();
+
+  ConfiguredProxyResolutionService service(std::move(config_service),
+                                           std::move(resolver_factory),
+                                           mock_host_resolver_.get(), nullptr,
+                                           /*quick_check_enabled=*/true);
+
+  // Start two resolve requests.
+  GURL url1("http://www.google.com/");
+  GURL url2("https://www.google.com/");
+  ProxyInfo info1, info2;
+  std::unique_ptr<ProxyResolutionRequest> request1, request2;
+
+  int callback1_result = ERR_IO_PENDING;
+  base::RunLoop run_loop1;
+  auto callback1 = base::BindLambdaForTesting([&](int rv) {
+    callback1_result = rv;
+    // Destroy the request inside the completion callback, simulating the
+    // behavior of consumers like HttpStreamFactory::JobController.
+    request1.reset();
+    run_loop1.Quit();
+  });
+
+  int callback2_result = ERR_IO_PENDING;
+  base::RunLoop run_loop2;
+  auto callback2 = base::BindLambdaForTesting([&](int rv) {
+    callback2_result = rv;
+    request2.reset();
+    run_loop2.Quit();
+  });
+
+  // Request 1: In-flight resolve request that will encounter
+  // ERR_PAC_SCRIPT_TERMINATED and trigger proxy re-initialization.
+  int rv1 = service.ResolveProxy(url1, std::string(), NetworkAnonymizationKey(),
+                                 handles::kInvalidNetworkHandle, &info1,
+                                 std::move(callback1), &request1,
+                                 NetLogWithSource(), DEFAULT_PRIORITY);
+
+  // Request 2: Concurrent in-flight resolve request that will be paused in
+  // `pending_requests_` and restarted upon re-initialization.
+  int rv2 = service.ResolveProxy(url2, std::string(), NetworkAnonymizationKey(),
+                                 handles::kInvalidNetworkHandle, &info2,
+                                 std::move(callback2), &request2,
+                                 NetLogWithSource(), DEFAULT_PRIORITY);
+
+  // Both requests should be waiting for PAC compilation now.
+  EXPECT_THAT(rv1, IsError(ERR_IO_PENDING));
+  EXPECT_THAT(rv2, IsError(ERR_IO_PENDING));
+
+  // The service must first create a ProxyResolver by fetching and compiling
+  // the PAC script. We should have one PAC factory creation request for
+  // "http://foopy/proxy.pac".
+  ASSERT_EQ(1u, resolver_factory_ptr->pending_requests().size());
+  EXPECT_EQ(GURL("http://foopy/proxy.pac"),
+            resolver_factory_ptr->pending_requests()[0]->script_data()->url());
+
+  // Complete PAC script compilation now so the service becomes ready and
+  // dispatches `url1` and `url2` to the resolver.
+  resolver_factory_ptr->pending_requests()[0]->CompleteNowWithForwarder(
+      OK, &resolver);
+
+  // Both `url1` and `url2` are now running as in-flight jobs in the resolver.
+  JobMap jobs = GetPendingJobsForURLs(resolver, url1, url2);
+
+  // Configure the next CreateProxyResolver call (when the service attempts to
+  // reset and re-initialize the PAC resolver after `url1` crashes) to fail
+  // synchronously.
+  resolver_factory_ptr->set_synchronous_fail(true);
+
+  // Simulate a PAC script crash on `url1`. DidFinishResolvingProxy() will then
+  // reset the configuration, attempt re-initialization (which fails
+  // synchronously), and fall back to DIRECT.
+  //
+  // Note: this covers the edge case reported in crbug.com/533534913 and
+  // confirms that destroying `request1` inside its completion callback does not
+  // cause a use-after-free.
+  jobs[url1]->CompleteNow(ERR_PAC_SCRIPT_TERMINATED);
+
+  // `request1` fell back to DIRECT per-request as the error fallback for
+  // PAC runtime script crash (ERR_PAC_SCRIPT_TERMINATED).
+  run_loop1.Run();
+  EXPECT_THAT(callback1_result, IsOk());
+  EXPECT_TRUE(info1.is_direct());
+
+  // PAC re-initialization failed, so the entire service entered DIRECT fallback
+  // mode and restarted `request2` against this new DIRECT configuration.
+  run_loop2.Run();
+  EXPECT_THAT(callback2_result, IsOk());
+  EXPECT_TRUE(info2.is_direct());
+}
+
 TEST_F(ConfiguredProxyResolutionServiceTest,
        PacFileFetcherFailsDownloadingMandatoryPac) {
   // Test what happens when the ProxyResolver fails to download a mandatory PAC
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.