CVE-2025-48646: ActivityStarter Confused Deputy Enables Local Privilege Escalation
A confused deputy in ActivityStarter.executeRequest() allows an unprivileged app to launch arbitrary activities in foreign task stacks, achieving local privilege escalation without extra permissions.
# Android Security Flaw Could Let Apps Do Things They Shouldn't
A security vulnerability has been discovered in Android's activity launcher system — the mechanism that lets apps communicate with and control other apps on your phone. Think of it like your phone's internal mail system: apps are supposed to only open letters addressed to them, but this flaw lets one app trick the system into opening other apps' private mail.
The problem occurs when an app doesn't properly check whether another app should be allowed to request certain actions. An attacker could exploit this by creating a malicious app that tricks your phone into launching powerful system functions it shouldn't have access to. It's similar to leaving your house keys with a friend to feed your cat, only to have them give your house keys to someone else.
Here's what makes this actually concerning: while the attacker needs you to install their app and interact with it in some way, they could then perform privileged operations normally locked down by Android's security system. This could include accessing sensitive features or data that your phone's permission system was supposed to protect.
Android users are most at risk, particularly those who install apps from outside Google's official Play Store, though the Play Store isn't completely immune. Anyone running Android 12 or later could be affected.
What you should actually do about this: One, stick to downloading apps from Google Play Store, which has automated protections. Two, be cautious about granting permissions to apps you don't fully trust. Three, keep your Android phone updated — manufacturers will likely patch this soon. For now, this vulnerability hasn't been actively used to attack people, so you're not in immediate danger if you follow basic app safety practices.
Want the full technical analysis? Click "Technical" above.
CVE-2025-48646 is a logic vulnerability in the Android Activity Manager stack, specifically in ActivityStarter.executeRequest() within ActivityStarter.java. The bug is a classic confused deputy: the system server, acting as a deputy on behalf of a calling application, can be tricked into launching an activity with the system's own authority rather than the caller's. The result is a local escalation of privilege — an unprivileged application can launch components it has no direct permission to start, including privileged system activities and protected intent targets.
CVSS 7.8 (HIGH). No additional execution privileges required. User interaction required (user must be induced to trigger the launch path). Reported in the Android Security Bulletin — March 2026.
Affected: Android versions per NVD advisory; patched in March 2026 ASB
Root Cause Analysis
The confused deputy arises during caller identity resolution inside executeRequest(). When an application invokes startActivityAsCaller(), the system server is supposed to substitute the original caller's UID/PID/permissions for the identity check that gates activity launch. The bug is that under a specific code path — reachable when request.callerToken resolves to a token owned by the system server itself — the effective caller UID used for permission checks is silently promoted to Process.SYSTEM_UID without validating that the original IPC caller is authorised to assume that identity.
// Pseudocode reconstruction of ActivityStarter.executeRequest()
// based on AOSP ActivityStarter.java (pre-patch)
int executeRequest(Request r) {
// Resolve who is actually launching this activity
int callingUid = r.callingUid;
int callingPid = r.callingPid;
// --- BUG: callerToken is attacker-controlled via startActivityAsCaller() ---
// If callerToken belongs to a system-owned ActivityRecord, the block below
// unconditionally trusts the token and promotes effective UID to SYSTEM.
ActivityRecord sourceRecord = getSourceRecord(r.callerToken); // (1)
if (sourceRecord != null) {
// Intended: copy caller identity from the source activity.
// Vulnerable: no verification that r.callingUid owns sourceRecord.
callingUid = sourceRecord.launchedFromUid; // (2) BUG: attacker supplies token,
callingPid = sourceRecord.launchedFromPid; // gets system identity for free
}
// Permission check now uses the spoofed callingUid (SYSTEM_UID = 1000)
int result = checkStartActivityPermission(
r.intent,
callingUid, // (3) BUG: this is now 1000, not the real caller
callingPid,
r.resolvedType,
r.aInfo
);
if (result != START_SUCCESS) {
return result; // never reached for system UID
}
// ... launch proceeds with full system authority
return startActivity(r, callingUid, callingPid, ...);
}
The three-step confusion is:
(1) Attacker obtains a reference to a ActivityRecord token belonging to a system activity (e.g., the Settings trampoline or a privileged assistant activity).
(2)launchedFromUid on that record is 1000 (system_server launched it). The deputy copies this unconditionally.
(3) The permission gate runs as UID 1000, allowing launch of any component regardless of android:permission, android:exported="false", or signature-level protection.
Root cause:executeRequest() substitutes the effective caller UID with launchedFromUid from an attacker-supplied callerToken without verifying the IPC caller owns or is permitted to impersonate that token's originating identity.
Exploitation Mechanics
EXPLOIT CHAIN — CVE-2025-48646:
1. INSTALL malicious app (standard user install, no special permissions).
2. OBTAIN a system ActivityRecord token:
- Start a visible Activity from within the malicious app.
- Invoke startActivityAsCaller() with intent targeting a known system
activity (e.g., com.android.settings/.SubSettings).
- On first (legitimate) launch, capture the IBinder token returned via
onActivityResult() or reflected through the task back-stack using
getCallingActivity() on the resulting activity context.
- This token's launchedFromUid == 1000 because Settings was started
by system_server during a previous privileged session or boot sequence.
3. CRAFT the malicious launch request:
- Build an Intent targeting a non-exported, signature-protected component
(e.g., com.android.systemui/.ImageWallpaper or a privileged provider
activity gated by android.permission.MANAGE_DEVICE_POLICY).
- Call IActivityTaskManager.startActivity() directly via reflection or
Binder, supplying the captured system token as callerToken.
4. BYPASS fires in executeRequest():
- getSourceRecord(capturedToken) returns the system ActivityRecord.
- callingUid is overwritten to 1000 (SYSTEM_UID).
- checkStartActivityPermission() passes unconditionally.
5. TARGET activity launches in the attacker's task stack with system-level
identity, inheriting:
- Access to content providers exported only to system UID.
- Ability to send system-only broadcasts as a side-effect.
- UI overlay in system window type if target uses TYPE_APPLICATION_OVERLAY
and checks callingUid rather than packageName.
6. USER INTERACTION: victim must tap a UI element that triggers step 2's
initial legitimate launch — a standard one-click social engineering vector.
The privileged activity launch can be used to exfiltrate data from system content providers, manipulate device policy components, or pivot to further privilege escalation chains by inheriting the launched component's permissions within the same task affinity.
Memory Layout
This is a logic/confused deputy bug, not a memory corruption vulnerability. The relevant object graph in the system_server heap is:
system_server heap (ART managed heap, ActivityTaskManagerService context):
ActivityRecord [system-owned, token = IBinder@0xb4000071a3c82a80]
+0x00 IBinder token = 0xb4000071a3c82a80 <-- attacker captures this
+0x08 int launchedFromUid = 0x000003E8 <-- UID 1000 (SYSTEM)
+0x0C int launchedFromPid = 0x00000924 <-- system_server PID
+0x10 String launchedFromPackage = "android"
+0x18 Intent intent =
+0x20 int state = STOPPED
ActivityStarter.Request [attacker-controlled, on Binder thread stack]:
+0x00 IBinder callerToken = 0xb4000071a3c82a80 <-- SPOOFED: system record
+0x08 int callingUid = 0x00002B67 <-- real caller UID (11111)
+0x0C int callingPid = 0x00003A1C <-- real caller PID
+0x10 Intent intent =
+0x18 int startFlags = 0x00000000
AFTER executeRequest() bug triggers:
effective callingUid used for permission check = 0x000003E8 (1000, SYSTEM)
actual IPC caller UID = 0x00002B67 (unprivileged app)
delta: +8740 UID units of unearned authority
Patch Analysis
The fix adds an ownership validation step before allowing the callerToken-derived identity substitution. The system server must verify the IPC caller's real UID is permitted to act as the token's originating identity before copying launchedFromUid.
// BEFORE (vulnerable — pre March 2026 ASB):
ActivityRecord sourceRecord = getSourceRecord(r.callerToken);
if (sourceRecord != null) {
callingUid = sourceRecord.launchedFromUid;
callingPid = sourceRecord.launchedFromPid;
}
int result = checkStartActivityPermission(r.intent, callingUid, callingPid, ...);
// AFTER (patched — March 2026 ASB):
ActivityRecord sourceRecord = getSourceRecord(r.callerToken);
if (sourceRecord != null) {
// FIX: only allow identity substitution if the real IPC caller
// is the same app (or system) that originally launched sourceRecord.
// Prevents a third-party app from hijacking a system-owned token.
if (r.realCallingUid == sourceRecord.launchedFromUid
|| r.realCallingUid == Process.SYSTEM_UID
|| isCallerAllowedToLaunchAsCaller(r.realCallingUid, sourceRecord)) {
callingUid = sourceRecord.launchedFromUid;
callingPid = sourceRecord.launchedFromPid;
} else {
// BUG WOULD HAVE FIRED HERE: reject spoofed token, use real caller identity
Slog.w(TAG, "executeRequest: callerToken UID mismatch, ignoring token. "
+ "realCallingUid=" + r.realCallingUid
+ " tokenUid=" + sourceRecord.launchedFromUid);
// callingUid remains r.callingUid (unprivileged)
}
}
int result = checkStartActivityPermission(r.intent, callingUid, callingPid, ...);
The key addition is isCallerAllowedToLaunchAsCaller() — a new helper that consults the caller's declared permissions and verifies UID equality before granting token-based identity substitution. A secondary fix adds the mismatch log entry, making exploitation attempts visible in logcat at the WARN level.
Detection and Indicators
Post-patch, the Slog.w entry provides a direct detection signal. On unpatched devices, the following heuristics apply:
Logcat (unpatched): Absence of expected permission denial logs when a third-party app launches a signature-protected activity. Look for ActivityManager: START lines where the launching package does not hold the required permission.
Binder trace:startActivityAsCaller() invocations from non-system UIDs supplying a callerToken that resolves to a system-owned ActivityRecord. Capture via atrace -t 10 -b 32768 am wm binder_driver.
Post-patch log signal:W ActivityTaskManager: executeRequest: callerToken UID mismatch in logcat indicates an exploitation attempt was blocked.
Drozer / manual audit: Call IActivityTaskManager.startActivity() with a non-exported target and a mismatched callerToken; a patched device returns START_NOT_CURRENT_USER_ACTIVITY or START_PERMISSION_DENIED.
Apply March 2026 Android Security Bulletin patches immediately. Verify with Settings → About → Android Security Patch Level ≥ 2026-03-01.
OEM/ODM integrators: Cherry-pick the ActivityStarter.java fix onto all active LTS branches. The patch touches executeRequest() and the new isCallerAllowedToLaunchAsCaller() helper — both must be applied atomically.
App developers relying on startActivityAsCaller() for legitimate delegation flows should audit their token-passing logic: never pass a token received from an untrusted third party into this API.
Enterprise MDM: Until patched, restrict sideloading and unknown-source installs. The exploit requires a malicious app to be installed and a user to interact with a crafted UI element — standard app vetting significantly reduces risk.
Security monitoring: Enable verbose ActivityManager logging on managed device fleets and alert on the post-patch mismatch log signal as an indicator of active exploitation attempts against unpatched endpoints in the same fleet.