CVE-2025-48533 AI-generated analysis
platform/frameworks/base, services/core WindowManager (ActivityRecord, StartingData, Transition, WindowState)
When the keyguard is locked and the activity does not request showWhenLocked, WindowManager now waits for the transition to finish before it removes the activity's starting (splash) window. Until then it reports the activity's other windows as not visible, so app content is not shown on a locked device during the unocclude animation.
Root cause
The starting window (splash screen) may carry FLAG_SHOW_WHEN_LOCKED because it holds no sensitive content. A WindowState method copies FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD | FLAG_ALLOW_LOCK_WHILE_SCREEN_ON from the app window into mActivityRecord.mStartingWindow.mAttrs. It just overwrote those flags: it did not re-check keyguard state (checkKeyguardFlagsChanged) and did not delay removal of the starting window. ActivityRecord.removeStartingWindow() had no keyguard-based deferral, and WindowState visibility depended only on atoken.isVisible() and animation state. So while the device was still locked, the app window of an activity that does not request showWhenLocked could briefly become visible, including through alpha blending during the unocclude animation. On Android 14, StartingData.mRemoveAfterTransaction was also only a boolean, so there was no state meaning 'remove after the transition finishes'.
How the fix works
- StartingData.java: mRemoveAfterTransaction becomes an @AfterTransaction int. Android 14 adds all four states (AFTER_TRANSACTION_IDLE=0, AFTER_TRANSACTION_REMOVE_DIRECTLY=1, AFTER_TRANSACTION_COPY_TO_CLIENT=2, AFTER_TRANSITION_FINISH=3). Android 15 only adds AFTER_TRANSITION_FINISH to the IntDef it already had.
- WindowState.java, the method that copies keyguard flags into mActivityRecord.mStartingWindow.mAttrs (around line 2893): it computes wasShowWhenLocked (the starting window had FLAG_SHOW_WHEN_LOCKED) and removeShowWhenLocked (the app window does not have it). When both are true it calls mActivityRecord.checkKeyguardFlagsChanged() to trigger the unocclude animation, then mActivityRecord.deferStartingWindowRemovalForKeyguardUnoccluding().
- New ActivityRecord.deferStartingWindowRemovalForKeyguardUnoccluding(): sets mStartingData.mRemoveAfterTransaction = AFTER_TRANSITION_FINISH when all of these hold: mStartingData != null, the state is not already AFTER_TRANSITION_FINISH, isKeyguardLocked(), !canShowWhenLockedInner(this), !isVisibleRequested(), and mTransitionController.inTransition(this).
- ActivityRecord.removeStartingWindow(): returns early, keeping the splash screen, while mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH.
- WindowState.java visibility check (around line 1931, the branch that evaluates atoken.isVisible() / isAnimationRunningSelfOrParent()): returns false for every window of the activity that is not TYPE_APPLICATION_STARTING while atoken.mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH. This stops the app window from showing through alpha blending during the unocclude animation.
- Transition.java, loop over ActivityRecords next to the enterAutoPip handling (the enclosing method is not named in the hunk): when ar.mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH and (!ar.isVisible() || !ar.mTransitionController.inTransition(ar)), it resets the state to AFTER_TRANSACTION_IDLE and calls ar.removeStartingWindow(). This performs the deferred removal.
- ActivityRecord starting-window transfer path (where tStartingWindow.mToken = this is set): resets AFTER_TRANSITION_FINISH to AFTER_TRANSACTION_IDLE when the starting window is handed to another activity.
- ActivityRecord.canShowWhenLocked(ActivityRecord) is refactored to use a new static helper, canShowWhenLockedInner(r) (= !inPinnedWindowingMode && (mShowWhenLocked || containsShowWhenLockedWindow())). The new deferral check reuses this helper.
- Android 14 only (state machine bundled into the backport): (1) the splash-screen transfer method (the one that calls requestCopySplashScreen()) now sets AFTER_TRANSACTION_COPY_TO_CLIENT and returns true while mStartingData.mWaitForSyncTransactionCommit is set; (2) the method that clears mWaitForSyncTransactionCommit now dispatches on the state (REMOVE_DIRECTLY -> removeStartingWindowAnimation(mPrepareRemoveAnimation), COPY_TO_CLIENT -> removeStartingWindow()); (3) the removal path that used to set mRemoveAfterTransaction = true now sets AFTER_TRANSACTION_REMOVE_DIRECTLY.
- Android 16: the logic was already present behind the aconfig flag keep_app_window_hide_while_locked. The security commit removes the flag from windowing_frontend.aconfig and from the WindowState condition, so the behaviour becomes unconditional. It also adds the mStartingData != null guard in deferStartingWindowRemovalForKeyguardUnoccluding().
Completeness
- The deferral is armed from only one place: the WindowState path where the app window's keyguard flags are merged into the starting window attrs. It is armed only when the starting window had FLAG_SHOW_WHEN_LOCKED and the app window does not.
- deferStartingWindowRemovalForKeyguardUnoccluding() requires mTransitionController.inTransition(this); if that is false, no deferral happens. The material shows the deferral released only in Transition.java and in the starting-window transfer path.
- The WindowState visibility override applies to every non-TYPE_APPLICATION_STARTING window of the activity while the state is AFTER_TRANSITION_FINISH. It is keyed on that window's own ActivityRecord.
- The state is cleared when the starting window is transferred to another activity and in the Transition.java loop. The material shows no other clearing path, for example for an aborted transition.
- Android 14 also gets the AFTER_TRANSACTION_COPY_TO_CLIENT deferral, which holds the splash-screen copy until the sync transaction commits. Android 15 already had this.
Behavior changes
- When the keyguard is locked and a launching activity does not request showWhenLocked, the splash screen stays until the transition completes instead of being removed directly.
- During that time WindowState reports the activity's non-starting windows as not visible.
- When the app window drops FLAG_SHOW_WHEN_LOCKED relative to the starting window, ActivityRecord.checkKeyguardFlagsChanged() is now called, which may trigger an unocclude animation.
- Android 14: splash-screen copy-to-client is deferred while a sync transaction is pending, and mRemoveAfterTransaction changes type from boolean to int.
- Android 16: the behaviour is now unconditional instead of gated by the keep_app_window_hide_while_locked aconfig flag.
Per major
- Android 14: Fixed by 71773c9f03af in android-security-14.0.0_r23. The change also bundles the boolean-to-int StartingData state machine and the COPY_TO_CLIENT deferral. 8/8 lines of the patched region present at android-security-14.0.0_r29.
- Android 15: Adapted fix 9916e7bbbf44 in android-security-15.0.0_r11. Only AFTER_TRANSITION_FINISH is added to the existing IntDef; the ActivityRecord, Transition and WindowState logic is the same, with the visibility hunk placed before the isStartingWindowAssociatedToTask() check. 8/8 lines present at android-security-15.0.0_r17.
- Android 16: Adapted fix 8874f3eae9a9 in android-security-16.0.0_r1. The logic was already present behind the aconfig flag keep_app_window_hide_while_locked; the commit removes the flag (making it unconditional) and adds the mStartingData null check. 5/8 lines of the patched region present at android-security-16.0.0_r8; the reason for the partial match is not shown.
- Android 17: Not affected (not listed by the bulletin). 3/8 lines of the patched region present at android-security-17.0.0_r1; the material does not explain why.
Unknowns
- The exact names of the two patched WindowState methods, of the ActivityRecord splash-transfer and sync-commit methods, and of the Transition.java method holding the release loop are not shown in the hunk headers.
- Whether an aborted (rather than finished) transition leaves mRemoveAfterTransaction at AFTER_TRANSITION_FINISH is not visible.
- How the fix behaves on devices that do not use shell transitions (legacy app transitions) is not shown.
- The concrete scenario (which launch path over the keyguard) is not described beyond the commit's test note.
- Why Android 16 r8 and 17 r1 only partially match the patched region is not shown.