CVE-2024-31326 AI-generated analysis
platform/frameworks/base, services/devicepolicy (DevicePolicyManagerService, Owners, OwnersData)
Adds a one-time, persisted "post upgrade" migration step in DevicePolicyManagerService that pushes screen-capture-disabled, lock task, and legacy user restriction policies held by device/profile owner ActiveAdmins into the DevicePolicyEngine, because the initial Android T to U migration did not carry them over correctly.
Root cause
On upgrade from Android T to Android U, migratePoliciesToDevicePolicyEngine() in DevicePolicyManagerService.java migrated legacy ActiveAdmin state into the DevicePolicyEngine, but three policy groups were not migrated properly: (1) migrateScreenCapturePolicyLocked() returned early when mPolicyCache.getScreenCaptureDisallowedUser() == UserHandle.USER_NULL, skipping the DO / org-owned-PO parent disableScreenCapture flag in that case; (2) lock task packages/features (DevicePolicyData.mLockTaskPackages / mLockTaskFeatures) had no migration; (3) user restrictions stored in ActiveAdmin.ensureUserRestrictions() (own and parent admin) had no migration, so per the second commit message they could not be queried after the OTA (its test notes that DISALLOW_FACTORY_RESET set by a device owner stopped applying). The initial migration is gated by shouldMigrateToDevicePolicyEngine(), which includes !mOwners.isMigratedToPolicyEngine(), so an already-migrated device would not re-run it to repair the gap.
How the fix works
- Commit 736462a777d0, OwnersData.java: adds persisted boolean mPoliciesMigratedPostUpdate serialized as attribute "migratedPostUpgrade" on the existing policy-engine-migration tag (written alongside migratedToPolicyEngine, read in the TAG_POLICY_ENGINE_MIGRATION parser case). Owners.java: adds markPostUpgradeMigration() (sets flag and calls mData.writeDeviceOwner()) and isMigratedPostUpdate(), both under synchronized(mData).
- Commit 736462a777d0, DevicePolicyManagerService.java:systemReady(PHASE_ACTIVITY_MANAGER_READY): after the existing shouldMigrateToDevicePolicyEngine()/migratePoliciesToDevicePolicyEngine() block, calls new maybeMigratePoliciesPostUpgradeToDevicePolicyEngineLocked(), which runs only if mOwners.isMigratedToPolicyEngine() is true and isMigratedPostUpdate() is false, then calls migratePoliciesPostUpgradeToDevicePolicyEngineLocked() and markPostUpgradeMigration().
- Commit 736462a777d0, DevicePolicyManagerService.java:migratePoliciesPostUpgradeToDevicePolicyEngineLocked(): new method that runs migrateScreenCapturePolicyLocked() and new migrateLockTaskPolicyLocked() inside try/catch(Exception), logging and returning false on failure.
- Commit 736462a777d0, DevicePolicyManagerService.java:migrateScreenCapturePolicyLocked(): removes the early return on mPolicyCache.getScreenCaptureDisallowedUser() == USER_NULL so the DO / org-owned PO parent disableScreenCapture flag is always evaluated; the call is removed from migratePoliciesToDevicePolicyEngine() and now lives only in the post-upgrade step.
- Commit 736462a777d0, DevicePolicyManagerService.java:migrateLockTaskPolicyLocked() + setLockTaskPolicyInPolicyEngine(): for the device owner's user and for every user with a profile owner where canDPCManagedUserUseLockTaskLocked(userId), if DevicePolicyData.mLockTaskPackages is non-empty, writes PolicyDefinition.LOCK_TASK with new LockTaskPolicy(new HashSet<>(packages), features) via mDevicePolicyEngine.setLocalPolicy for an EnforcingAdmin.createEnterpriseEnforcingAdmin of that admin.
- Commit 736462a777d0, DevicePolicyManagerService.java: the MANAGE_PROFILE_AND_DEVICE_OWNERS-gated forced migration entry point (around line 24151) now executes inside synchronized(getLockObject()) and runs migratePoliciesToDevicePolicyEngine() followed by migratePoliciesPostUpgradeToDevicePolicyEngineLocked(), returning the AND of both results.
- Commit f1eb8e719dfb, DevicePolicyManagerService.java:setBackwardCompatibleUserRestriction(): refactored to compute an ownerType (OWNER_TYPE_DEVICE_OWNER / OWNER_TYPE_PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE / OWNER_TYPE_PROFILE_OWNER) from the CallerIdentity and delegate to new setBackwardCompatibleUserRestrictionLocked(ownerType, admin, userId, key, enabled, parent), which keeps the previous global-vs-local routing (UserRestrictionsUtils.isGlobal, getProfileParentId for parent).
- Commit f1eb8e719dfb, DevicePolicyManagerService.java:migrateUserRestrictionsLocked(): new method added to migratePoliciesPostUpgradeToDevicePolicyEngineLocked() after the lock task migration; for each user from mUserManager.getUsers() with getProfileOwnerOrDeviceOwnerLocked(), determines ownerType (throwing IllegalStateException on invalid DO/PO state), then calls setBackwardCompatibleUserRestrictionLocked(..., enabled=true, parent=false) for every key in admin.ensureUserRestrictions() and (..., enabled=true, parent=true) for every key in admin.getParentActiveAdmin().ensureUserRestrictions().
- Commit 736462a777d0, DevicePolicyManagerServiceMigrationTest.java: three COMP migration tests (testCompMigrationUnAffiliated_skipped, testCompMigration_keepSuspendedAppsWhenDpcIsRPlus, testCompMigration_unsuspendAppsWhenDpcNotRPlus) are marked @Ignore("b/277916462"); no new test coverage is added (Test: manual).
Completeness
- Covers: DO and org-owned-PO (parent) disableScreenCapture; DO and PO lock task packages/features; own and parent user restrictions of every DO/PO ActiveAdmin. On boot it runs once, guarded by the new persisted migratedPostUpgrade flag; the forced migration entry point runs it without checking or setting that flag.
- maybeMigratePoliciesPostUpgradeToDevicePolicyEngineLocked() ignores the boolean returned by migratePoliciesPostUpgradeToDevicePolicyEngineLocked() and calls markPostUpgradeMigration() unconditionally, so if any sub-migration throws (caught and logged), the remaining sub-migrations are skipped and the boot path does not retry on later boots.
- migrateLockTaskPolicyLocked() skips users whose mLockTaskPackages is empty (so mLockTaskFeatures alone is not migrated) and carries a TODO about persistent preferred activities, which are not migrated.
- migrateScreenCapturePolicyLocked() only handles the device owner's own flag and the org-owned PO's parent admin flag; the disableScreenCapture flag of a non-parent profile owner ActiveAdmin is not migrated by this method (whether another path handles it is not shown).
- migrateUserRestrictionsLocked() only migrates keys present in ensureUserRestrictions() as enabled=true, only for the DO/PO of each user, and dereferences admin.getParentActiveAdmin() directly; a null would throw, be caught by the outer try/catch, and abort the rest of the step.
- The post-upgrade step runs at boot only when mOwners.isMigratedToPolicyEngine() is true; whether it also runs on the same boot as an initial migration depends on whether migratePoliciesToDevicePolicyEngine() sets that flag, which is not shown. Since screen capture migration was removed from the initial migration, it depends on the post-upgrade step running.
Behavior changes
- New persisted attribute migratedPostUpgrade under the policy-engine-migration tag in the owners data, written via mData.writeDeviceOwner().
- At PHASE_ACTIVITY_MANAGER_READY, already-migrated devices perform a one-time re-application of screen capture, lock task and user restriction policies into the DevicePolicyEngine.
- Screen capture migration no longer depends on the policy cache state; it is driven solely by ActiveAdmin.disableScreenCapture.
- The forced migration entry point now holds the DPMS lock across the whole operation, also runs the post-upgrade step, and returns false if either step fails.
- Legacy user restrictions are re-set as enabled=true through setBackwardCompatibleUserRestrictionLocked, using the same global/local routing as setBackwardCompatibleUserRestriction().
- Three COMP migration unit tests are disabled.
Per major
- Android 14: same fix: commits 736462a777d0 and f1eb8e719dfb in android-security-14.0.0_r9 (both linked by bug 318497672); at android-security-14.0.0_r29 the patched region is fully present (8/8 lines), including maybeMigratePoliciesPostUpgradeToDevicePolicyEngineLocked() in systemReady and ATTR_MIGRATED_POST_UPGRADE in OwnersData.
- Android 15: not affected per the bulletin; at android-security-15.0.0_r17 the patched region partially matches (5/8 lines).
- Android 16: not affected per the bulletin; at android-security-16.0.0_r8 the patched region partially matches (5/8 lines).
- Android 17: not affected per the bulletin; at android-security-17.0.0_r1 the patched region partially matches (5/8 lines).
Unknowns
- The exact name of the MANAGE_PROFILE_AND_DEVICE_OWNERS-gated forced migration method around line 24151 is not shown in the diff context.
- Whether migratePoliciesToDevicePolicyEngine() sets mOwners' migratedToPolicyEngine flag (and thus whether the post-upgrade step runs on the same boot as an initial migration) is not shown.
- Why the early return on mPolicyCache.getScreenCaptureDisallowedUser() was incorrect is not explained.
- Whether ActiveAdmin.getParentActiveAdmin() can return null for a DO or non-org-owned PO, which would make migrateUserRestrictionsLocked() throw and abort the step, is not shown.
- Whether a separate path migrates non-parent profile owner disableScreenCapture is not shown.
- The concrete report that triggered the bulletin entry (which of the three policy groups) is not stated; the second commit's test description references DISALLOW_FACTORY_RESET.
- Why the three COMP migration tests were disabled (b/277916462) is not explained in the material.
- How Android 15 and later handle these migrations is not shown beyond the partial line matches.