A missing permission check in createSessionInternal allows any app to hijack installer session ownership, enabling local privilege escalation without additional execution privileges.
Android phones have a system for installing and updating apps that's supposed to work like a bouncer at an exclusive club—it checks your credentials before letting you do anything important. A new security flaw discovered in this system forgot to do that checking.
Here's the problem: when an app wants to update itself or install something new, it has to ask permission first. This vulnerability lets a misbehaving app skip that question entirely and just go ahead anyway. Think of it like a house guest who doesn't just ask to use your kitchen—they somehow convince your home security system they own the place.
An attacker could use this to install malware in a way that looks legitimate, or upgrade a harmless app into a dangerous one without your knowledge. The damage could range from stealing your passwords to eavesdropping through your microphone.
The good news: no one's actively exploiting this yet in the wild, which means you likely haven't been targeted. The bad news: once this gets fixed, attackers know exactly where to look if they want to cause trouble before updates roll out.
Who's at risk? Mostly people with older Android devices that take months to receive security patches, and anyone who installed an app they didn't fully vet. It doesn't require you to click anything suspicious—just having the malicious app installed is enough.
What you can do: First, keep your phone updated as soon as patches arrive. Second, only download apps from the official Google Play Store, which has more security checks than random websites. Third, periodically check your installed apps and uninstall anything you don't recognize or use anymore. These steps won't prevent a determined attacker, but they dramatically reduce your chances of being a victim.
Want the full technical analysis? Click "Technical" above.
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.
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
# 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.