CVE-2025-48584 AI-generated analysis
platform/frameworks/base, NotificationManagerService (services/core/java/com/android/server/notification)
Adds a per-listener cap (5,000) on the conversation channels a privileged NotificationListenerService can create for other packages via createConversationNotificationChannelForPackageFromPrivilegedListener, and adds null and edge-case checks in that method so it no longer hits NPEs in system_server.
Root cause
In NotificationManagerService.createConversationNotificationChannelForPackageFromPrivilegedListener, a privileged listener (one with a CDM association for the target package, or a notification assistant) could create conversation channels for other packages with no per-listener aggregate limit. The method also called .copy() directly on the results of mPreferencesHelper.getNotificationChannel(...) and getConversationNotificationChannel(...) without null checks, and did not check the resolved uid. Per the commit message, these edge conditions could result in NPEs. It also always re-created the conversation channel from the parent, which per the new code comment would override an existing conversation's customizations with the parent's values.
How the fix works
- New class services/core/java/com/android/server/notification/NotificationListenerStats.java: keeps a SparseIntArray mapping nlsUid to the number of channels created, guarded by mLock. MAX_CHANNELS = 5_000, and a constructor allows tests to override it. isAllowedToCreateChannel(ManagedServiceInfo) returns count < max. logCreatedChannels(nls, increase) adds to the count. onPackageRemoved(uid, pkg) drops the entry for that uid. writeXml/readXml persist the counts under the tag 'nlsStats', with one 'nls' child per uid (attributes uid, channelCount). dump() prints the per-uid counts.
- NotificationManagerService.java: new field mNotificationListenerStats, injected through init(...) as a new last parameter and constructed with new NotificationListenerStats() next to publishBinderService. The policy XML parsing loop reads the 'nlsStats' tag via NotificationListenerStats.isXmlTag/readXml. The policy XML writer writes it only when !forBackup. The listener dump section prints the stats. The package-removed handler calls mNotificationListenerStats.onPackageRemoved(uid, pkg).
- NotificationManagerService.java:verifyPrivilegedListener now returns the @NonNull ManagedServiceInfo it validated instead of void.
- NotificationManagerService.java:createConversationNotificationChannelForPackageFromPrivilegedListener: after verifyPrivilegedListener, if the listener is not SystemUI (nlsInfo.isSystemUi()), is not a notification assistant (isNotificationAssistant(nlsInfo.service)), and isAllowedToCreateChannel returns false, the method logs with Slog.e and returns null.
- Same method: returns null if getUidForPackageAndUser yields INVALID_UID. Returns null if the parent channel lookup is null. If getConversationNotificationChannel already returns a channel, returns that existing channel. Otherwise it copies the parent, sets the id and conversation id, calls createNotificationChannelsImpl, then re-reads the conversation channel. Only if that result is non-null does it call logCreatedChannels(nlsInfo, 1) and handleSavePolicyFile(). The result is returned without .copy().
- Renames isServiceTokenValid to isNotificationAssistant (used in isInteractionVisibleToListener and in the channel-changed notification path). The body is unchanged, so this is a pure rename.
- Tests: NotificationManagerServiceTest uses the real PreferencesHelper with a test cap of 10 (MAX_CHANNELS_CREATED_BY_NLS_FOR_TESTING) and adds the tests tooManyChannels_ignored, noParentChannel_ignored and existingChannel_returnsSame. RoleObserverTest is updated for the new init() parameter.
Completeness
- Within the diff, the cap is checked and counted only in createConversationNotificationChannelForPackageFromPrivilegedListener. No other channel-creation path is changed to use NotificationListenerStats.
- The counter only goes up: it is never decremented when conversation channels are later deleted. An entry is cleared only when a package whose uid matches the NLS uid is removed (onPackageRemoved deletes by uid). Removing a target package does not reduce the listener's count.
- SystemUI listeners and notification assistants are exempt from the cap, so the cap applies to other privileged (for example CDM-associated) listeners.
- Counts are persisted in the notification policy XML but not in backups, so they survive a reboot. readXml defaults missing attributes to 0.
- The edge cases handled are an unknown package uid, a missing parent channel, and a null result after creation. An existing conversation channel is now returned instead of being re-created.
- The cap is keyed on nls.uid, so multiple listener components in the same uid share one budget.
Behavior changes
- Once a non-SystemUI, non-assistant listener has created 5,000 channels in total, createConversationNotificationChannelForPackageFromPrivilegedListener returns null and logs with Slog.e instead of creating the channel.
- The method returns null when the package uid is INVALID_UID, when the parent channel does not exist, or when creation did not produce a channel.
- Requesting an existing conversation channel again returns the stored channel unchanged instead of re-creating it from the parent.
- The returned channel is no longer a copy (the test asserts isSameInstanceAs on a repeated call).
- handleSavePolicyFile is called only when a channel was actually created.
- New 'nlsStats' section in the persisted notification policy XML and in dumpsys output.
- NotificationManagerService.init() gains a NotificationListenerStats parameter.
Per major
- Android 14: not affected per bulletin. The resolver reports 7/8 patched-region lines present at android-security-14.0.0_r29. The material does not show whether the vulnerable method exists on 14.
- Android 15: not affected per bulletin. The resolver reports 8/8 patched-region lines present at android-security-15.0.0_r17. The material does not show whether the vulnerable method exists on 15.
- Android 16: same fix: commit 08a0766708db in android-security-16.0.0_r3 (cherry-pick of 16b6836078042fc1b2288912e9656d01d9d20af5, Change-Id I21033ce09185992adc0a377c701a442e8e81a9be). The patched region is fully present at android-security-16.0.0_r8.
- Android 17: not affected per bulletin (not listed). The resolver reports 8/8 patched-region lines present at android-security-17.0.0_r1.
Unknowns
- Whether the reported DoS was the unbounded channel creation, the NPE paths, or both. The commit message covers both.
- How getUidForPackageAndUser behaves (returns INVALID_UID or throws) is not shown in the material.
- Whether other privileged-listener channel APIs have similar null-handling issues or lack a cap is not shown.
- The scope of PreferencesHelper#NOTIFICATION_CHANNEL_COUNT_LIMIT is not shown.
- Whether the vulnerable method exists on Android 14, 15 and 17 is not shown.