CVE-2025-48601 AI-generated analysis
platform/frameworks/base, services/core notification (NotificationManagerService, PreferencesHelper)
Stops NotificationManagerService/PreferencesHelper from lazily creating (and letting callers mutate) PackagePreferences for packages that do not exist, by making getOrCreatePackagePreferencesLocked reject Process.INVALID_UID, making pure getters non-creating, and early-exiting channel lookups on an invalid uid.
Root cause
PreferencesHelper.getOrCreatePackagePreferencesLocked(pkg, uid) lazily created and stored a PackagePreferences entry for any pkg/uid pair. It only special-cased its own sentinel UNKNOWN_UID (UserHandle.USER_NULL), while NotificationManagerService used a different sentinel INVALID_UID (-1), which per the commit message is what NMS passes along from getUidForPackageAndUser for a missing package. Per the commit message, most API paths were protected by other checks (e.g. canNotifyAsPackage), but the "fromPrivilegedListener" methods (tests cover getNotificationChannelsFromPrivilegedListener, getNotificationChannelGroupsFromPrivilegedListener, updateNotificationChannelFromPrivilegedListener) had no similar enforcement, so PackagePreferences could be created for a non-existing package. Many read-only getters (canShowBadge, getBubblePreference, isImportanceLocked, ...) also had creation as a side effect, and the RoleObserver passed Pair(pkg, -1) entries to updateDefaultApps when a role holder package was missing.
How the fix works
- PreferencesHelper.java:getOrCreatePackagePreferencesLocked(String, int): now requires non-null pkg and Preconditions.checkArgument(uid != INVALID_UID, "Valid uid required to get settings of %s"), throwing IllegalArgumentException for an invalid uid. Setters such as createNotificationChannelGroup, createNotificationChannel, both updateNotificationChannel overloads and setAppImportanceLocked go through this method; their now-dead 'if (r == null) throw IllegalArgumentException("Invalid package")' checks were removed.
- PreferencesHelper.java: the 8-argument overload is renamed getOrCreatePackagePreferencesSupportingInvalidUidLocked and documented as the only path that may receive INVALID_UID (storing into mRestoredWithoutUids); it is called from the readXml package-parsing path and from the validated 2-arg method.
- PreferencesHelper.java: UNKNOWN_UID (UserHandle.USER_NULL) is removed and every use (uid/delegate parsing in the XML read path, pkgInstalled for restore, createDefaultChannelIfNeededLocked, dump output, the permission migration loop, PackagePreferences.uid default, Delegate.isAllowed) switched to android.os.Process.INVALID_UID, so the helper and NMS agree on the missing-package sentinel.
- PreferencesHelper.java: getters getBubblePreference, getAppLockedFields, canShowBadge, isInInvalidMsgState, hasUserDemotedInvalidMsgApp, hasSentInvalidMsg, hasSentValidMsg, didUserEverDemoteInvalidMsgApp, hasSentValidBubble, isImportanceLocked, the group-blocked check (groupId lookup), getPackagePriority and getPackageVisibility now use getPackagePreferencesLocked (nullable, non-creating) and return the DEFAULT_* value / false when no entry exists.
- PreferencesHelper.java: getNotificationChannel and getConversationNotificationChannel return null and getNotificationChannels returns ParceledListSlice.emptyList() when uid == INVALID_UID, before any getOrCreate call. Unused getNotificationChannelsByConversationId was deleted.
- NotificationManagerService.java: local 'static final int INVALID_UID = -1' replaced by import of android.os.Process.INVALID_UID; getUidForPackageAndUser returns mPackageManager.getPackageUid directly; the two canNotifyAsPackage paths initialise targetUid = INVALID_UID; getUidForPackage returns INVALID_UID.
- NotificationManagerService.java RoleObserver: init() and onRoleHoldersChanged() skip role-holder packages whose uid resolves to INVALID_UID (logging Slog.e) instead of passing Pair(pkg, -1) to mPreferencesHelper.updateDefaultApps; the browser-role trampoline update path compares against INVALID_UID.
- Tests: NotificationManagerServiceTest adds MISSING_PACKAGE cases for the three fromPrivilegedListener methods (channels empty, groups empty, update throws IllegalArgumentException with the new message); PreferencesHelperTest replaces canShowBadge-as-creator with setShowBadge and mocks missing packages via NameNotFoundException; RoleObserverTest adds testInit_withInvalidPackages_invalidIgnored.
Completeness
- The guard is centralised in the 2-arg getOrCreatePackagePreferencesLocked, so every setter in PreferencesHelper that resolves preferences through it is covered, not just the fromPrivilegedListener entry points.
- Only the sentinel INVALID_UID is rejected; no positive validation that the uid belongs to pkg is added in PreferencesHelper, which remains the caller's responsibility.
- The readXml path (getOrCreatePackagePreferencesSupportingInvalidUidLocked) intentionally still accepts INVALID_UID and stores into mRestoredWithoutUids, as the restore-before-install behaviour.
- Since the converted getters no longer create entries, lazy entry creation (including default-channel setup via createDefaultChannelIfNeededLocked) appears to happen only through the remaining getOrCreate callers (setters and getNotificationChannel/getConversationNotificationChannel with a valid uid).
Behavior changes
- updateNotificationChannelFromPrivilegedListener (and other setters reached with a missing package) now throws IllegalArgumentException("Valid uid required to get settings of <pkg>") instead of creating a PackagePreferences entry.
- getNotificationChannelsFromPrivilegedListener / getNotificationChannels return an empty ParceledListSlice and getNotificationChannel / getConversationNotificationChannel return null for an INVALID_UID.
- Read-only getters (canShowBadge, getBubblePreference, isImportanceLocked, getPackagePriority, getPackageVisibility, ...) return defaults without storing a new entry.
- Missing role-holder packages are skipped with a Slog.e in RoleObserver init/onRoleHoldersChanged instead of being added with uid -1.
- The uid sentinel for restored-without-uid entries changes from UserHandle.USER_NULL to Process.INVALID_UID; dump output prints "INVALID_UID" instead of "UNKNOWN_UID".
Per major
- Android 14: same fix: commit 30b11a3523d6 in android-security-14.0.0_r24 (cherry-pick of 236cff6879b2); patched region fully present at android-security-14.0.0_r29.
- Android 15: adapted fix: commit ed9af8192664 in android-security-15.0.0_r12; same logic on code that uses mLock instead of synchronized(mPackagePreferences), the Flags.persistIncompleteRestoreData/creationTime variant of the 9-arg overload, and keeps setAppImportanceLocked public with @VisibleForTesting; patched region fully present at android-security-15.0.0_r17.
- Android 16: adapted fix: commit 93629e575f69 in android-security-16.0.0_r3; additionally adds an INVALID_UID early return in a bundle-type channel getter, wraps the bundle-type reserved-channel path (getOrCreate + addReservedChannelLocked) in synchronized(mLock), handles getNotificationChannels(..., includeBundles), uses @VisibleForTesting(otherwise = NONE), and removes an INVALID_UID early return after getUidForPackageAndUser in an NMS caller; patched region fully present at android-security-16.0.0_r8.
- Android 17: not affected (not listed by the bulletin); patched region present at android-security-17.0.0_r1.
Unknowns
- Concrete security impact of a stray PackagePreferences entry for a non-existing package is not shown; the bulletin only rates it High EoP.
- How previously persisted XML containing the old UNKNOWN_UID value (USER_NULL) is treated after the sentinel change.
- Whether the Android 14 NMS code has an equivalent of the INVALID_UID early return that the Android 16 commit removes is not shown.