CVE-2026-11072
Overview
Files Changed
android_webview/java/src/org/chromium/android_webview/AwPacProcessor.javaandroid_webview/javatests/src/org/chromium/android_webview/test/AwPacProcessorTest.java
Patch
From 3cfba8ec65b79c2c4446585059c6cda0dfd8ac5f Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> 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 <[email protected]> Commit-Queue: Andrew Paseltiner <[email protected]> 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(); + } }
Regression Test / PoC
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();
+ }
}
Original Bug Report
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.javaandroid_webview/browser/aw_pac_processor.ccandroid_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.
- Triggering the Race: A local attacker app granted
VpnServicepermission establishes a VPN and rapidly adds/removes IP addresses, flooding theConnectivityThreadwith callbacks. - Freeing the Object: The system gracefully destroys the
AwPacProcessor. The 40-byte C++ object is freed into a 48-byte PartitionAlloc bucket. - Late Execution: A delayed callback fires, extracting the attacker-controlled VPN IP addresses and passing them through JNI.
- 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 freedAwPacProcessorobject. - 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 thehost_resolver_pointer field of the reclaimed object. - UAF Execution: The JNI stub casts the dangling
longto anAwPacProcessor*and callsSetNetworkAndLinkAddresses. It reads the attacker-forgedhost_resolver_pointer and posts a task usingbase::Unretained(host_resolver_.get()). - Memory Corruption: When the background task executes
HostResolver::SetNetworkAndLinkAddresses, it performs astd::vectorassignment (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 thecom.android.pacprocessorprocess.
Suggested Fix
- Java Layer (Primary Fix): Add a
private boolean mIsDestroyed = false;flag toAwPacProcessor.java. Set this flag totrueinsidedestroy(). Checkif (mIsDestroyed) return;at the beginning ofonLinkPropertiesChanged()or before any native JNI calls are made to prevent using the danglingmNativePacProcessorhandle. - C++ Layer (Defense in Depth): Avoid using
base::UnretainedinAwPacProcessor::SetNetworkAndLinkAddresseswhen posting tasks to theHostResolver. Consider using abase::WeakPtrfor theAwPacProcessoror managing theHostResolverlifetime 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.