← Chrome — 2026-W40

457776ec7c7642b111a3936e46ebe2684a7f997e  [M150-LTS][Fortify] [CPRS] Fix UAF in ConfiguredProxyResolutionRequest::QueryComplete

severity high class UAF confidence 0.96 signal score 14.87 net/ patch-now AI review CONFIRMED
Primitive: re-entrant QueryComplete frees the request object while outer frames execute (UAF)
Triage note: M150-LTS 'Fix UAF'; QueryComplete now RemovePendingRequest(this)+CHECK(!was_completed()) before QueryDidComplete so a synchronous SetReady()/re-dispatch on ERR_PAC_SCRIPT_TERMINATED can't re-enter and destroy the still-listed request. Attacker-triggerable via malicious PAC.
Structural signals: smartptr_member_added (+6.47), security_assert (+3.0), null_guard (+1.2), msg_strong (+3.0), area:net/ (+1.2)

The bug at a glance

Re-entrant use-after-free in net/proxy_resolution’s ConfiguredProxyResolutionRequest, which runs in the browser process; it destroys a live request object while outer frames still use it. Backported as an M150-LTS Fortify fix, consistent with a security-relevant browser-process UAF (High).

A PAC/proxy configuration an attacker or network can influence (e.g. one that causes ERR_PAC_SCRIPT_TERMINATED and a synchronous DIRECT fallback) makes proxy resolution re-enter itself and free a request mid-completion, so a hostile or crash-prone PAC script becomes a memory-corruption primitive in the browser process.

Root cause

ConfiguredProxyResolutionRequest::QueryComplete originally called QueryDidComplete (-> DidFinishResolvingProxy on the service) while the request was still in the service’s pending_requests_, and only removed itself afterward. When PAC execution terminated, DidFinishResolvingProxy reset the proxy config and re-initialized the resolver; if that re-init failed or completed synchronously (falling back to DIRECT), SetReady() synchronously re-dispatched the still-registered request, re-entering QueryComplete. The inner QueryComplete ran the consumer callback (which, like HttpStreamFactory::JobController, destroys the request), so when the outer QueryComplete frame resumed it operated on a freed this — a UAF. The fix removes this from pending_requests_ (service_->RemovePendingRequest(this)) and adds CHECK(!was_completed()) before any notification, so the request is no longer reachable for synchronous re-dispatch; it then nulls service_ and runs the user callback last.

Key code

configured_proxy_resolution_request.cc — deregister before notifying, and assert single completion

void ConfiguredProxyResolutionRequest::QueryComplete(int result_code) {
  CHECK(!was_completed());
  // Remove `this` before notifying 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_);
  user_callback_.Reset();
  std::move(callback).Run(result_code);
}

Patch walkthrough

  • net/proxy_resolution/configured_proxy_resolution_request.cc — QueryComplete is reordered: it now opens with CHECK(!was_completed()), then calls service_->RemovePendingRequest(this) BEFORE QueryDidComplete, and sets service_ = nullptr right after. Because DidFinishResolvingProxy (reached via QueryDidComplete) can synchronously re-dispatch any request still in pending_requests_, removing this first makes re-entrant dispatch of this request impossible; the user callback is still run last.
  • net/proxy_resolution/configured_proxy_resolution_service.cc — The re-initialization guard after ResetProxyConfig changes from ‘pending_requests_.size() > 1’ to ‘!pending_requests_.empty()’. Since the completing request is now removed from pending_requests_ before DidFinishResolvingProxy runs, the old count is off by one and would skip re-applying the config when exactly one other request remains.
  • net/proxy_resolution/configured_proxy_resolution_service_unittest.cc — Adds ProxyResolverTerminatedDuringRequestWithSyncReinit, a regression test with a SyncFailProxyResolverFactory that fails re-initialization synchronously; both callbacks destroy their request inside the completion callback and expect DIRECT. Without the fix, destroying request1 inside its callback during the re-entrant dispatch is a UAF.

Background

ConfiguredProxyResolutionRequest — Per-resolve object tracked in the service’s pending_requests_; QueryComplete finalizes it, runs the consumer’s completion callback, and the consumer typically deletes the request from inside that callback.

DidFinishResolvingProxy / SetReady — Service-side path reached via QueryDidComplete; on a reset config it re-initializes the resolver and, when ready, synchronously re-dispatches pending requests.

ERR_PAC_SCRIPT_TERMINATED — Error from a PAC runtime crash/termination; it triggers the config reset and resolver re-initialization that opens the re-entrancy window, with DIRECT as the per-request fallback.

Re-entrant completion — Synchronous re-init lets the inner QueryComplete run and free the request while the outer QueryComplete is still on the stack — a self-referential UAF.

Vulnerability window

  1. Original fix — Landed on main as commit 78fb4f541c61 (CL 8287885).
  2. LTS backport — Cherry-picked as 457776ec7c (dated 2026-10-01) to the M150-LTS branch under the [Fortify]/[CPRS] label, CL 8396969.
  3. Tracking — The included regression test ties directly to the tracked bug (crbug.com/533534913).

Proof of concept

Grounded in the regression test ProxyResolverTerminatedDuringRequestWithSyncReinit. Two concurrent resolves are dispatched; the factory is set to fail re-initialization synchronously; completing url1 with ERR_PAC_SCRIPT_TERMINATED resets the config and synchronously re-dispatches, re-entering QueryComplete, while each callback destroys its own request. On the unpatched code the outer QueryComplete then touches freed memory. Mapping this to a remote/web trigger (serving a PAC that terminates mid-resolve) is (inferred).

// Condensed from the diff's regression test
service.ResolveProxy(url1, ..., callback1, &request1, ...);  // ERR_IO_PENDING
service.ResolveProxy(url2, ..., callback2, &request2, ...);  // ERR_IO_PENDING
factory->pending_requests()[0]->CompleteNowWithForwarder(OK, &resolver);
factory->set_synchronous_fail(true);
// callback1/callback2 destroy their request: request1.reset(); request2.reset();
jobs[url1]->CompleteNow(ERR_PAC_SCRIPT_TERMINATED);  // re-entrant path

Exploitation

  1. Setup — Arrange a proxy configuration with a PAC script and at least two concurrent proxy resolutions in flight.
  2. Trigger — Cause ERR_PAC_SCRIPT_TERMINATED on one in-flight resolve so DidFinishResolvingProxy resets the config and re-initializes the resolver synchronously (DIRECT fallback), re-entering QueryComplete on a request still in pending_requests_.
  3. Free — The consumer callback destroys the request inside the re-entrant completion; control returns to the outer QueryComplete frame which uses the freed request object.
  4. Escalate — (inferred) Steering a remote PAC to terminate on demand, reclaiming the freed request allocation, and any corruption beyond the crash are not shown by the patch and are (inferred).

Detection & hunting

For defenders and detection engineers:

  • CHECK(!was_completed()) aborts — Post-patch, a crash on the new CHECK(!was_completed()) in QueryComplete is a direct marker of an attempted double/re-entrant completion; monitor browser-process crash reports for it.
  • PAC termination clusters — Hunt for bursts of ERR_PAC_SCRIPT_TERMINATED / ERR_PAC_SCRIPT_FAILED in NetLog or enterprise proxy telemetry, especially when followed by DIRECT fallback and proxy-resolution crashes.
  • UAF in proxy_resolution frames — On ASAN builds, flag heap-use-after-free with ConfiguredProxyResolutionRequest::QueryComplete / ConfiguredProxyResolutionService::DidFinishResolvingProxy on the stack.

Audit directions

  • Deregister-before-notify ordering — Audit all net/ request objects that live in a service-owned pending list and run a user callback on completion: ensure they remove themselves from the list before any notification that can synchronously re-dispatch.
  • Synchronous resolver re-init paths — Review ResetProxyConfig / ApplyProxyConfigIfAvailable / SetReady and any resolver-factory that can complete or fail synchronously (DIRECT fallback) for re-entrancy into in-flight requests.
  • Off-by-one container guards — Check other ‘pending_requests_.size() > N’ style guards that assume the completing request is still present; reordering removal invalidates such counts.

Before / after

Loading diff…