CVE-2025-26420 AI-generated analysis
platform/packages/modules/Permission, PermissionController runtime permission grant UI (GrantPermissionsActivity)
GrantPermissionsActivity now merges an older, still-showing permission dialog from the same package and task into a newly launched one, and caps simultaneous dialogs per package/task at 10, instead of letting multiple independent dialogs coexist.
Root cause
In GrantPermissionsActivity.onCreate, the sCurrentGrantRequests map (keyed by Pair<package, taskId>) handled only three cases: no current activity (register this one), a new system-triggered request (delegate and finish), or an existing system-triggered dialog (merge into it). When a normal app request arrived while another normal request from the same package in the same task was already registered, none of these branches applied. The new activity went on independently with its own view model and UI, and there was no limit on how many such dialogs could stack in one task. showNextRequest and the photo picker, settings and Health Connect launches went to the top follower activity ('top') instead of the current activity.
How the fix works
- GrantPermissionsActivity.java:onCreate: GrantPermissionsViewModelFactory/mViewModel creation moves before the synchronized (sCurrentGrantRequests) block, so the new activity has a view model when it absorbs an older dialog. The later creation inside `if (!mDelegated)` is removed and only the observe() call remains.
- GrantPermissionsActivity.java:onCreate: a new else branch handles a current dialog that is not system-triggered. If current.mFollowerActivities.size() + 2 > MAX_DIALOGS_PER_PKG_TASK (10), the new activity calls finishAfterTransition() and returns. Otherwise it calls onNewFollowerActivity(current, current.mRequestedPermissions, true) to merge the older dialog into itself, then replaces the map entry with `this`.
- GrantPermissionsActivity.java:onNewFollowerActivity: adds a boolean parameter followerIsOlder. When it is true: the older follower is marked mDelegated = true; the pre-merge shown group and the current RequestInfos are read from the older activity (its mRootView visibility and its mViewModel LiveData) instead of from `this`; if the older dialog was visible, the new activity takes over its request list (mRequestInfos = currentGroups; showNextRequest()) and sets the older activity's mRootView to GONE; the older activity's own followers are merged recursively into the new activity and its follower list is cleared. Existing callers pass false.
- GrantPermissionsActivity.java:showNextRequest: returns early if mDelegated is set. The `top` follower selection is removed, and sendDirectlyToSettings, openPhotoPicker and handleHealthConnectPermissions are now called with `this`.
- GrantPermissionsActivity.java, GRANTED_USER_SELECTED photo picker branch (near line 678; the material does not name the enclosing method): mViewModel.openPhotoPicker is called with `this` instead of the computed `top` activity.
- GrantPermissionsActivity.java:setResultIfNeeded: mResultCode is assigned before removeActivityFromMap(). If mRequestedPermissions changed in the meantime (a merge came in), mResultCode is reset to Integer.MAX_VALUE and false is returned.
- GrantPermissionsActivity.java:onCreate: when icicle is null, window.setDimAmount(0f) now runs only if mRootView is null or not VISIBLE, so a dialog that is already showing merged content is not un-dimmed.
Completeness
- Covers the previously unhandled case of two or more normal (non-system-triggered) requests from the same package in the same task: older dialogs merge upward into the newest one, and more than 10 simultaneous dialogs per package/task are refused.
- The system-triggered branches (a new system request delegates and finishes; a normal request merges into an existing system-triggered dialog) are unchanged apart from the new `false` argument. The 10-dialog cap does not apply on those paths.
- The merge key is still Pair<package, taskId>, and the finishSystemStartedDialogsOnOtherTasksLocked() call is unchanged. Requests from the same package in different tasks are not merged by this change.
- In the GRANTED_USER_SELECTED photo picker branch, the local `Activity top = ...` is still computed but no longer used (dead code).
- The per-major facts report 7/8 lines of the patched region present at android-security-14.0.0_r29. The material does not show which line differs.
Behavior changes
- A new permission request from an app that already has a non-system permission dialog open in the same task now takes over that dialog: the older activity is hidden (root view GONE) and marked delegated, and the newer activity shows its pending request groups.
- When the follower count reaches the limit (current.mFollowerActivities.size() + 2 > 10), a new dialog for the same package and task is finished at once via finishAfterTransition() and is not shown.
- Settings redirect, photo picker and Health Connect flows are launched from `this` (the newest, owning activity) instead of the top follower.
- Screen dim is no longer reset to 0 when the root view is already visible after a merge.
Per major
- Android 14: same fix: cherry-pick with merge conflicts resolved (RESTRICT AUTOMERGE), commit 6bed47f63ec0 in android-security-14.0.0_r20, Change-Id I25f72c30c22429472bfc75073431f84431debd7b. Patched region largely present (7/8 lines) at android-security-14.0.0_r29.
- Android 15: not affected per the bulletin (not listed). Patched region largely present (7/8 lines) at android-security-15.0.0_r17.
- Android 16: not affected per the bulletin (not listed). Patched region largely present (7/8 lines) at android-security-16.0.0_r8.
- Android 17: not affected per the bulletin (not listed). Patched region largely present (7/8 lines) at android-security-17.0.0_r1.
Unknowns
- The concrete security impact of stacked independent dialogs (how the EoP shows up) is not in the material.
- Why setResultIfNeeded now assigns mResultCode before removeActivityFromMap() is not explained in the material.
- Which single line of the 8-line patched region differs at the latest tags, and whether that difference matters.
- Whether Android 13 (listed as affected by the bulletin) received an equivalent change; no Android 13 commit is in the material.
- The original upstream commits (6edd0982..., 61447324..., 87809935...) and whether they changed tests (PermissionTest23#testAppCanOnlyShowOneDialog) beyond this file.