
Android complete exploit chain that enables privilege escalation from a local untrusted app to root/kernel, combination of CVE-2026-49881 and CVE-2026-43284
A complete exploit chain that enables privilege escalation from a local untrusted app to root/kernel. It does not involve memory corruptions or race conditions, so attackers don't need to perform complex heap spraying or bypass mitigations against memory corruption vulnerabilities such as KASLR, MTE or CFI, making this exploit chain a 100% success rate on vulnerable devices.
Tested on Pixel 10 running the initial Android 17 official release. Note that it does not work on Pixel 6a and this issue may also occur on other devices running 6.1.xxx-android14 kernel trees due to another bug in these kernels.
Usage: Install KernelSU app, open this app, click "Run userspace exploit" then "Run kernel exploit and load KernelSU". After a successful exploitation, KernelSU will be activated and you can use it to grant root access to other apps. Known issue: If you have already run the kernel exploit and want to run it again, you need to reboot the device.
Screen recording: click here
The chain is made from two distinct vulnerabilities: one is a 0-day in the Telecom service, while the other is a kernel 1-day that was disclosed 3 months ago. But AOSP and Pixel devices (except those running beta QPR versions) remain vulnerable at the time of writing.
The first vulnerability of the chain is a simple logic bug introduced in Android 17. It originates from a crazy change, which adds the following code to InCallController.java:
PackageManager packageManager = mContext.getPackageManager();
Context userContext = mContext.createContextAsUser(userHandle,
0 /* flags */);
PackageManager userPackageManager = userContext != null ?
userContext.getPackageManager() : packageManager;
List<ResolveInfo> entries;
entries = userPackageManager.queryIntentServices(
serviceIntent,
PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_COMPONENTS);
for (ResolveInfo entry : entries) {
ServiceInfo serviceInfo = entry.serviceInfo;
if (serviceInfo != null) {
boolean isMetaFlag = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(
"android.telecom.CLASS_EXISTENCE_CHECK", false);
if (isMetaFlag && !serviceClassExists(serviceInfo, userHandle)) {
continue;
}
}
}
The relevant serviceClassExists() method is defined as follows:
/**
* Verifies that the class for a given ServiceInfo exists within its package.
* This prevents a system crash if a service is declared in the manifest but its
* class was not included in the compiled code.
* @param serviceInfo The ServiceInfo of the service to check.
* @param userHandle The user under which to check for the service.
* @return {@code true} if the class exists, {@code false} otherwise.
*/
private boolean serviceClassExists(ServiceInfo serviceInfo, UserHandle userHandle) {
Log.i(this, "serviceClassExists check");
try {
Context packageContext = mContext.createPackageContextAsUser(
serviceInfo.packageName,
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY, userHandle);
ClassLoader classLoader = packageContext.getClassLoader();
Class.forName(serviceInfo.name, false, classLoader);
return true;
} catch (NameNotFoundException | ClassNotFoundException e) {
Log.w(this, "Skipping InCallService: class not found for " + serviceInfo.name);
return false;
} catch (Exception e) {
Log.e(this, e, "Error checking for existence of " + serviceInfo.name);
return false;
}
}
This is the most unbelievable vulnerability I've ever seen. The code uses Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY to load code from an arbitrary app; While there appear to be some measures intended to prevent arbitrary code execution, such as passing false to Class.forName() to prevent class initialization, the app can still declare a custom AppComponentFactory that is invoked when getClassLoader() is called.
On the other hand, the bug exists in InCallController.java, which is a part of the com.android.server.telecom package rather than com.android.phone. It is worth noting that the package declares android:sharedUserId="android.uid.system" and android:process="system" in AndroidManifest.xml, so it runs in the system_server process, one of the most privileged userspace processes in Android. Therefore, we now have the ability to execute arbitrary Java code inside system_server.
It's a surprise that even a Google engineer can make such a big mistake in the AI era. We found and reported it to the Android Security Team on July 23, 2026. They told us it was a duplicate. Google has switched the monthly security bulletin to quarterly release, which may explain why the vulnerability was not fixed 3 months after the release of Android 17.
The vulnerability was assigned CVE-2026-49881 and fixed in September 2026 by Remove serviceClassExists logic to address security vulnerability.
The first bug allows us to escalate privileges to system, but is still far away from root. A complete root requires at least UID 0 and not being restricted by SELinux.
Now it's time to introduce the kernel 1-day: the DirtyFrag vulnerabilities. The underlying principles will not be elaborated here; please refer to the original reporter's writeup. There are 2 variants: CVE-2026-43500 requires RxRPC which is disabled for Android Generic Kernels; CVE-2026-43284 requires xfrm-ESP and is exploitable on Android. However, SELinux forbids untrusted apps from using that feature:
# Privileged netlink socket interfaces.
neverallow { appdomain -network_stack }
domain:{
netlink_tcpdiag_socket
netlink_nflog_socket
netlink_xfrm_socket
netlink_audit_socket
netlink_dnrt_socket
} *;
The only allowed domains are system_server, network_stack and netd. In order to exploit DirtyFrag, attackers must first compromise one of the allowlisted privileged processes.