CVE-2025-48577 AI-generated analysis
platform/frameworks/base, SystemUI keyguard (KeyguardViewMediator)
KeyguardViewMediator now re-checks the target user id at several points in the biometric-auth and keyguardDone/keyguardGoingAway dismissal chain. If the current user changed in between, it resets the keyguard instead of letting a pending dismissal started before the user switch complete.
Root cause
In packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java, the dismissal pipeline checked the target user only at the entry points: keyguardDone and keyguardDonePending compared targetUserId with KeyguardUpdateMonitor.getCurrentUser(). After that, it tracked the request with a boolean mKeyguardDonePending that held no user, and passed a single shared Runnable, mHideAnimationFinishedRunnable, to startPreHideAnimation. readyForKeyguardDone and tryKeyguardDone also carried no user. Per the commit message, if the user changed while these deferred steps were in flight, the prior keyguard could be dismissed. In addition, mUpdateCallback.onBiometricAuthenticated did not check that the authenticated userId was the current user. The mUiBgExecutor body of mKeyguardGoingAwayRunnable also called ActivityTaskManager.getService().keyguardGoingAway() without re-checking that mGoingAwayRequestedForUserId still matched the current user.
How the fix works
- KeyguardViewMediator.java:mUpdateCallback.onBiometricAuthenticated: a new early return with a warning log when userId != KeyguardUpdateMonitor.getCurrentUser(). Biometric success for a non-current user is no longer reported to DevicePolicyManager.reportSuccessfulBiometricAttempt.
- KeyguardViewMediator.java: the boolean mKeyguardDonePending is replaced by int mKeyguardDonePendingForUser, with the sentinel NO_KEYGUARD_DONE_PENDING (-10000). keyguardDonePending stores targetUserId. readyForKeyguardDone reads the stored user, clears it and calls tryKeyguardDone(user). hideWithAnimation, resetKeyguardDonePendingLocked and dump are updated to use the new field.
- KeyguardViewMediator.java:tryKeyguardDone now takes an int userId. Its completion check uses mKeyguardDonePendingForUser == NO_KEYGUARD_DONE_PENDING. When it starts the pre-hide animation, it passes new OnHideAnimationFinished(userId).
- KeyguardViewMediator.java: the shared mHideAnimationFinishedRunnable is removed and replaced by the inner class OnHideAnimationFinished(int userId). Its run() clears mHideAnimationRunning, then compares the captured mUserId with KeyguardUpdateMonitor.getCurrentUser(). On a mismatch it logs, calls mShadeController.get().instantCollapseShade() and resetStateLocked(), and returns without calling tryKeyguardDone. keyguardDonePending also passes new OnHideAnimationFinished(targetUserId) to startPreHideAnimation.
- KeyguardViewMediator.java:mKeyguardGoingAwayRunnable: inside the mUiBgExecutor lambda, before ActivityTaskManager.getService().keyguardGoingAway(keyguardFlag), it re-reads KeyguardUpdateMonitor.getCurrentUser(). If that differs from mGoingAwayRequestedForUserId, it logs, calls mUpdateMonitor.setKeyguardGoingAway(false) and mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false), and returns.
- KeyguardViewMediator.java: in the branch where mHiding is set to false and mLockPatternUtils.isSecure(currentUserId) leads to doKeyguardLocked(null), the fix adds mShadeController.get().instantCollapseShade() and setShowingLocked(true, true /* force */), forcing the keyguard back to showing.
- KeyguardViewMediatorTest.java: adds four tests: biometricAuthSuccessForCurrentUserDoesProceed, biometricAuthSuccessForDifferentUserDoesNotProceed, testDonePendingExecutesOnHideAnimationFinished and testDonePendingExecutesOnHideAnimationFinishedButTerminatesWhenUserHasChanged.
Completeness
- Covered, all in KeyguardViewMediator.java: the biometric success callback, the keyguardDonePending -> pre-hide animation -> OnHideAnimationFinished -> tryKeyguardDone chain, readyForKeyguardDone, the background keyguardGoingAway call, and the secure-user recovery branch shown in the hunk near line 2964.
- The Android 14 commit does not include the extra hardening in the 15/16 commits. It has no mDismissToken for the 500 ms delayed dismiss() posted after a user switch, and handleBeforeUserSwitching does not remove the HIDE message or that delayed dismiss runnable. The material does not show whether that path matters on 14.
- The user checks in OnHideAnimationFinished and in the goingAway runnable compare against KeyguardUpdateMonitor.getCurrentUser() at run time. They rely on that static value already reflecting the switch when the deferred step runs.
- keyguardDone and keyguardDonePending keep their existing entry checks. tryKeyguardDone itself does not re-check the user; it relies on its callers and on OnHideAnimationFinished.
Behavior changes
- Biometric success for a user other than the current user is ignored (Log.w) and is no longer reported to DevicePolicyManager.reportSuccessfulBiometricAttempt.
- If the user changes between keyguardDonePending and the end of the pre-hide animation, the shade is collapsed and the keyguard state is reset instead of the keyguard being dismissed.
- If the user changes before the background keyguardGoingAway call runs, the call is skipped and the going-away flags are cleared.
- In the modified secure-user branch where mHiding is reset, the shade is collapsed and setShowingLocked(true, force) is applied.
- Several DEBUG-gated logs become unconditional Log.d logs that include user ids. The dumpsys field is renamed to mKeyguardDonePendingForUser.
Per major
- Android 14: Fixed by 2acf4a1801c1 in android-security-14.0.0_r24. This is the analysed commit; it uses KeyguardUpdateMonitor.getCurrentUser(). It lacks the mDismissToken / removeMessages(HIDE) additions of 15/16. At android-security-14.0.0_r29, 7/8 lines of the patched region are present.
- Android 15: Adapted fix 3b6cc0893907 in android-security-15.0.0_r12. Same logic, using mSelectedUserInteractor.getSelectedUserId(). It adds mDismissToken for the delayed dismiss, removes the HIDE message and the token callbacks in handleBeforeUserSwitching, and calls mKeyguardInteractor.showKeyguard() in the recovery branch. At android-security-15.0.0_r17, 8/8 lines are present.
- Android 16: Adapted fix 78760f098fab in android-security-16.0.0_r3. Same as 15, except the recovery branch is gated on !KeyguardWmStateRefactor.isEnabled() and setShowingLocked takes a reason string. The goingAway check comes before the ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS branch. At android-security-16.0.0_r8, 8/8 lines are present.
- Android 17: Not affected: the bulletin does not list Android 17. At android-security-17.0.0_r1, 7/8 lines of the patched region are present.
Unknowns
- Which line of the patched region differs at android-security-14.0.0_r29 and android-security-17.0.0_r1 (the resolver reports 7/8 lines).
- Whether the Android 14 tree has the 500 ms delayed dismiss() post that 15/16 additionally guard with mDismissToken.
- The enclosing method of the recovery branch modified near line 2964 is not shown in the 14 diff hunk (the 16 log string suggests handleStartKeyguardExitAnimation).
- What onBiometricAuthenticated does after reportSuccessfulBiometricAttempt (that part of the method is not shown).