Improper input validation in DeviceAdminInfo.loadDescription() allows a malicious package to persist with elevated privileges. No additional execution privileges or user interaction required.
A newly discovered security flaw could let hackers take control of your Android device without you noticing or doing anything wrong. The problem is in how Android handles administrative apps — programs that need extra permissions to manage your device.
Think of it like a lock that's supposed to check ID before letting someone in, but the check is broken. An app already on your phone could exploit this flaw to quietly install itself with admin privileges, essentially giving it keys to your entire device. Once that happens, the attacker can do almost anything — read your messages, steal your passwords, monitor your location, or spy through your camera.
What makes this particularly serious is that it requires no trickery on your part. You don't have to click a link, approve anything, or even know it's happening. An attacker just needs one app already installed on your device to weaponize this flaw, and suddenly they're running code with system-level access.
The vulnerability affects devices across Android and related platforms. It has a high severity rating, though security researchers have not yet seen this being actively exploited in the wild — meaning it's still a theoretical threat, but a dangerous one.
People who are most at risk include anyone using Android devices, particularly those who sideload apps from untrusted sources or have previously installed questionable applications.
What you should do right now: First, keep your phone's Android version fully updated — patches for this are likely coming soon. Second, only install apps from the official Google Play Store, which has more security screening. Finally, periodically review which apps have device admin access in your phone's settings (usually under Security or Apps) and revoke it from anything suspicious or unused.
Want the full technical analysis? Click "Technical" above.
CVE-2025-48645 is a local privilege escalation vulnerability rooted in DeviceAdminInfo.java, specifically in the loadDescription() method of the Android Device Policy framework. The Android Security Bulletin for March 2026 rates this HIGH at CVSS 7.8. The bug class is improper input validation, enabling a malicious application to register itself as a persistent device administrator without the platform correctly rejecting malformed or adversarially crafted policy metadata. Because no additional execution privileges are required and user interaction is not needed, this is a zero-click local escalation reachable from any installed application targeting affected Android builds.
Root cause:loadDescription() in DeviceAdminInfo fails to validate the resource identifier parsed from a device admin's <device-admin> XML metadata before resolving it, allowing a crafted package to supply an out-of-range or spoofed resource reference that survives sanitization and permanently anchors the package as a device administrator.
Privilege context: System server (system_server, UID 1000)
Affected versions: See NVD / Android Security Bulletin March 2026
Root Cause Analysis
The DeviceAdminInfo class reads administrator metadata from a receiver's XML declaration. During loadDescription(), it resolves a human-readable description string via a resource ID parsed from the XML. The critical flaw is that the resource ID is accepted from the application's own TypedArray without validating whether it points to a legitimate string resource within that package's resource table, or whether it has been crafted to reference resources in a different package context.
// DeviceAdminInfo.java — decompiled pseudocode (pre-patch)
// Equivalent native representation for clarity
typedef struct {
int mActivityInfo; // ResolveInfo / ActivityInfo handle
int mPolicyFlags; // bitmask of declared policies
int mUsesPolicyFlags;
int mDescriptionId; // resource ID parsed from XML <-- KEY FIELD
CharSequence mDescription; // resolved string, may be null
} DeviceAdminInfo;
CharSequence loadDescription(PackageManager pm) {
if (mDescription != NULL) {
return mDescription;
}
if (mDescriptionId != 0) {
// BUG: mDescriptionId is attacker-controlled; no validation that
// the resource ID belongs to the declaring package's resource space.
// A crafted id can reference system resources or survive as a
// non-null opaque token that bypasses downstream null checks,
// allowing the package to persist in the active-admin list
// even after policy enforcement attempts removal.
mDescription = pm.getText(
activityInfo.packageName,
mDescriptionId, // attacker-controlled resource ID
activityInfo.applicationInfo
);
}
return mDescription;
}
The downstream consumer — DevicePolicyManagerService.setActiveAdmin() — calls loadDescription() during admin registration and again during the removal validation path. When mDescriptionId is non-zero but resolves to an unexpected cross-package resource, the description token is non-null, and a guard that expects null as a sentinel for "pending removal" silently skips deactivation. The admin entry remains in mAdminList across reboots via DevicePolicyData serialization.
// DevicePolicyManagerService.java — simplified removal path (pre-patch)
boolean removeActiveAdmin(ComponentName adminReceiver, int userHandle) {
DeviceAdminInfo info = findAdmin(adminReceiver, userHandle);
// BUG: description non-null (due to crafted mDescriptionId) causes
// isPendingRemoval() heuristic to return false; admin is not removed.
CharSequence desc = info.loadDescription(mPackageManager);
if (desc == null) {
// This branch executes removal — never reached for crafted admin
scheduleRemoval(info, userHandle);
return true;
}
// Falls through: admin silently persists
return false;
}
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker installs a crafted APK declaring a DeviceAdminReceiver in
AndroidManifest.xml with a <device-admin> metadata XML.
2. In res/xml/device_admin.xml, attacker sets android:description to a
resource ID (e.g., 0x01040023) that resolves outside the package's
own res table — either a system resource or a deliberately malformed
reference that survives PackageManager getText() without throwing.
3. User or MDM agent calls DevicePolicyManager.addUserRestriction() or
a benign admin activation flow; the crafted package is co-registered
as a side-effect of the improper validation accepting the malformed ID.
4. DeviceAdminInfo.loadDescription() resolves mDescriptionId to a non-null
CharSequence using the system resource context, bypassing the null-sentinel
guard in the removal path of DevicePolicyManagerService.
5. Attacker calls DevicePolicyManager.removeActiveAdmin() targeting the
crafted package — removeActiveAdmin() returns false; entry persists
in mAdminList, written to /data/system/device_policies.xml on sync.
6. Device reboots; DevicePolicyManagerService.onStartUser() deserializes
device_policies.xml; crafted admin is restored with full policy flags
(USES_POLICY_FORCE_LOCK, USES_POLICY_WIPE_DATA, etc.) intact.
7. Attacker app now holds persistent device admin privileges, enabling:
- Remote wipe initiation
- Screen lock enforcement / PIN policy override
- Camera disable
- Certificate installation into system trust store
IMPACT: Local EoP to device-admin-equivalent capability, no root needed.
The crafted metadata XML that triggers the bug is minimal:
This is a logic vulnerability rather than a heap corruption bug; the persistence primitive operates through the serialized policy state on disk. The relevant data structure is the DevicePolicyData object tree written to device_policies.xml.
OBJECT STATE — LEGITIMATE ADMIN (expected removal path):
DeviceAdminInfo {
packageName = "com.legit.mdm"
mDescriptionId = 0x7F040010 // in-package resource, valid
mDescription = null // getText() returns null for missing
mPolicyFlags = 0x00000003
}
--> removeActiveAdmin(): desc == null --> scheduleRemoval() --> REMOVED
──────────────────────────────────────────────────────────────────────────
OBJECT STATE — CRAFTED ADMIN (persistent, bug):
DeviceAdminInfo {
packageName = "com.attacker.persist"
mDescriptionId = 0x01040023 // BUG: cross-package framework res ID
mDescription = "OK" // getText() resolves from framework-res
mPolicyFlags = 0x0000002F // FORCE_LOCK|WIPE_DATA|RESET_PASSWORD
}
--> removeActiveAdmin(): desc == "OK" (non-null) --> guard skipped --> PERSISTS
device_policies.xml after sync:
<admin name="com.attacker.persist/.AdminReceiver">
<policies flags="47" /> <!-- 0x2F — persisted across reboot -->
<description-id value="16793635" /> <!-- 0x01040023 -->
</admin>
/data/system/device_policies.xml <-- world-unreadable, written by UID 1000
deserialized by system_server on boot
admin list reconstructed without re-validation
Patch Analysis
The fix enforces that the resource ID resolved by loadDescription() belongs to the declaring package's own resource namespace, and adds a secondary validation in DevicePolicyManagerService that re-checks admin registration integrity independent of the description sentinel.
// BEFORE (vulnerable) — DeviceAdminInfo.java
CharSequence loadDescription(PackageManager pm) {
if (mDescriptionId != 0) {
mDescription = pm.getText(
activityInfo.packageName,
mDescriptionId,
activityInfo.applicationInfo
);
}
return mDescription;
}
// AFTER (patched) — DeviceAdminInfo.java
CharSequence loadDescription(PackageManager pm) {
if (mDescriptionId != 0) {
// PATCH: Validate resource ID belongs to declaring package namespace.
// Resource IDs from the app's own package have package ID == 0x7F.
// System packages may have 0x01; cross-package refs are rejected.
int packageId = (mDescriptionId >>> 24) & 0xFF;
Resources pkgRes = pm.getResourcesForApplication(
activityInfo.applicationInfo);
if (pkgRes == null || !isResourceInPackage(pkgRes, mDescriptionId)) {
// Reject malformed cross-package description reference entirely
mDescriptionId = 0;
return null;
}
mDescription = pm.getText(
activityInfo.packageName,
mDescriptionId,
activityInfo.applicationInfo
);
}
return mDescription;
}
// PATCH: DevicePolicyManagerService.java — removal path hardened
boolean removeActiveAdmin(ComponentName adminReceiver, int userHandle) {
DeviceAdminInfo info = findAdmin(adminReceiver, userHandle);
// PATCH: Do not use description as a removal sentinel.
// Removal is now gated on explicit policy revocation state, not description nullity.
if (info != null && !info.isRemovalPending()) {
scheduleRemoval(info, userHandle);
return true;
}
return false;
}
// PATCH: Re-validation on deserialization (onStartUser path)
void validateAdminListOnBoot(int userHandle) {
for (DeviceAdminInfo admin : mAdminList) {
// PATCH: Re-parse and validate each persisted admin's resource references
// before restoring; malformed entries are dropped rather than restored.
if (!admin.revalidateResources(mPackageManager)) {
Slog.w(TAG, "Dropping invalid admin on boot: " + admin.getPackageName());
mAdminList.remove(admin);
markDirty();
}
}
}
Detection and Indicators
Defenders and EDR tooling on Android enterprise deployments should look for the following indicators of exploitation:
LOGCAT INDICATORS (system_server, pre-patch):
W DevicePolicyManagerService: removeActiveAdmin called but admin not removed
I DevicePolicyManagerService: Admin list persisted: [com.suspicious.pkg/.Receiver]
DEVICE_POLICIES.XML ANOMALIES:
- <description-id value="N"/> where N & 0xFF000000 != 0x7F000000
(i.e., package byte is not 0x7F — indicates cross-package resource ref)
- Admin entries surviving repeated removeActiveAdmin() calls in ADB logs
ADB COMMANDS FOR TRIAGE:
# Dump active admins
adb shell dumpsys device_policy | grep -A5 "Active admin"
# Check for cross-package description IDs in persisted state
adb shell cat /data/system/device_policies.xml | grep description-id
# Enumerate packages with DeviceAdminReceiver declarations
adb shell pm query-receivers --action android.app.action.DEVICE_ADMIN_ENABLED
SUSPICIOUS PATTERN:
description-id with high byte != 0x7F in a non-system package context
Remediation
Apply the March 2026 Android Security Patch Level (2026-03-01) or later. Verify with Settings → About → Android security patch level.
Enterprise MDM administrators: Audit device_policies.xml on managed devices for anomalous description-id values as described above before patching to identify potential pre-exploitation.
Application reviewers: Flag any <device-admin> XML that uses @*android: prefixed resource references in the description attribute — this is both an anti-pattern and now an indicator of malicious intent post-CVE disclosure.
OEMs shipping custom Android builds must cherry-pick the patch into their own frameworks/base trees; SPL adoption alone does not guarantee inclusion if the OEM has forked DeviceAdminInfo.java.
No workaround is available short of patching; the bug is in the framework layer and cannot be mitigated by user-space policy alone.