CVE-2024-0047 AI-generated analysis
platform/frameworks/base, UserManagerService (services/core pm) and DevicePolicyEngine (services/devicepolicy)
Three frameworks/base commits make UserManagerService persist device policy global and local user restrictions under distinct XML tags, read the pre-userVersion-10 local restriction format correctly on upgrade, and force DevicePolicyEngine to re-sync global and per-user restriction state into UserManager whenever a DPC sets or clears a user restriction.
Root cause
In UserManagerService.writeUserLP, both mDevicePolicyUserRestrictions.getRestrictions(UserHandle.USER_ALL) (global) and getRestrictions(userInfo.id) (local) were serialized under TAG_DEVICE_POLICY_RESTRICTIONS. readUserLP parses that tag as legacyLocalRestrictions rather than as globalRestrictions/localRestrictions. The commit message of f51673939874 states that on certain Android 14 builds global restrictions set by the DPC can go into UserManager's local restriction store, a state where they "cannot be unset in future". Separately, when upgrading from userVersion 9 (Android 13), readUserLP parsed TAG_DEVICE_POLICY_LOCAL_RESTRICTIONS with UserRestrictionsUtils.readRestrictions, but the v9 format stores local restrictions as restrictions_user sub tags grouped by source user id, so they were misread (commit message: read as base restrictions).
How the fix works
- Commit 3cd8a2c783fc, services/core/java/com/android/server/pm/UserManagerService.java:writeUserLP: writes the USER_ALL restrictions with TAG_DEVICE_POLICY_GLOBAL_RESTRICTIONS and the per-user restrictions with TAG_DEVICE_POLICY_LOCAL_RESTRICTIONS, matching the tags readUserLP parses into globalRestrictions and localRestrictions. setUserRestrictionInner is made package-visible with @VisibleForTesting; a new test testWriteReadDevicePolicyUserRestrictions in UserManagerServiceUserInfoTest round-trips a global and a local restriction.
- Commit bd5cc7f03256, UserManagerService.java:readUserLP(int id, int userVersion) and readUserLP(int id, InputStream is, int userVersion): the caller in the user list reader passes mUserVersion. When userVersion < 10 and the tag is TAG_DEVICE_POLICY_LOCAL_RESTRICTIONS, the block is parsed with RestrictionsSet.readRestrictions(parser, TAG_DEVICE_POLICY_LOCAL_RESTRICTIONS) and flattened with mergeAll(); otherwise the flat UserRestrictionsUtils.readRestrictions(parser) path is kept. A v9 fixture user_100_v9.xml and test testUserRestrictionsUpgradeFromV9 assert the restrictions land as user restrictions, not base restrictions.
- Commit f51673939874, services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java: adds maybeForceEnforcementRefreshLocked, shouldForceEnforcementRefresh (true only when the PolicyKey is a UserRestrictionPolicyKey) and forceEnforcementRefreshLocked, which under a cleared calling identity reads the resolved global PolicyState and calls enforcePolicy(definition, value, USER_ALL), then for every user from mUserManager.getUsers() reads the resolved local PolicyState and calls enforcePolicy for that user (defaulting to BooleanPolicyValue(false) when the policy has no state at that scope). It is invoked in four places: the local and global policy set paths (when !skipEnforcePolicy) and the local and global policy removal paths, so both UserManager restriction scopes are overwritten from policy engine state on each DPC set/clear. All throwables are caught and logged.
Completeness
- The writer fix covers both restriction scopes written by writeUserLP; readUserLP already distinguished the global, local and legacy tags, so new files round-trip correctly.
- The upgrade fix is gated on userVersion < 10 and only changes parsing of TAG_DEVICE_POLICY_LOCAL_RESTRICTIONS; TAG_DEVICE_POLICY_RESTRICTIONS continues to be read as legacyLocalRestrictions. The material does not show how legacyLocalRestrictions is applied, so how files already written with the wrong tag are handled on read is not shown.
- The DevicePolicyEngine resync repairs UserManager state only for the specific user restriction policy being set or cleared and only when a DPC touches it through the set/remove paths (set paths skipped when skipEnforcePolicy is true); restrictions that are never set or cleared again are not resynced by this code.
- The resync iterates mUserManager.getUsers(); whether this covers every user whose file may hold stale state is not shown in the material.
Behavior changes
- Per-user files now store device policy restrictions under TAG_DEVICE_POLICY_GLOBAL_RESTRICTIONS and TAG_DEVICE_POLICY_LOCAL_RESTRICTIONS instead of two TAG_DEVICE_POLICY_RESTRICTIONS elements.
- readUserLP gained a userVersion parameter (internal and @VisibleForTesting signatures changed).
- Every DPC set or clear of a user restriction now triggers enforcePolicy for USER_ALL and for each user returned by mUserManager.getUsers(), with the calling identity cleared; failures are logged at error level and swallowed.
Per major
- Android 14: same fix: three cherry-picked commits (3cd8a2c783fc, bd5cc7f03256, f51673939874) in android-security-14.0.0_r6; at android-security-14.0.0_r29 6/8 lines of the patched region are present
- Android 15: not affected per the bulletin; at android-security-15.0.0_r17 6/8 lines of the patched region are present
- Android 16: not affected per the bulletin; at android-security-16.0.0_r8 6/8 lines of the patched region are present
- Android 17: not affected per the bulletin; at android-security-17.0.0_r1 6/8 lines of the patched region are present
Unknowns
- The exact DoS consequence of restrictions becoming unclearable is not described beyond the bulletin type and the commit message.
- How legacyLocalRestrictions (TAG_DEVICE_POLICY_RESTRICTIONS) is applied later in readUserLP, and therefore the full effect of files written by unpatched builds, is not shown.
- The names of the DevicePolicyEngine methods into which maybeForceEnforcementRefreshLocked was inserted are not shown in the diff hunks.
- Which commit the per-major '6/8 lines present' metric refers to, and why two lines differ at the latest tags.