CVE-2025-22420 AI-generated analysis
platform/frameworks/base, NotificationManagerService / PreferencesHelper (notification channels)
Adds a Uri read-permission check (UriGrantsManagerInternal.checkGrantUriPermission) on a content:// sound Uri when a new notification channel is created in PreferencesHelper.createNotificationChannel, and moves the check into a new PermissionHelper.grantUriPermission helper. On Android 14 a first commit made channel creation throw SecurityException; a second commit falls back to the default notification sound instead.
Root cause
PreferencesHelper.createNotificationChannel accepted and stored any sound Uri supplied for a new channel without checking that the uid could read or grant that Uri. Per the commit message, Uri permissions for the channel sound were only checked later, by NotificationRecord (visitGrantableUri) when a notification was posted. For privileged listener updates a check also existed in NotificationManagerService. So the stored channel sound Uri was not validated when it entered the channel preferences.
How the fix works
- PermissionHelper.java: new static grantUriPermission(UriGrantsManagerInternal ugmInternal, Uri uri, int sourceUid). It returns immediately when uri is null or its scheme is not ContentResolver.SCHEME_CONTENT. Otherwise, under Binder.withCleanCallingIdentity, it calls ugmInternal.checkGrantUriPermission(sourceUid, null, ContentProvider.getUriWithoutUserId(uri), Intent.FLAG_GRANT_READ_URI_PERMISSION, ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))), which throws SecurityException if sourceUid cannot grant read access.
- PreferencesHelper.java: the constructor gains a UriGrantsManagerInternal ugmInternal parameter, stored in mUgmInternal. In createNotificationChannel, in the new-channel branch right after clearLockedFieldsLocked(channel), the fix calls PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid). Commit 04a5d5dd0910 (android-security-14.0.0_r19) lets the SecurityException propagate, so the channel is not created (testCreateChannel_noSoundUriPermission_contentSchemeVerified asserts the channel is null). Commit ccc01c147ead (android-security-14.0.0_r22) wraps the call in try/catch(SecurityException); on failure it calls channel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, Notification.AUDIO_ATTRIBUTES_DEFAULT), so the channel is created with the default sound (testCreateChannel_noSoundUriPermission_replaceWithDefaultSound).
- NotificationManagerService.java: passes mUgmInternal into the PreferencesHelper constructor. In the privileged-listener channel update path (around line 5876), the inline Binder.withCleanCallingIdentity/checkGrantUriPermission block on the new soundUri is replaced by PermissionHelper.grantUriPermission(mUgmInternal, soundUri, sourceUid). The check still runs only when soundUri is non-null and differs from the original channel's sound (test ..._oldSoundNoUriPerm_newSoundHasUriPerm).
- NotificationRecord.java visitGrantableUri: replaces the inline clearCallingIdentity/checkGrantUriPermission with PermissionHelper.grantUriPermission(mUgmInternal, uri, sourceUid). On Android 14 the fix also adds an early return when mGrantableUris already contains the uri. Commit 04a5d5dd0910 dropped the uri == null early return; ccc01c147ead restores 'if (uri == null) return;'. The SYSTEM_UID exemption is kept on Android 14. The existing catch(SecurityException) handling (replace the sound with DEFAULT_NOTIFICATION_URI, or log/throw depending on target SDK) is unchanged.
- Tests: PreferencesHelperTest gains content, file and android.resource scheme cases, and all constructor call sites are updated. NotificationManagerServiceTest gains the oldSoundNoUriPerm_newSoundHasUriPerm case. NotificationRecordTest (first commit only) switches to a UriGrantsManagerInternal mock provided through LocalServices.
Completeness
- The new check is applied only in the new-channel branch of PreferencesHelper.createNotificationChannel. The existing-channel update branch of the same function and PreferencesHelper.updateNotificationChannel are not touched by the diff. The code comment says regular apps can only set the sound before creating a channel, citing NotificationChannel#setSound, which is not in the material.
- Only content:// Uris are checked. file:// and android.resource:// sounds are accepted without a check (the tests explicitly call these 'ignored').
- The check in createNotificationChannel passes the `uid` variable to the helper. Whether this differs from the caller's uid in delegated creation is not shown.
- The diff does not touch the readXml or restore paths, so channel sounds loaded from persisted XML are not re-verified by this fix.
- In the privileged listener update path, the check still runs only when the new sound differs from the original channel sound.
- NotificationRecord.visitGrantableUri on Android 14 still exempts Process.SYSTEM_UID. The Android 15/16 versions of the same function have no such exemption and already had the mGrantableUris dedupe before this fix.
Behavior changes
- Android 14 r19 (04a5d5dd0910): createNotificationChannel throws SecurityException to the caller when a content:// sound Uri cannot be read or granted by the checked uid, and the channel is not created.
- Android 14 r22 and Android 15/16 (ccc01c147ead, 6c7c10e6234b, fb8f76eca907): instead of throwing, the channel is created with Settings.System.DEFAULT_NOTIFICATION_URI and Notification.AUDIO_ATTRIBUTES_DEFAULT. The requested sound and audio attributes are silently replaced.
- Privileged listener channel updates with a non-content sound Uri no longer call checkGrantUriPermission (the helper returns early). Before the fix the call was made for any scheme.
- NotificationRecord.visitGrantableUri now adds non-content Uris to mGrantableUris (the helper returns without throwing for them). Before the fix such Uris returned early and were never added.
- The PreferencesHelper constructor signature changes (new UriGrantsManagerInternal parameter).
Per major
- Android 14: Two commits linked by bug 337775777. 04a5d5dd0910 in android-security-14.0.0_r19 throws SecurityException on creation. ccc01c147ead in android-security-14.0.0_r22 does the same refactor but falls back to the default sound and restores the uri == null check. The r29 code shown has the ccc01c147ead form of NotificationRecord.visitGrantableUri. The per-major facts report 8/8 lines of the patched region present at android-security-14.0.0_r29.
- Android 15: Adapted fix 6c7c10e6234b in android-security-15.0.0_r10 (fallback-to-default variant). The PreferencesHelper constructor already has PermissionManager, UserProfiles and Clock parameters, and ugmInternal is inserted before showReviewPermissionsNotification. NotificationRecord.visitGrantableUri already had the mGrantableUris dedupe and no SYSTEM_UID exemption. 8/8 lines present at android-security-15.0.0_r17.
- Android 16: Adapted fix fb8f76eca907 in android-security-16.0.0_r3. Same as 15, plus the TestPreferencesHelper subclass constructor in PreferencesHelperTest is updated. 8/8 lines present at android-security-16.0.0_r8.
- Android 17: Not affected (not listed by the bulletin). For information: 6/8 lines of the patched region are present at android-security-17.0.0_r1.
Unknowns
- The material does not show which consumer of the stored channel sound Uri made the missing check an EoP. Only the commit message statement that checks previously happened at post time is available.
- Whether the r19 throwing commit was reverted on Android 14 before r22 or simply overwritten is not shown; both diffs apply against the same base.
- Which variant the 8/8 line match at android-security-14.0.0_r29 corresponds to for PreferencesHelper.createNotificationChannel is not shown.
- The createNotificationChannel signature, and whether `uid` is the owning package uid or the caller uid, is not shown.
- Whether mUgmInternal is initialised before PreferencesHelper is constructed in NotificationManagerService is not shown.
- Why Android 17 is not affected, and what the 2 of 8 lines that differ at android-security-17.0.0_r1 are, is not shown.
- Whether NotificationChannel#setSound actually prevents apps from changing the sound of an existing channel (the stated reason for checking only new channels) is not in the material.