Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebView
DescriptionUse after free in WebView
ComponentWebView
Bug ClassUAF
Tracker499238195
Fix commit3cfba8ec65b7 (chromium/src) +25/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • android_webview/java/src/org/chromium/android_webview/AwPacProcessor.java
  • android_webview/javatests/src/org/chromium/android_webview/test/AwPacProcessorTest.java
From 3cfba8ec65b79c2c4446585059c6cda0dfd8ac5f Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Wed, 08 Apr 2026 12:42:58 -0700
Subject: [PATCH] android_webview: Fix race condition and potential UAF in AwPacProcessor

A race condition in AwPacProcessor allowed asynchronous Android network
callbacks to execute after the underlying native object was destroyed,
potentially leading to a Use-After-Free (UAF).

This CL addresses the issue by:

1. Removing the 'final' modifier from 'mNativePacProcessor' to allow it
   to be cleared.
2. Setting 'mNativePacProcessor' to 0 in 'destroy()' before
   unregistering callbacks and destroying the native object.
3. Adding checks to ensure JNI calls are only made if
   'mNativePacProcessor' is non-zero.
4. Adding a test case to verify that 'destroy()' is safe and idempotent.

Fixed: 499238195
Change-Id: I963a9671b6deb78dac29c2310e9b15b04eac8719
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7736909
Reviewed-by: Richard (Torne) Coles <torne@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1611728}
---

diff --git a/android_webview/java/src/org/chromium/android_webview/AwPacProcessor.java b/android_webview/java/src/org/chromium/android_webview/AwPacProcessor.java
index 09d84bb..999c05b90 100644
--- a/android_webview/java/src/org/chromium/android_webview/AwPacProcessor.java
+++ b/android_webview/java/src/org/chromium/android_webview/AwPacProcessor.java
@@ -29,7 +29,8 @@
 // TODO(amalova): remove UsedByReflection
 @UsedByReflection("Android")
 public class AwPacProcessor {
-    private final long mNativePacProcessor;
+    // 0 if it's already been destroyed.
+    private long mNativePacProcessor;
     private Network mNetwork;
     private ConnectivityManager.NetworkCallback mNetworkCallback;
 
@@ -65,6 +66,7 @@
     }
 
     public void setNetworkAndLinkAddresses(long networkHandle, List<String> addresses) {
+        if (mNativePacProcessor == 0) return;
         AwPacProcessorJni.get()
                 .setNetworkAndLinkAddresses(mNativePacProcessor, networkHandle, addresses);
     }
@@ -97,17 +99,22 @@
     // The calling code must not call any methods after it called destroy().
     @UsedByReflection("Android")
     public void destroy() {
+        if (mNativePacProcessor == 0) return;
+        long nativePacProcessor = mNativePacProcessor;
+        mNativePacProcessor = 0;
         unregisterNetworkCallback();
-        AwPacProcessorJni.get().destroyNative(mNativePacProcessor);
+        AwPacProcessorJni.get().destroyNative(nativePacProcessor);
     }
 
     @UsedByReflection("Android")
     public boolean setProxyScript(String script) {
+        if (mNativePacProcessor == 0) return false;
         return AwPacProcessorJni.get().setProxyScript(mNativePacProcessor, script);
     }
 
     @UsedByReflection("Android")
     public String makeProxyRequest(String url) {
+        if (mNativePacProcessor == 0) return null;
         return AwPacProcessorJni.get().makeProxyRequest(mNativePacProcessor, url);
     }
 
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwPacProcessorTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwPacProcessorTest.java
index 1e2ccf9..758b4b6 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwPacProcessorTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwPacProcessorTest.java
@@ -68,4 +68,20 @@
         mProcessor.setNetwork(null);
         Assert.assertEquals(proxyResultNetworkIsNotSet, mProcessor.makeProxyRequest(M_TEST_URL));
     }
+
+    @Test
+    @SmallTest
+    public void testDestroyIsIdempotentAndSafe() throws Throwable {
+        Assert.assertTrue(mProcessor.setProxyScript(PAC_SCRIPT));
+
+        mProcessor.destroy();
+
+        // Subsequent calls should fail gracefully and not crash.
+        Assert.assertFalse(mProcessor.setProxyScript(PAC_SCRIPT));
+        Assert.assertNull(mProcessor.makeProxyRequest(M_TEST_URL));
+        mProcessor.setNetworkAndLinkAddresses(42, List.of("1.2.3.4"));
+
+        // Calling destroy again should be safe.
+        mProcessor.destroy();
+    }
 }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwPacProcessorTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwPacProcessorTest.java
index 1e2ccf9..758b4b6 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwPacProcessorTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwPacProcessorTest.java
@@ -68,4 +68,20 @@
         mProcessor.setNetwork(null);
         Assert.assertEquals(proxyResultNetworkIsNotSet, mProcessor.makeProxyRequest(M_TEST_URL));
     }
+
+    @Test
+    @SmallTest
+    public void testDestroyIsIdempotentAndSafe() throws Throwable {
+        Assert.assertTrue(mProcessor.setProxyScript(PAC_SCRIPT));
+
+        mProcessor.destroy();
+
+        // Subsequent calls should fail gracefully and not crash.
+        Assert.assertFalse(mProcessor.setProxyScript(PAC_SCRIPT));
+        Assert.assertNull(mProcessor.makeProxyRequest(M_TEST_URL));
+        mProcessor.setNetworkAndLinkAddresses(42, List.of("1.2.3.4"));
+
+        // Calling destroy again should be safe.
+        mProcessor.destroy();
+    }
 }
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in AwPacProcessor via NetworkCallback Race Condition

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.

Overview: A race condition in AwPacProcessor allows an asynchronous Android network callback to execute after the underlying native object is destroyed. This causes a dangling pointer to be passed through JNI, leading to a potential Use-After-Free (UAF). A local malicious app could exploit this to achieve arbitrary code execution in the highly privileged Android PAC processor service (UID 1000).

Affected files:

  • android_webview/java/src/org/chromium/android_webview/AwPacProcessor.java
  • android_webview/browser/aw_pac_processor.cc
  • android_webview/browser/aw_pac_processor.h

Estimated timestamp from git blame: 2025-06-30

Description

A race condition exists in AwPacProcessor.java between the destroy() method and asynchronous ConnectivityManager callbacks. This leads to a potential Use-After-Free (UAF) vulnerability where native C++ methods are called on a deleted object.

When AwPacProcessor.destroy() is called, it unregisters the network callback and immediately deletes the native C++ object (destroyNative). However, Android’s unregisterNetworkCallback does not purge pending onLinkPropertiesChanged callbacks already queued on the ConnectivityThread’s MessageQueue.

Because AwPacProcessor lacks a destruction flag (and mNativePacProcessor is a final long that cannot be cleared), any delayed callback will execute successfully and pass the dangling pointer via JNI to AwPacProcessorJni.get().setNetworkAndLinkAddresses().

Potential Exploitation Steps

Note: These are potential steps; our tooling agent does not have the ability to run code to verify a live proof of concept.

  1. Triggering the Race: A local attacker app granted VpnService permission establishes a VPN and rapidly adds/removes IP addresses, flooding the ConnectivityThread with callbacks.
  2. Freeing the Object: The system gracefully destroys the AwPacProcessor. The 40-byte C++ object is freed into a 48-byte PartitionAlloc bucket.
  3. Late Execution: A delayed callback fires, extracting the attacker-controlled VPN IP addresses and passing them through JNI.
  4. Heap Reclamation: The generated JNI stub converts the Java List<String> to a C++ std::vector<std::string>. If the attacker provides exactly two IP addresses, this allocates exactly 48 bytes, reliably reclaiming the recently freed AwPacProcessor object.
  5. Pointer Forgery: The first IP address is copied into the first std::string. Under Chromium’s libc++ (ABI v2), Small String Optimization (SSO) places the character data at offset 0. Characters 8-15 of the attacker’s string perfectly overlap the host_resolver_ pointer field of the reclaimed object.
  6. UAF Execution: The JNI stub casts the dangling long to an AwPacProcessor* and calls SetNetworkAndLinkAddresses. It reads the attacker-forged host_resolver_ pointer and posts a task using base::Unretained(host_resolver_.get()).
  7. Memory Corruption: When the background task executes HostResolver::SetNetworkAndLinkAddresses, it performs a std::vector assignment (link_addresses_ = link_addresses;) on the forged target. By controlling the target vector’s pointers, the attacker achieves Arbitrary Free and Constrained Arbitrary Write primitives, ultimately leading to code execution in the com.android.pacprocessor process.

Suggested Fix

  1. Java Layer (Primary Fix): Add a private boolean mIsDestroyed = false; flag to AwPacProcessor.java. Set this flag to true inside destroy(). Check if (mIsDestroyed) return; at the beginning of onLinkPropertiesChanged() or before any native JNI calls are made to prevent using the dangling mNativePacProcessor handle.
  2. C++ Layer (Defense in Depth): Avoid using base::Unretained in AwPacProcessor::SetNetworkAndLinkAddresses when posting tasks to the HostResolver. Consider using a base::WeakPtr for the AwPacProcessor or managing the HostResolver lifetime such that pending tasks are safely canceled or invalidated upon destruction.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker