CVE-2025-48537 AI-generated analysis
platform/frameworks/base, NotificationManagerService / Zen mode (ZenModeConditions, ConditionProviders)
ZenModeConditions.evaluateRule now maps condition ids to built-in system ConditionProviderServices only for system-owned or manual rules, rejects non-system rules whose component is in the "android" package, and tracks subscriptions by (Uri, ComponentName) pairs instead of by Uri alone.
Root cause
In services/core/java/com/android/server/notification/ZenModeConditions.java, evaluateRule looped over mConditionProviders.getSystemProviders() for every rule. When sp.isValidConditionId(id) matched, it called ensureRecordExists for the system CPS and overwrote rule.component with the system component, no matter which package owned the rule. Nothing stopped a non-system rule from carrying a component in the SYSTEM_AUTHORITY ("android") package. In addition, mSubscriptions was an ArrayMap<Uri, ComponentName> and the `current` set in evaluateConfig was an ArraySet<Uri>. Subscription bookkeeping (subscribeIfNecessary / unsubscribeIfNecessary) was therefore keyed by Uri only, which mixed up rules that share a conditionId but belong to different CPSes.
How the fix works
- ZenModeConditions.java:evaluateRule: computes isSystemRule = isManual || ZenModeConfig.SYSTEM_AUTHORITY.equals(rule.getPkg()). If the rule is not a system rule but rule.component.getPackageName() equals SYSTEM_AUTHORITY, it logs Slog.w and returns before recording or subscribing the rule.
- ZenModeConditions.java:evaluateRule: the loop over getSystemProviders() that calls ensureRecordExists and rewrites rule.component to the system CPS now runs only when isSystemRule is true. Non-system rules fall through to the existing findConditionProvider(rule.component) path. On Android 14 that path still sets rule.enabled = false and returns when the CPS is not alive.
- ZenModeConditions.java:evaluateRule / evaluateConfig: `current` becomes ArraySet<Pair<Uri, ComponentName>>. evaluateRule adds new Pair<>(id, rule.component) to it and to mSubscriptions, which is now ArraySet<Pair<Uri, ComponentName>>. The cleanup loop in evaluateConfig compares whole pairs and calls unsubscribeIfNecessary(subscription.second, subscription.first), so unsubscribe decisions are made per (Uri, CPS) rather than per Uri.
- ZenModeConditions.java (Android 14 only): evaluateRule gains the isManual parameter. evaluateConfig passes true for config.manualRule and false for automatic rules. On 15/16 this parameter already existed.
- ConditionProviders.java: adds a @VisibleForTesting getRecord(Uri, ComponentName) wrapper around getRecordLocked(id, component, false) and makes ConditionRecord package-private. This is test plumbing.
- Tests: a new ZenModeConditionsTest covers three cases: a system rule with a system CPS is evaluated; a package rule with a system CPS is ignored; a package rule with its own CPS but a system-looking condition id binds to the package CPS, not the system CPS. ZenModeHelperTest.testRulesWithSameUri is reworked to use a registered package CPS instead of the schedule provider.
Completeness
- Covers both issues named in the commit message, within ZenModeConditions: binding to a system CPS is now gated on rule ownership, and subscription tracking is keyed by (Uri, ComponentName).
- The new ownership check is enforced in evaluateRule. The diff does not touch the rule creation or update paths.
- Rules with configurationActivity != null still return early, before any of the new checks (this early return is unchanged).
- ConditionProviders.notifyConditions / getRecordLocked already key records by (id, component), per the code context, and are not changed.
- Rules owned by the system package (rule.getPkg() == SYSTEM_AUTHORITY) and the manual rule keep the previous behaviour, including rewriting the component to the matching system CPS.
- The commit does not touch other ZenModeConditions callbacks or ZenModeHelper. Whether those match rules by condition Uri alone is not shown in the material.
Behavior changes
- A non-system automatic rule whose component is in package "android" is no longer evaluated or subscribed. A Slog.w line ("Rule ... belongs to package ... but has component=... which is not allowed!") is logged instead.
- A non-system rule whose conditionId looks like a built-in system condition (schedule, event, etc.) is no longer redirected to the system CPS. It is resolved through its own rule.component. If that CPS is not alive, the existing not-alive path handles it (rule.enabled = false on Android 14).
- Unsubscribe now works per (Uri, ComponentName): a rule from another CPS that shares the same Uri no longer keeps alive or removes a different CPS's subscription.
- The @VisibleForTesting field mSubscriptions changes type from ArrayMap<Uri, ComponentName> to ArraySet<Pair<Uri, ComponentName>>.
Per major
- Android 14: adapted fix: commit 49078c4bfdce in android-security-14.0.0_r22. Same logic, but it also adds the isManual parameter to evaluateRule and updates both evaluateConfig call sites. 7/8 lines of the patched region are present at android-security-14.0.0_r29.
- Android 15: same fix: commit 63aab59ce138 in android-security-15.0.0_r10. isManual already existed; only the type of the `current` parameter changes. 7/8 lines present at android-security-15.0.0_r17.
- Android 16: same fix: commit 57451c039840 in android-security-16.0.0_r1. 7/8 lines present at android-security-16.0.0_r8.
- Android 17: not listed by the bulletin. 7/8 lines of the patched region are present at android-security-17.0.0_r1, which suggests the fix is already in the base code.
Unknowns
- What information is disclosed and through which path (the bulletin only classifies it as ID / High).
- Which line of the 8-line patched region differs in the latest security tags (7/8 reported), and whether that difference matters functionally.
- Whether the rule creation or update paths validate rule.component or conditionId; the diff does not touch them.
- Whether other ZenModeConditions / ZenModeHelper paths (e.g. condition-changed callbacks) match rules by Uri alone; not shown in the material.