ProtectedPackages 数据保护
本文面向已经了解设备所有者、Profile Owner 和 PMS 卸载/数据清理入口的读者。它聚焦一个经常被误判的边界:为什么 system 或 privileged app 仍然不能禁用、隐藏或清除某些包。本文不重新讲设备策略服务如何选出 owner,而是从 Android 17 的 ProtectedPackages 到 PMS 的真实调用点,说明“受保护”究竟保护了什么、按哪个 user 生效,以及调用失败时在哪一层返回。
1. Owner 数据
源码文件:frameworks/base/services/core/java/com/android/server/pm/ProtectedPackages.java。
ProtectedPackages 自身不持久化 owner 信息,类注释明确留下了 TODO;它接收 DevicePolicyManager 侧同步来的包名,并在内存中按 user 保存:
@GuardedBy("this")
private int mDeviceOwnerUserId;
@GuardedBy("this")
private String mDeviceOwnerPackage;
@GuardedBy("this")
private SparseArray<String> mProfileOwnerPackages;
@GuardedBy("this")
private SparseArray<String> mDevicePolicyControllerPackages;
@GuardedBy("this")
private final SparseArray<Set<String>> mOwnerProtectedPackages =
new SparseArray<>();受保护来源有五类:device/profile owner 包、DPC 包、系统设备 provisioning 包、owner 额外设置的 protected package,以及 supervision role holder。它们最终汇入 isProtectedPackage,但不共享同一个配置来源。
2. 两种保护
isPackageStateProtected(userId, packageName) 和 isPackageDataProtected(userId, packageName) 当前实现都返回相同的 owner/protected 判断,但调用语义不同。前者用于改变 enabled/hidden 等包状态;后者用于清除 app data。保留两个 API 让 PMS 在未来可以独立扩展状态保护和数据保护。
public boolean isPackageStateProtected(int userId, String packageName) {
return hasDeviceOwnerOrProfileOwner(userId, packageName)
|| isProtectedPackage(userId, packageName);
}
public boolean isPackageDataProtected(int userId, String packageName) {
return hasDeviceOwnerOrProfileOwner(userId, packageName)
|| isProtectedPackage(userId, packageName);
}类注释给出的安全含义是:除包 owner 自己外,system 或 privileged app 也不能修改受保护包的数据或 package state。这个条件不是普通 MANAGE_DEVICE_ADMINS 权限的替代品,而是 PMS 在具体目标包上增加的一道拒绝检查。
3. 同步入口
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;符号:setDeviceAndProfileOwnerPackages、setDevicePolicyControllerPackages。
public void setDeviceAndProfileOwnerPackages(
int deviceOwnerUserId, String deviceOwnerPackage,
SparseArray<String> profileOwnerPackages) {
mProtectedPackages.setDeviceAndProfileOwnerPackages(
deviceOwnerUserId, deviceOwnerPackage, profileOwnerPackages);
...
}
public void setDevicePolicyControllerPackages(
SparseArray<String> packages) {
mProtectedPackages.setDevicePolicyControllerPackages(packages);
...
}两个同步方法都在 PackageManagerInternal 暴露给系统内部调用。ProtectedPackages 对传入的 SparseArray 做 clone,避免调用方随后修改原对象而绕过服务的状态边界;setOwnerProtectedPackages 对 package list 构造新的 ArraySet。
同步后 PMS 还会移除对应 user 的非系统 package suspension。这是策略联动,不是保护判断本身:DPC 成为 owner 后,旧的普通挂起状态不能继续阻止系统管理该用户的包。
4. 清数据拒绝
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;符号:IPackageManagerImpl.clearApplicationUserData。
snapshot.enforceCrossUserPermission(callingUid, userId,
true /* requireFullPermission */, false /* checkShell */,
"clear application data");
if (snapshot.getPackageStateForInstalledAndFiltered(
packageName, callingUid, userId) == null) {
// 对调用者不可见或未安装,异步回调 false
return;
}
if (mProtectedPackages.isPackageDataProtected(userId, packageName)) {
throw new SecurityException(
"Cannot clear data for a protected package: " + packageName);
}检查顺序有实际意义:先做跨用户权限和可见性检查,再判断 protected。一个不可见包不会通过异常内容泄露“它是否是受保护包”。保护命中后不会创建 PackageFreezer,也不会进入 installer 的数据删除调用。
5. 状态修改拒绝
组件 enabled/disabled 修改在 PMS 内部先验证调用者是否是目标 app 或拥有管理权限,再检查 protected state:
if (!isCallerTargetApp
&& mProtectedPackages.isPackageStateProtected(userId, packageName)) {
throw new SecurityException(
"Cannot disable a protected package: " + packageName);
}隐藏包的路径使用不同策略:只有受保护包自己可以请求隐藏;其他调用者即使是 system,也会记录 warning 并返回 false。这个“自己可隐藏、别人不可修改”的例外只属于 hide 操作,不能外推到 clear data 或 disable component。
6. Owner 特殊包
hasDeviceOwnerOrProfileOwner 只按给定 userId 比较 device owner user 和 profile owner map。device owner 包在其他 user 上不会因为“设备上存在 device owner”而自动成为那个 user 的 protected package。
isProtectedPackage 还检查:
- 当前 user 的 DPC package;
config_deviceProvisioningPackage指定的 provisioning 包;setOwnerProtectedPackages传入的 user 或USER_ALL集合;- supervision role 在该 user 是否启用且包是否为 role holder。
supervision 检查通过 Binder.withCleanCallingIdentity 查询 RoleManager,避免把调用者身份带入系统角色查询。若 SupervisionManager 或 RoleManager 不可用,源码分别记录 warning 并按“不属于 supervision protected”继续。
7. 安装现有包
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java;符号:installExistingPackageAsUser。
安装现有包为某 user 时,protected state 参与 instant app 约束:系统不会把 system、updated system、device admin 或 protected package 作为 instant app 安装目标。这里的保护不是拒绝普通 full app 安装,而是阻止改变 package 的 instant/full 身份。
boolean isProtectedPackage = mPm.mProtectedPackages != null
&& mPm.mProtectedPackages.isPackageStateProtected(userId, packageName);
if (instantApp && (pkgSetting.isSystem()
|| pkgSetting.isUpdatedSystemApp()
|| isPackageDeviceAdmin || isProtectedPackage)) {
return Pair.create(INSTALL_FAILED_INVALID_URI, intentSender);
}因此“安装 existing 失败”要先区分 install flags;同一个包用 full-app 路径可能成功,而 instant-app 路径会被保护规则拒绝。
8. 用户维度
保护状态属于 user。PMS 的 PackageManagerInternal.isPackageStateProtected(packageName, userId) 会先执行 cross-user permission,再检查调用者是否为 system/root 或拥有 MANAGE_DEVICE_ADMINS;普通应用不能用这个内部查询接口枚举保护状态。
工作资料、克隆用户或受限用户的问题,不能只看 device owner 包名;必须把调用 userId、profile owner map 和 owner protected list 一起打印。
9. 失败定位
| 现象 | 首先检查 | 结论边界 |
|---|---|---|
| 清数据抛 SecurityException | isPackageDataProtected 调用前的 user、owner、DPC | 保护命中时不会进入 freezer/installer |
| 禁用组件失败 | isPackageStateProtected 与调用者是否目标 app | 目标 app 自己与管理调用者路径不同 |
| hide 返回 false | hide 路径的 protected 分支 | 这是 warning + false,不等同于 SecurityException |
| instant app 安装失败 | installExistingPackageAsUser 的 flags | protected 只限制 instant 身份变更 |
| 保护状态看起来过期 | owner 同步方法、SparseArray clone、userId | ProtectedPackages 不自行持久化,需追同步 owner |
| system UID 也无法清理 | PMS 保护检查 | system/privileged 不会自动绕过 protected package |
10. 源码练习
- 给定 device owner 位于 user 0、目标包安装在 user 10,按照
hasDeviceOwnerOrProfileOwner判断 user 10 是否自动受保护,并说明 profile owner 的差异。 - 追踪
clearApplicationUserData的检查顺序,解释为什么不可见包不会暴露 protected 状态。 - 对比 disable、hide 和 clear data 三条路径,列出各自的异常类型、返回值和“目标 app 自己”例外。
- 构造一个 protected package 走
installExistingPackageAsUser(INSTALL_INSTANT_APP)的输入,指出INSTALL_FAILED_INVALID_URI在哪一层返回,以及为什么 full app 路径不能据此判定也会失败。
