home intel cve-2025-48646-activitystarter-confused-deputy-lpe
CVE Analysis 2026-03-02 · 9 min read

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.

#confused-deputy#intent-validation#activity-hijacking#privilege-escalation#android-security
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2025-48646 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2025-48646HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

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 Component

  • File: frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
  • Method: ActivityStarter.executeRequest(Request r)
  • Service: ActivityTaskManagerService (system_server process, UID 1000)
  • IPC Surface: IActivityTaskManager.startActivity() / startActivityAsCaller()
  • 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. (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. (2) launchedFromUid on that record is 1000 (system_server launched it). The deputy copies this unconditionally.
  3. (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.
// adb logcat filter for exploitation attempt (patched device):
adb logcat -s ActivityTaskManager:W | grep "callerToken UID mismatch"

// Expected output on blocked attempt:
W ActivityTaskManager: executeRequest: callerToken UID mismatch,
    ignoring token. realCallingUid=11111 tokenUid=1000

Remediation

  • 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.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →