CVE-2025-48547 AI-generated analysis
platform/packages/modules/Permission, PermissionController (one-time permission session handling in AppPermissionGroup.java and KotlinUtils.kt)
PermissionController now stops a package's one-time permission session on revoke only when no remaining permission of that package is still granted as one-time, and the Kotlin helper that makes this decision is rewritten so its grant and one-time checks evaluate correctly.
Root cause
A package has a single one-time permission session covering all of its one-time grants (commit message). In AppPermissionGroup.java, the else branch of the revoke path called PermissionManager.stopOneTimePermissionSession(packageName) unconditionally. In KotlinUtils.kt, the revoke path gated the stop on anyPermsOfPackageOneTimeGranted(app, newGroup.packageInfo, newGroup), but that helper was flawed in four ways. It skipped every permission belonging to the group passed in. It returned true early when group.isOneTime was true. It masked the permission flags with FLAG_PERMISSION_ONE_TIME before testing FLAG_PERMISSION_REVOKED_COMPAT, so that test always passed. It tested grant state with requestedPermissionsFlags[idx] == PackageManager.PERMISSION_GRANTED instead of testing the REQUESTED_PERMISSION_GRANTED bit. As the commit message states, stopping the session on one revoke terminated the only session running for all of the package's one-time grants.
How the fix works
- AppPermissionGroup.java (revoke path, else branch around line 1648): the unconditional call to stopOneTimePermissionSession(packageName) becomes `else if (!anyPermsOfPackageOneTimeGranted(getApp()))`, so the session is stopped only when no permission in the package is still granted as one-time.
- AppPermissionGroup.java: adds a new private helper anyPermsOfPackageOneTimeGranted(PackageInfo), annotated @SuppressLint("MissingPermission"). It returns false if requestedPermissions or requestedPermissionsFlags is null. Otherwise it walks requestedPermissions and skips entries whose requestedPermissionsFlags[i] lacks PackageInfo.REQUESTED_PERMISSION_GRANTED. For each remaining entry it reads the full flags via mPackageManager.getPermissionFlags(perm, packageName, getUser()) and returns true if FLAG_PERMISSION_REVOKED_COMPAT is clear and FLAG_PERMISSION_ONE_TIME is set.
- KotlinUtils.kt (revoke path around line 1042): the call becomes anyPermsOfPackageOneTimeGranted(app, newGroup.packageInfo). Dropping the group argument means the scan no longer excludes the permissions of the group being revoked.
- KotlinUtils.kt:anyPermsOfPackageOneTimeGranted: removes the optional LightAppPermGroup parameter and the early `group?.isOneTime == true` return. The user is now always UserHandle.getUserHandleForUid(packageInfo.uid). The grant test now checks the REQUESTED_PERMISSION_GRANTED bit in requestedPermissionsFlags[index]. Flags are no longer pre-masked with FLAG_PERMISSION_ONE_TIME, so the FLAG_PERMISSION_REVOKED_COMPAT test is meaningful. isGrantedOneTime requires REVOKED_COMPAT clear and ONE_TIME set. This is the same grant and one-time test as the new Java helper, although the Java helper also has a null guard and takes the user from getUser().
Completeness
- Both revoke paths in the material that call stopOneTimePermissionSession (the Java AppPermissionGroup model and KotlinUtils.kt) are now gated on a package-wide one-time check.
- The Java check takes the grant bit from the requestedPermissionsFlags snapshot in getApp() and reads flags live. Whether getApp() is refreshed before this point is not shown.
- The fix does not change framework-side session handling (PermissionManager.stopOneTimePermissionSession itself). It changes only when PermissionController decides to call it.
- Any other callers of stopOneTimePermissionSession outside these two sites are not covered by the diff and are not shown in the material.
Behavior changes
- Revoking one one-time permission group no longer stops the package's one-time session while another permission of the package is still granted as one-time.
- The Java path now makes extra PackageManager.getPermissionFlags calls during revoke, one per granted requested permission.
- In the Kotlin path, the package-wide scan now includes the permissions of the group being revoked (previously skipped), and the grant and flag tests now use the REQUESTED_PERMISSION_GRANTED bit and unmasked flags. So the set of situations in which the stop call is issued changes.
Per major
- Android 14: same fix: 122e39ced57a in android-security-14.0.0_r22 (cherry-pick of the internal change). The patched region is fully present (8/8 lines) at android-security-14.0.0_r29.
- Android 15: same fix: 5dca0ccb26f2 in android-security-15.0.0_r10. The Java and helper changes are the same. The Kotlin caller context differs only by the existing ContextCompat.createDeviceContext lines. 8/8 lines present at android-security-15.0.0_r17.
- Android 16: same fix: 78fc913decb3 in android-security-16.0.0_r1, matching the 15 change. 8/8 lines present at android-security-16.0.0_r8.
- Android 17: not affected: the bulletin does not list Android 17. The patched region (8/8 lines) is present at android-security-17.0.0_r1.
Unknowns
- The material does not show the name of the method in AppPermissionGroup.java that encloses the patched else branch.
- The framework-side effect of stopOneTimePermissionSession on the remaining one-time grants is not shown. The material only states that the shared session was terminated.
- The fix ships without a test (Test: manual), so the exact scenario Google validated is unknown.
- Whether getApp() returns a refreshed PackageInfo after the revoke, and how a stale grant bit would affect the Java check, is not shown.
- Whether other callers of stopOneTimePermissionSession exist is not shown.
- Whether an Android 13 commit exists is not shown in the material.