home intel cve-2026-0023-packageinstaller-ownership-escalation
CVE Analysis 2026-03-02 · 8 min read

CVE-2026-0023: PackageInstallerService Missing Permission Check Enables Privilege Escalation

A missing permission check in createSessionInternal allows any app to hijack installer session ownership, enabling local privilege escalation without additional execution privileges.

#privilege-escalation#permission-check-bypass#package-installer#local-attack#android-framework
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-0023 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-0023HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-0026-0023, disclosed in the Android Security Bulletin — March 2026, is a logic-level privilege escalation in PackageInstallerService.java. The bug lives in createSessionInternal(), a method responsible for establishing installation sessions on behalf of calling applications. Due to a missing permission check, a third-party app can supply an arbitrary installer package name and effectively claim ownership of an installation session it did not originate — breaking the chain of trust that ties session ownership to verified callers.

CVSS score: 7.8 (HIGH). No user interaction required. No additional execution privileges needed beyond a standard unprivileged application context.

Root cause: createSessionInternal() accepts a caller-supplied installerPackageName without verifying that the calling UID actually owns or is associated with that package, allowing arbitrary session ownership assignment.

Affected Component

  • File: frameworks/base/services/core/java/com/android/server/pm/PackageInstallerService.java
  • Method: createSessionInternal(SessionParams params, String installerPackageName, String installerAttributionTag, int userId)
  • Service: PackageInstallerService — the system server component backing IPackageInstaller
  • Affected versions: See NVD / Android Security Bulletin March 2026
  • Privilege boundary crossed: Unprivileged app UID → installer session ownership (system-level install trust)

Root Cause Analysis

The installation session model in Android ties a session's authority to the creating application's identity. SessionParams.installerPackageName is a field the caller populates before invoking createSession(). In the vulnerable code path, the system server receives this field and uses it verbatim — without asserting the calling UID maps to that package name via PackageManager.

// PackageInstallerService.java — createSessionInternal() — VULNERABLE
// Pseudocode reconstruction from decompiled AOSP sources

int createSessionInternal(SessionParams params,
                          String installerPackageName,
                          String installerAttributionTag,
                          int userId) {

    final int callingUid = Binder.getCallingUid();
    final int callingPid = Binder.getCallingPid();

    // ... permission checks for INSTALL_PACKAGES follow for specific fields ...

    // BUG: installerPackageName is accepted from caller-controlled params
    // with no verification that callingUid owns installerPackageName.
    // An attacker can pass any package name here — including system packages.
    String resolvedInstallerPackage = params.installerPackageName;  // attacker-controlled

    // The session is created and registered under resolvedInstallerPackage,
    // not under the verified owner of callingUid.
    PackageInstallerSession session = new PackageInstallerSession(
        mSessionCallback,
        mContext,
        mPm,
        this,
        mStagingManager,
        mInstallThread.getLooper(),
        mStagingManager,
        sessionId,
        userId,
        callingUid,
        resolvedInstallerPackage,   // BUG: attacker-supplied, not validated
        installerAttributionTag,
        params,
        createdMillis,
        stageDir,
        stageCid,
        prepared,
        committed,
        destroyed,
        sealed
    );

    // Session is stored in mSessions; ownership is now set to attacker's string
    mSessions.put(sessionId, session);
    return sessionId;
}

The critical omission: the patched version of this method calls something equivalent to mPm.getPackagesForUid(callingUid) and asserts that resolvedInstallerPackage appears in the returned array. Without this check, the mapping is purely trust-based on caller input passed over Binder.

Session ownership in Android's package installer is not cosmetic. It gates requestUpdateOwnership semantics — introduced in Android 14 — which allow the session's declared installer to be treated as the authoritative update owner of an installed package. Hijacking this field means a malicious app can register itself (or a spoofed system package identity) as the update owner of an arbitrary installed APK.

Exploitation Mechanics

EXPLOIT CHAIN — CVE-2026-0023

1. Attacker installs a zero-permission APK (minSdk target, no declared permissions).

2. App constructs a SessionParams object:
       SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
       params.setInstallerPackageName("com.android.vending");
       params.setRequestUpdateOwnership(true);

3. App calls PackageInstaller.createSession(params).
   → IPC crosses into system_server via Binder.
   → createSessionInternal() invoked with callingUid = unprivileged app UID.

4. BUG: system_server stores session with installerPackageName = "com.android.vending"
   despite callingUid having no association with that package.
   Session is registered in mSessions under the spoofed identity.

5. App commits the session targeting a victim package already installed
   (e.g., a banking app previously installed by the Play Store).

6. On commit, PackageInstallerSession.computeUser() resolves installer identity
   from the stored (spoofed) installerPackageName, not from callingUid.

7. The victim package's update ownership record in PackageManager is
   overwritten: app becomes the declared update owner of the target package.

8. Attacker now controls future updates to the target package under the
   spoofed installer identity — enabling silent siloed update injection
   or blocking legitimate updates from the real installer.

IMPACT SUMMARY:
- Ownership of any installed package can be claimed
- No special permissions required on the attacking app
- Works silently; no user prompt triggered
- Effective against packages installed by privileged installers

Memory Layout

This is a logic vulnerability, not a memory corruption bug — heap diagrams are not the relevant attack surface. The critical data structure is the in-memory session registry and the PackageInstallerSession object layout that persists installer identity.

PackageInstallerSession — relevant field layout (reconstructed):

  +0x00  int            sessionId
  +0x04  int            userId
  +0x08  int            installerUid        ← set to real callingUid (correct)
  +0x10  String         installerPackageName ← set from params (ATTACKER-CONTROLLED)
  +0x18  String         installerAttributionTag
  +0x20  SessionParams  params
  +0x28  boolean        mRequestUpdateOwnership  ← set true by attacker via params
  ...

mSessions (SparseArray):
  key=sessionId → PackageInstallerSession {
      installerUid          = 10234  (unprivileged app)
      installerPackageName  = "com.android.vending"  ← SPOOFED
      mRequestUpdateOwnership = true
  }

PackageManager update ownership record (post-commit):
  pkg="com.target.banking"
    updateOwner = "com.android.vending"  ← overwritten from spoofed session
    previously  = "com.android.vending"  ← attacker matches; no conflict detected

Patch Analysis

The fix enforces that the caller-supplied installerPackageName is actually owned by callingUid before accepting it. If no package name is supplied, the system resolves it automatically from the calling UID.

// BEFORE (vulnerable):
String resolvedInstallerPackage = params.installerPackageName;
// No validation — attacker-controlled string used directly as session owner.


// AFTER (patched — March 2026 bulletin):
String resolvedInstallerPackage = params.installerPackageName;

if (resolvedInstallerPackage != null) {
    // Verify callingUid actually owns the claimed package name.
    // mPm.getPackagesForUid() returns packages registered to this UID.
    String[] packagesForUid = mPm.getPackagesForUid(callingUid);
    if (packagesForUid == null ||
        !ArrayUtils.contains(packagesForUid, resolvedInstallerPackage)) {
        // Caller does not own the declared installer package name.
        throw new SecurityException(
            "UID " + callingUid + " does not own package " +
            resolvedInstallerPackage);
    }
} else {
    // Auto-resolve: pick the first package associated with callingUid.
    String[] packagesForUid = mPm.getPackagesForUid(callingUid);
    resolvedInstallerPackage = (packagesForUid != null && packagesForUid.length > 0)
        ? packagesForUid[0]
        : null;
}
// resolvedInstallerPackage is now verified against callingUid before use.

The additional guard for requestUpdateOwnership semantics also validates that the session's resolved installer is consistent with the caller before allowing the ownership transfer to propagate into PackageManager's internal records on commitSession().

Detection and Indicators

Because this is a logic flaw with no memory corruption, crash logs will not reveal exploitation. Detection requires behavioral and audit-log analysis.

Logcat indicators (system_server):
// Anomalous: session created where installerPackageName != packages owned by UID
PackageInstallerService: createSession [sessionId=1337] uid=10234
  installerPackageName=com.android.vending
  // com.android.vending is uid=10137 — MISMATCH, should have thrown SecurityException

// Post-patch: attack attempt produces:
PackageInstallerService: SecurityException: UID 10234 does not own package com.android.vending
Programmatic detection (privileged monitoring agent):
# Audit active installer sessions for UID/package mismatches
import subprocess, json

def audit_sessions():
    raw = subprocess.check_output(
        ["adb", "shell", "dumpsys", "package", "installer-sessions"],
        text=True
    )
    # Parse sessions and cross-reference installerPackageName with known UID mappings
    for line in raw.splitlines():
        if "installerPackageName=" in line and "installerUid=" in line:
            pkg  = line.split("installerPackageName=")[1].split()[0]
            uid  = int(line.split("installerUid=")[1].split()[0])
            owned = get_packages_for_uid(uid)  # via pm list packages --uid
            if pkg not in owned:
                print(f"[SUSPICIOUS] uid={uid} claims installer={pkg}, owns={owned}")

audit_sessions()

Remediation

  • Apply the March 2026 Android Security Patch Level (SPL: 2026-03-01) or later. This is the only complete fix.
  • Device vendors shipping custom forks of PackageInstallerService must backport the validation block to their own trees — the check is not automatic in vendor branches.
  • Apps that use SessionParams.setInstallerPackageName() should be audited to confirm they only ever supply their own package name; supply chain tooling that sets arbitrary installer names for analytics purposes may be affected by the patch.
  • Enterprise MDM deployments using requestUpdateOwnership for managed package control should re-validate session integrity after patching to confirm no ownership records were corrupted by exploitation prior to patch application.
  • On unpatched devices, restrict sideloading vectors and monitor dumpsys package installer-sessions output for the UID/package mismatch pattern described above.
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 →