a87859ad7a790f27f99930894f0180c76a384ff3 [M152] [Fortify] [CPRS] Fix UAF in ConfiguredProxyResolutionRequest::QueryComplete
The bug at a glance
The commit message and new unit test describe a real re-entrancy use-after-free in the net stack’s proxy resolution service (network service process): a ConfiguredProxyResolutionRequest is re-dispatched and destroyed by its consumer callback while outer QueryComplete frames still use it. It is rated high rather than critical because triggering it needs a PAC-configured proxy, a PAC runtime termination (ERR_PAC_SCRIPT_TERMINATED) and a re-initialization that completes synchronously, so it is not reachable from arbitrary web content on a default DIRECT setup.
Whoever controls or can disrupt the PAC script (a WPAD/PAC server on the local network, or a PAC URL set by enterprise/system policy) can drive the resolver into the terminated, then synchronous-fallback path while several proxy resolutions are in flight, turning ordinary concurrent page loads into a heap UAF in the privileged network stack (inferred). Defenders care because proxy resolution runs outside the renderer sandbox and the bug is a classic callback re-entrancy pattern.
Root cause
QueryComplete() called QueryDidComplete(), which calls ConfiguredProxyResolutionService::DidFinishResolvingProxy, before removing this from pending_requests_. On ERR_PAC_SCRIPT_TERMINATED the service calls ResetProxyConfig() and then ApplyProxyConfigIfAvailable(). If re-initialization fails or completes synchronously (for example falling back to DIRECT), SetReady() re-dispatches every request still in pending_requests_, including the one currently completing. That re-entrant QueryComplete runs the consumer callback, which destroys the request object (as HttpStreamFactory::JobController-style consumers do), and the outer frame then keeps touching the freed request (user_callback_, service_). The fix removes the request from pending_requests_ first, adds CHECK(!was_completed()), and changes the re-init guard from pending_requests_.size() > 1 to !pending_requests_.empty(), because the completing request is no longer counted.
Key code
The request now leaves pending_requests_ before the service is told it completed, so SetReady() cannot re-dispatch it re-entrantly
void ConfiguredProxyResolutionRequest::QueryComplete(int result_code) {
+ CHECK(!was_completed());
+ service_->RemovePendingRequest(this);
+
result_code = QueryDidComplete(result_code);
+ service_ = nullptr;
CompletionOnceCallback callback = std::move(user_callback_);
-
- service_->RemovePendingRequest(this);
- service_ = nullptr;
...
- if (pending_requests_.size() > 1)
+ if (!pending_requests_.empty()) {
ApplyProxyConfigIfAvailable();
Patch walkthrough
net/proxy_resolution/configured_proxy_resolution_request.cc— QueryComplete now calls service_->RemovePendingRequest(this) before QueryDidComplete, so a synchronous reset or re-init inside DidFinishResolvingProxy cannot find and re-dispatch this request. The new CHECK(!was_completed()) turns any remaining double completion into a safe crash instead of a UAF.net/proxy_resolution/configured_proxy_resolution_service.cc— DidFinishResolvingProxy’s reset_config branch usedsize() > 1to mean ‘other requests are waiting besides the current one’. The current request is now removed earlier, so the test becomes!empty(), which keeps the existing behaviour of re-applying config for the other waiters. The comment about destruction-time ordering is updated to match.net/proxy_resolution/configured_proxy_resolution_service_unittest.cc— Adds ProxyResolverTerminatedDuringRequestWithSyncReinit. It starts two in-flight PAC resolutions, makes the next CreateProxyResolver fail synchronously, completes request 1 with ERR_PAC_SCRIPT_TERMINATED, and destroys each request inside its callback. This reproduces the reported crash, and after the fix both requests fall back to DIRECT.
Background
ConfiguredProxyResolutionService — The net-stack service that resolves the proxy for each URL from the configured proxy settings, including PAC scripts, and keeps outstanding requests in pending_requests_.
PAC / ERR_PAC_SCRIPT_TERMINATED — A Proxy Auto-Config script is JavaScript that the proxy resolver runs. A runtime termination of that execution makes the service reset its config and re-initialize the resolver.
SetReady() re-dispatch — When the service becomes ready after (re)initialization, it restarts every request still listed in pending_requests_. That restart is the re-entrancy vector here.
Destroy-in-callback consumers — Callers such as HttpStreamFactory::JobController can free the ProxyResolutionRequest inside its completion callback, which is what makes re-entrant completion fatal.
Vulnerability window
- Reachability — Only reachable when the browser uses a PAC-based proxy configuration (system, enterprise policy, or WPAD auto-detect). Stock builds on DIRECT or fixed-proxy setups never take this path. Web content only supplies the concurrent navigations or fetches that cause proxy resolutions (inferred).
- Trigger — With two or more resolutions in flight, one fails with ERR_PAC_SCRIPT_TERMINATED and the resolver re-initialization completes synchronously (for example a synchronous ERR_PAC_SCRIPT_FAILED that falls back to DIRECT), as the unit test models.
- Primitive — SetReady() re-dispatches the still-pending completing request, which runs QueryComplete re-entrantly. The consumer callback frees the request, and the outer QueryComplete frame then reads and writes members of the freed ConfiguredProxyResolutionRequest.
- Escalation — A heap UAF in the network service could in principle be groomed toward control of freed object fields such as callbacks (inferred). The diff shows only the lifetime bug, not a weaponization path.
Proof of concept
Conceptually: configure a PAC proxy, start at least two proxy resolutions at once, make the PAC execution for one of them terminate (ERR_PAC_SCRIPT_TERMINATED), and arrange for the next resolver creation to fail synchronously so the service falls back to DIRECT and re-dispatches pending requests while the first request is still completing. The consumer must destroy the request in its completion callback. The commit’s unit test exercises exactly this sequence with mocks.
Exploitation
- Environment control — The attacker needs influence over PAC configuration or execution, for example a malicious or unstable WPAD/PAC endpoint on the local network (inferred).
- Race shaping — Keep several proxy resolutions pending so that
pending_requests_is non-empty when the termination path resets config (inferred). - Free and reuse — The freed ConfiguredProxyResolutionRequest would have to be reclaimed before the outer frame touches
user_callback_andservice_. Partition/heap hardening and the new CHECK make this hard (inferred).
Detection & hunting
For defenders and detection engineers:
- grep -n ‘RemovePendingRequest’ net/proxy_resolution/configured_proxy_resolution_request.cc — In a vulnerable build, RemovePendingRequest(this) appears after QueryDidComplete(result_code) in QueryComplete. In a fixed build it comes first, preceded by CHECK(!was_completed()).
- grep ‘pending_requests_.size() > 1’ net/proxy_resolution/configured_proxy_resolution_service.cc — This old guard in DidFinishResolvingProxy marks the pre-fix ordering.
- Crash stacks in network service with ConfiguredProxyResolutionRequest::QueryComplete re-entered under ConfiguredProxyResolutionService::SetReady / DidFinishResolvingProxy — Triage crash reports from PAC-configured fleets for this nested frame pattern, or for the new CHECK(!was_completed()) firing.
- NetLog PROXY_RESOLUTION_SERVICE events with ERR_PAC_SCRIPT_TERMINATED followed by immediate DIRECT fallback — Enterprise defenders can flag hosts where PAC execution repeatedly terminates, which may indicate a hostile or broken PAC/WPAD source.
Audit directions
- net/proxy_resolution/ other completion paths (e.g. DidFinishResolvingProxy, OnInitProxyResolverComplete, Cancel)* — Look for other places that notify the service or run callbacks while the object is still registered in a container the service may iterate and re-dispatch synchronously.
- Services that iterate pending_requests_ and restart them on SetReady-like transitions — Check whether a synchronous state transition can restart an item that is in the middle of completing. Snapshotting or removing the item before notifying is the safe pattern.
- Consumers that destroy requests inside completion callbacks (HttpStreamFactory JobController, WebSocket, PAC-dependent fetchers) — Make sure the callee’s frames never touch
thisafter running a callback that may delete it.