CVE-2026-0036 AI-generated analysis
platform/frameworks/base, libs/WindowManager/Shell split screen (StageCoordinator / shell transitions)
Adds a shell-side check in StageCoordinator.startAnimation. When split is not active, it detects a split-enter transition whose opening tasks belong to only one split stage and starts a follow-up TRANSIT_CLOSE transition that reparents those tasks back to the TaskDisplayArea. Previously such tasks were left to the default transition handler, which showed them outside of split.
Root cause
Per the commit message and the pre-fix code in StageCoordinator.startAnimation: when two tasks are launched immediately one after another and the second is an adjacent launch, both launches are collected into the same transition. The transition request reports the first task as the trigger task, so StageCoordinator is not resolved as the handler for that transition. In startAnimation, a transition that was not pending in mSplitTransitions and arrived while !mMainStage.isActive() simply did 'return false' without inspecting its contents. The transition fell through to the default transition handler, which showed the adjacent task (in a split stage) on its own without split being entered. This left the hierarchy in a broken state that the shell did not correct.
How the fix works
- New file libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitTransitionUtils.java: handleMalformedEnterTransition(TransitionInfo info, Function<RunningTaskInfo, StageTaskListener> stageResolver) iterates info.getChanges(). A change is recorded when its getTaskInfo() is non-null, stageResolver maps it to a StageTaskListener, and its getMode() is TRANSIT_OPEN or TRANSIT_TO_FRONT (isOpeningMode). The task goes into leafSplitTasks and its stage into a HashSet parentStages. The transition is deemed malformed when leafSplitTasks is non-empty and parentStages.size() <= 1. In that case the method logs 'Malformed enter transition=%d detected' and returns a WindowContainerTransaction that calls wct.reparent(task.token, null /* parent */, true /* onTop */) for each leaf split task. Otherwise it returns null.
- StageCoordinator.java:startAnimation: in the non-pending-transition branch, a new ProtoLog 'startAnimation: transition=%d isSplitActive=%b' is added. The former one-liner 'if (!mMainStage.isActive()) return false;' becomes a block that calls SplitTransitionUtils.handleMalformedEnterTransition(info, taskInfo -> getStageOfTask(taskInfo)). If a WCT is returned, it calls mTransitions.startTransition(TRANSIT_CLOSE, wct, null). The method still returns false, so the original transition is still animated by another handler; the correction happens in a separate follow-up transition.
- StageCoordinator.java: adds a 'private final Transitions mTransitions' field, assigned in both constructors ('mTransitions = transitions; mTransitions.addHandler(this);'), so startAnimation can start the corrective transition. Also adds the TRANSIT_CLOSE static import and the SplitTransitionUtils import.
- StageTaskListener.java: class visibility changed from package-private to public so SplitTransitionUtils in package com.android.wm.shell.common.split can reference it.
- New test SplitTransitionUtilsTests.java. Two changes resolving to two stages but with no opening mode set return null. One opening task in one stage returns a non-null WCT. Tasks resolving to no stage return null. A non-opening mode returns null. A change without task info returns null.
Completeness
- The check runs only on the StageCoordinator.startAnimation path, for transitions not pending in mSplitTransitions, and only when split is not active (!mMainStage.isActive() on 14/15, !isSplitActive() on 16). Transitions arriving while split is already active go through the existing house-keeping code and are not checked by the new helper.
- Only changes with mode TRANSIT_OPEN or TRANSIT_TO_FRONT are considered (on 14/15/17 via the local helper). Other modes for tasks inside a stage are ignored by handleMalformedEnterTransition.
- The fix corrects the state after the fact rather than preventing it. startAnimation still returns false, so the malformed transition is animated by another handler, and the reparent happens in a subsequently started TRANSIT_CLOSE transition. The behaviour described in the commit message (the trigger task reported as the first launch when two launches merge into one transition) is not changed in this commit, which only touches Shell files.
- The malformed condition is parentStages.size() <= 1 with at least one leaf split task. A transition whose opening tasks all land in the same single stage is therefore also treated as malformed, and its tasks are restored to the TDA.
Behavior changes
- When StageCoordinator receives a non-pending transition while split is inactive, and that transition contains opening tasks resolving to at most one split stage, it now starts a new TRANSIT_CLOSE transition that reparents those tasks to the TaskDisplayArea on top. Previously nothing was done.
- Two new WM_SHELL_SPLIT_SCREEN ProtoLog debug lines: 'startAnimation: transition=%d isSplitActive=%b' and 'Malformed enter transition=%d detected'.
- StageTaskListener becomes a public class (14/15/17 diffs); StageCoordinator keeps a reference to Transitions in a new mTransitions field.
- On the Android 16 variant, the previous 'startAnimation: transition=%d' ProtoLog was reached only when split was active. It is removed in favour of the new combined log, which is emitted for every non-pending transition.
Per major
- Android 14: Same fix: commit 3ef64c4444d5 in android-security-14.0.0_r28 (cherry-pick of 3be8f7ff61ba). Adds the mTransitions field, makes StageTaskListener public, uses a local isOpeningMode helper, and guards the check with !mMainStage.isActive(). 8/8 patched lines present at android-security-14.0.0_r29.
- Android 15: Same fix (fa1c418fd8aa in android-security-15.0.0_r16). SplitTransitionUtils.java and the StageTaskListener change are identical. StageCoordinator hunks are adapted to different surrounding fields and constructors; the guard is !mMainStage.isActive(). 8/8 lines present at android-security-15.0.0_r17.
- Android 16: Adapted fix (04814cb8a86e in android-security-16.0.0_r7). SplitTransitionUtils uses com.android.wm.shell.shared.TransitionUtil.isOpeningMode and com.android.internal.protolog.ProtoLog instead of a local helper. The StageCoordinator diff does not add mTransitions or the TRANSIT_CLOSE import (they appear to already exist), and the guard is !isSplitActive(). The diff does not change StageTaskListener. 8/8 lines present at android-security-16.0.0_r8.
- Android 17: Resolved via base commit f60c7771358c in android-17.0.0_r1; 7/8 patched lines present at android-security-17.0.0_r1. The hunks shown are textually identical to the Android 14 commit (same line offsets, mMainStage.isActive() guard). The StageCoordinator blob indices differ from 14, while the StageTaskListener blob indices match.
Unknowns
- What information is disclosed and to whom. The bulletin rates it High/ID, but the material does not describe the attacker-visible effect beyond the adjacent task being shown outside split.
- Whether the behaviour that merges the two launches into one transition and misreports the trigger task was fixed separately.
- Why the Android 17 hunks shown are textually identical to the Android 14 commit despite different StageCoordinator blob indices, and which line is missing in the 7/8 count at android-security-17.0.0_r1.
- Whether the new reparent-to-TDA correction can fire for legitimate single-stage opening transitions when split is inactive. The test suite only covers synthetic TransitionInfo objects.
- Whether TransitionUtil.isOpeningMode on 16 covers exactly the same modes as the local helper (TRANSIT_OPEN, TRANSIT_TO_FRONT).