CVE-2026-0034: ManagedServices Policy Desync Enables Local Privilege Escalation
A missing validation check in setPackageOrComponentEnabled() allows an unprivileged caller to desync Android's notification policy state, enabling local privilege escalation without user interaction.
# Android Notification System Has a Dangerous Shortcut
Your phone's notification settings might have a security flaw that lets apps quietly gain extra permissions they shouldn't have. Think of it like a lock on your apartment door that doesn't properly check whether someone has a real key.
The problem sits in Android's notification management system, which decides what apps can show you alerts and messages. A malicious app could exploit this to trick the system into thinking it has legitimate permissions it never asked for—essentially sneaking past the normal safety checks.
Here's why this matters: notifications touch everything on your phone. An app that gains unexpected permissions through this trick could potentially access your messages, location, contacts, or camera without you ever noticing. It happens silently in the background with no warning dialog.
Android phones are most at risk, particularly older devices that haven't received security updates. This is one of several reasons why security experts always recommend keeping your phone updated—manufacturers patch these kinds of holes regularly.
The good news is there's no evidence anyone is actively exploiting this yet, so you're not in immediate danger. But once this vulnerability becomes widely known, that could change quickly.
What you can do right now: First, check your Android phone's settings and make sure it's running the latest software update available. Go to Settings, About Phone, and look for system updates. Second, be cautious about which apps you give notification permissions to—only approve this for apps you genuinely trust. Finally, consider enabling Google Play Protect if you haven't already, which scans your phone for suspicious apps regularly.
Want the full technical analysis? Click "Technical" above.
CVE-2026-0034 is a logic-class vulnerability residing in ManagedServices.java, specifically within the setPackageOrComponentEnabled() method responsible for governing which packages and components are permitted to act as notification listeners, condition providers, or assistant services. Improper input validation allows an attacker to induce a notification policy desync — a state where the in-memory representation of allowed components diverges from the persisted policy on disk — which can be leveraged for local escalation of privilege. No additional execution privileges are required, and no user interaction is necessary. CVSS 8.4 (HIGH).
The vulnerability was patched in the Android March 2026 Security Bulletin. It affects all Android releases prior to the patch date across the framework component frameworks/base.
Root cause:setPackageOrComponentEnabled() fails to validate that the caller-supplied component name resolves to a package already present in the approved managed services set before mutating the enabled/disabled state map, allowing a desync between runtime policy state and the persisted allowlist that the system trusts for capability checks.
Affected Component
The bug lives entirely within the Android framework notification subsystem:
Method:setPackageOrComponentEnabled(String pkgOrComponent, int userId, boolean isPrimary, boolean enabled, boolean userSet)
Callers:NotificationManagerService, ConditionProviderService binding logic, Settings UI via INotificationManager Binder interface
Permission context: Callable by system UID and, critically, by the owning package for self-management paths — the validation gap exists on the self-management path
Root Cause Analysis
ManagedServices maintains two authoritative data structures: mApproved (the persistent allowlist of approved packages/components per user) and mEnabledServicesForCurrentProfiles (the runtime set used for capability checks). The policy desync occurs because setPackageOrComponentEnabled() writes into mEnabledServicesForCurrentProfiles without first confirming the target is present in mApproved.
// Reconstructed pseudocode of the vulnerable method in ManagedServices.java
// Equivalent Java logic shown as pseudocode for clarity.
void setPackageOrComponentEnabled(
String pkgOrComponent,
int userId,
boolean isPrimary,
boolean enabled,
boolean userSet) {
// Normalize to ComponentName or package string
ComponentName cn = ComponentName.unflattenFromString(pkgOrComponent);
// BUG: No validation that pkgOrComponent exists in mApproved[userId]
// before modifying the enabled state. An attacker-supplied component
// that was never approved can be inserted into mEnabledServicesPackages
// or removed from it, causing the runtime set to diverge from policy.
ArrayMap> userServices = mEnabledServicesForCurrentProfiles;
if (cn == null) {
// Package-level enable/disable path
ArraySet pkgSet = userServices.getOrDefault(isPrimary, new ArraySet<>());
if (enabled) {
pkgSet.add(pkgOrComponent); // BUG: unconditional add, no approval check
} else {
pkgSet.remove(pkgOrComponent);
}
userServices.put(isPrimary, pkgSet);
} else {
// Component-level path — same issue
String flat = cn.flattenToString();
ArraySet compSet = userServices.getOrDefault(isPrimary, new ArraySet<>());
if (enabled) {
compSet.add(flat); // BUG: attacker-supplied component inserted
} else {
compSet.remove(flat);
}
userServices.put(isPrimary, compSet);
}
// Policy written back to disk without cross-checking mApproved
rebindServices(false, userId); // triggers capability re-evaluation
}
The key invariant that should hold is: mEnabledServicesForCurrentProfiles ⊆ mApproved. The missing check breaks this invariant. When rebindServices() subsequently iterates over mEnabledServicesForCurrentProfiles to bind notification listener services, it trusts this set as authoritative and will attempt to bind — and grant listener capabilities to — components that were never formally approved.
Exploitation Mechanics
EXPLOIT CHAIN — CVE-2026-0034:
1. SETUP: Attacker installs a malicious app (com.evil.listener) containing a
declared NotificationListenerService component. App is NOT granted
notification access through the normal Settings approval flow, so
com.evil.listener is absent from mApproved[userId].
2. TRIGGER: Attacker calls INotificationManager.setNotificationListenerAccessGranted()
(or the package self-management Binder path) with:
pkgOrComponent = "com.evil.listener/com.evil.listener.ListenerSvc"
enabled = true
userSet = false <-- bypasses the userSet guard in some paths
3. DESYNC: setPackageOrComponentEnabled() inserts the component into
mEnabledServicesForCurrentProfiles without consulting mApproved.
Runtime state now shows com.evil.listener as enabled.
4. REBIND: rebindServices() iterates the now-poisoned runtime set,
calls bindServiceAsUser() for com.evil.listener.ListenerSvc,
granting it BIND_NOTIFICATION_LISTENER_SERVICE — a privileged binding
that exposes all active notifications cross-user.
5. ESCALATION: The bound ListenerService receives onNotificationPosted()
callbacks containing notification payloads from all foreground UIDs,
including system app notifications carrying auth tokens, OTP codes,
and inter-process messaging data from privileged callers.
6. PERSISTENCE: Because the poisoned entry lives in the runtime set and
rebindServices() is called on boot, the malicious listener survives
device reboots until mApproved is explicitly reconciled.
Memory Layout
This is a logic/state desync vulnerability rather than a memory corruption bug, but the critical data structure layout is worth examining. The desync affects two heap-resident ArrayMap objects within the ManagedServices singleton in the system_server process.
The fix introduces an explicit approval membership check immediately upon entry to setPackageOrComponentEnabled(), before any mutation of the runtime state map. The patch ensures the invariant runtime ⊆ approved is enforced at write time rather than relying on callers to pre-validate.
// BEFORE (vulnerable) — ManagedServices.java pre-patch:
void setPackageOrComponentEnabled(String pkgOrComponent, int userId,
boolean isPrimary, boolean enabled, boolean userSet) {
ComponentName cn = ComponentName.unflattenFromString(pkgOrComponent);
// No approval membership check here.
// Proceeds directly to mutate mEnabledServicesForCurrentProfiles.
ArraySet targetSet = getOrCreateEnabledSet(isPrimary, userId);
if (enabled) {
targetSet.add(pkgOrComponent);
} else {
targetSet.remove(pkgOrComponent);
}
rebindServices(false, userId);
}
// AFTER (patched) — March 2026 Security Bulletin fix:
void setPackageOrComponentEnabled(String pkgOrComponent, int userId,
boolean isPrimary, boolean enabled, boolean userSet) {
ComponentName cn = ComponentName.unflattenFromString(pkgOrComponent);
// PATCH: validate membership in mApproved before mutating runtime state.
// If enabling, the component/package MUST already be in the approved set.
if (enabled && !isPackageOrComponentAllowed(pkgOrComponent, userId)) {
Slog.w(TAG, "setPackageOrComponentEnabled: " + pkgOrComponent
+ " not in approved set for user " + userId + ", ignoring.");
return; // <-- early exit prevents desync
}
ArraySet targetSet = getOrCreateEnabledSet(isPrimary, userId);
if (enabled) {
targetSet.add(pkgOrComponent);
} else {
targetSet.remove(pkgOrComponent);
}
rebindServices(false, userId);
}
// isPackageOrComponentAllowed() (new helper, also patched in):
boolean isPackageOrComponentAllowed(String pkgOrComponent, int userId) {
ArrayMap> approved = mApproved.get(userId);
if (approved == null) return false;
for (ArraySet approvedSet : approved.values()) {
if (approvedSet.contains(pkgOrComponent)) return true;
// Also check package prefix match for component entries
ComponentName cn = ComponentName.unflattenFromString(pkgOrComponent);
if (cn != null && approvedSet.contains(cn.getPackageName())) return true;
}
return false;
}
Detection and Indicators
Detection focuses on observing the desync condition at runtime within system_server. The following indicators suggest active exploitation or a device in a desynced state:
Logcat tag ManagedServices: Post-patch devices will emit the new Slog.w warning on blocked attempts. Pre-patch devices will be silent.
Anomalous notification listener bindings:adb shell dumpsys notification — inspect the Enabled listeners section for components not present in Allowed notification listeners in Settings.
Cross-reference mApproved vs runtime bound services: Any component bound via BIND_NOTIFICATION_LISTENER_SERVICE that lacks a corresponding entry in /data/system/notification_policy.xml is suspect.
SELinux audit logs: Binding a listener service from an unprivileged UID may generate avc: denied entries in dmesg depending on device policy — absence of such denials on a pre-patch device confirms successful exploitation.
// Audit command to detect desynced listener state:
$ adb shell dumpsys notification | grep -A 50 "Enabled listeners"
$ adb shell dumpsys notification | grep -A 50 "Allowed listeners"
// Desynced state: a component appears in "Enabled" but NOT in "Allowed"
// This is the smoking gun for CVE-2026-0034 exploitation.
Remediation
Apply the March 2026 Android Security Bulletin patches for frameworks/base immediately. OEM patch timelines vary; verify your build's SPL (Security Patch Level) is 2026-03-01 or later via Settings → About phone → Android security update.
Audit notification listener grants on managed/enterprise devices. MDM solutions should enumerate NotificationListenerService bindings via NotificationManager.getEnabledNotificationListeners() and flag any package not in the enterprise-approved list.
Defense in depth: Restrict the INotificationManager.setNotificationListenerAccessGranted() Binder endpoint to callers holding MANAGE_NOTIFICATION_LISTENERS — verify your device's SEPolicy enforces this. Some OEM customisations loosen this restriction.
Runtime detection: Instrument system_server via a custom log.tag.ManagedServices=VERBOSE property on canary fleet devices to catch the post-patch warning message, which would indicate active exploitation attempts against pre-patch devices in your environment.