system与priv-app
/system/app 和 /system/priv-app 的差别不是目录名带来的“权限自动升级”。在 Android 17 的启动扫描中,目录先由 InitAppsHelper.scanSystemDirs() 转成不同的 scanFlags,再由 InstallPackageHelper 和 ScanPackageUtils 把这些 flags 应用到 ParsedPackage、PackageSetting、ApplicationInfo 和权限检查。priv-app 的特权身份来自 SCAN_AS_PRIVILEGED,但能够获得哪些 privileged permission 还要经过 allowlist、签名和 shared UID 规则。
本篇聚焦 system/priv-app 的真实扫描主线:
/system/framework、priv-app、app 的参数为什么不同;SCAN_AS_SYSTEM、SCAN_AS_PRIVILEGED、PARSE_IS_SYSTEM_DIR和分区 flags 在哪里组合;ScanPackageUtils.applyPolicy()如何设置 system、privileged、direct boot、stub 和平台签名属性;- 更新系统应用从
/data/app扫描时,为什么必须从旧的 disabled system setting 恢复分区/特权 flags; - shared UID 中只要有一个 privileged 成员时,其他成员为什么也必须满足特权标记规则;
- system overlay、framework package、priv-app 权限 allowlist 和失败 cleanup 的 owner 分别是谁;
- 测试如何区分 flags 传递、包对象属性、权限授予和真实启动扫描。
PMS016 已讲过整体阶段,PMS017 已讲过目录筛选;PMS019 会独立展开 /data/app 用户安装包恢复,PMS020 再讨论 vendor/product/system_ext 分区。本文不把这些路径混成一个“大扫描函数”。
1. 目录到参数
1.1 四类输入
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private void scanSystemDirs(PackageParser2 packageParser, ExecutorService executorService) {
File frameworkDir = new File(Environment.getRootDirectory(), "framework");
List<ScanParams> scanParamsList = new ArrayList<>();
// Collect overlay packages before scanning any apps.
for (int i = mDirsToScanAsSystem.size() - 1; i >= 0; i--) {
final ScanPartition partition = mDirsToScanAsSystem.get(i);
if (partition.getOverlayFolder() == null) {
continue;
}
collectScanParams(scanParamsList, partition.getOverlayFolder(),
mSystemParseFlags, mSystemScanFlags | partition.scanFlag,
packageParser, executorService, partition.apexInfo);
}
collectScanParams(scanParamsList, frameworkDir, mSystemParseFlags,
mSystemScanFlags | SCAN_NO_DEX | SCAN_AS_PRIVILEGED,
packageParser, executorService, null);
for (int i = 0, size = mDirsToScanAsSystem.size(); i < size; i++) {
final ScanPartition partition = mDirsToScanAsSystem.get(i);
if (partition.getPrivAppFolder() != null) {
collectScanParams(scanParamsList, partition.getPrivAppFolder(),
mSystemParseFlags,
mSystemScanFlags | SCAN_AS_PRIVILEGED | partition.scanFlag,
packageParser, executorService, partition.apexInfo);
}
collectScanParams(scanParamsList, partition.getAppFolder(), mSystemParseFlags,
mSystemScanFlags | partition.scanFlag, packageParser, executorService,
partition.apexInfo);
}
parallelScanDirTracedLI(scanParamsList, packageParser, executorService);
}四类输入的差异:
| 目录 | parse flags | scan flags 额外项 | 结果含义 |
|---|---|---|---|
| overlay | mSystemParseFlags | partition flag | system overlay,后续执行 overlay policy |
| framework | mSystemParseFlags | SCAN_NO_DEX、SCAN_AS_PRIVILEGED | framework 核心包,禁止普通 dex 扫描路径 |
| priv-app | mSystemParseFlags | SCAN_AS_PRIVILEGED、partition flag | system + privileged app |
| app | mSystemParseFlags | partition flag | system app,但不是 privileged app |
mSystemScanFlags 已经包含 SCAN_BOOTING | SCAN_INITIAL | SCAN_AS_SYSTEM,首次启动/OTA 还可能包含 SCAN_FIRST_BOOT_OR_UPGRADE。因此 app 和 priv-app 都是 system app;只有 priv-app/framework 路径显式追加 SCAN_AS_PRIVILEGED。
1.2 Framework flags
framework 目录里的核心 APK 不是普通 /system/app 应用。源码为它附加 SCAN_NO_DEX | SCAN_AS_PRIVILEGED,表示它属于系统核心路径,不走常规 dex 扫描策略,并按 privileged package 参与后续 policy。这个标志来自目录角色,不是由 APK 自己在 Manifest 中声明。
1.3 App与priv-app
可以把两个参数表达式单独拿出来:
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java
// priv-app
mSystemScanFlags | SCAN_AS_PRIVILEGED | partition.scanFlag
// app
mSystemScanFlags | partition.scanFlag二者共享 SCAN_AS_SYSTEM、启动/初始扫描和分区归属;唯一直接差异是 SCAN_AS_PRIVILEGED。但这一个 bit 会影响 ParsedPackage.isPrivileged()、PackageSetting private flags、ApplicationInfo.PRIVATE_FLAG_PRIVILEGED 和 privileged permission allowlist 路径。
2. flags与包对象
2.1 applyPolicy
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.java
/**
* Applies policy to the parsed package based upon the given policy flags.
* Ensures the package is in a good state.
* <p>
* Implementation detail: This method must NOT have any side effect. It would
* ideally be static, but, it requires locks to read system state.
*/
public static void applyPolicy(ParsedPackage parsedPackage,
final @PackageManagerService.ScanFlags int scanFlags,
@Nullable AndroidPackage platformPkg, boolean isUpdatedSystemApp) {方法名虽然是 apply policy,但源码注释要求它不能产生外部 side effect;它修改的是当前 ParsedPackage 的解析/策略属性,供后续 scan/register 使用。平台包 platformPkg 只用于签名比较,不是一个写入 owner。
2.2 System标志
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.java
boolean isSystemApp = isUpdatedSystemApp;
if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
isSystemApp = true;
parsedPackage.setSystem(true);
if (parsedPackage.isDirectBootAware()) {
parsedPackage.setAllComponentsDirectBootAware(true);
}
if (compressedFileExists(parsedPackage.getPath())) {
parsedPackage.setStub(true);
}
} else {
parsedPackage
.clearProtectedBroadcasts()
.setCoreApp(false)
.setPersistent(false)
.setDefaultToDeviceProtectedStorage(false)
.setDirectBootAware(false)
.capPermissionPriorities();
}system scan 不只是设置 isSystem=true:
- direct-boot-aware system package 会把所有组件标记为 direct boot aware;
- 压缩 code path 会被识别为 stub package;
- 非 system package 则清除 protected broadcasts、core、persistent、device-protected storage、direct boot 和 permission priority 等只允许系统使用的属性。
这解释了为什么同一个 APK 从 system 目录和 data 目录扫描,可能得到不同的 parsed policy,即使 Manifest 内容没有变化。
2.3 分区属性
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.java
if ((scanFlags & SCAN_AS_PRIVILEGED) == 0) {
parsedPackage.markNotActivitiesAsNotExportedIfSingleUser();
}
parsedPackage.setApex((scanFlags & SCAN_AS_APEX) != 0);
parsedPackage.setPrivileged((scanFlags & SCAN_AS_PRIVILEGED) != 0)
.setOem((scanFlags & SCAN_AS_OEM) != 0)
.setVendor((scanFlags & SCAN_AS_VENDOR) != 0)
.setProduct((scanFlags & SCAN_AS_PRODUCT) != 0)
.setSystemExt((scanFlags & SCAN_AS_SYSTEM_EXT) != 0)
.setOdm((scanFlags & SCAN_AS_ODM) != 0);没有 SCAN_AS_PRIVILEGED 时,parser 会对 single-user 场景执行额外的 non-exported 处理;priv-app/framework 路径跳过这一步。随后 system、privileged、OEM、vendor、product、system_ext、ODM 等属性从 scan flags 写入 ParsedPackage。
2.4 平台签名
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.java
parsedPackage.setSignedWithPlatformKey(
PLATFORM_PACKAGE_NAME.equals(parsedPackage.getPackageName())
|| (platformPkg != null && compareSignatures(
platformPkg.getSigningDetails(),
parsedPackage.getSigningDetails()) == PackageManager.SIGNATURE_MATCH));setSignedWithPlatformKey() 与 setPrivileged() 是两个独立属性。一个 /system/app 包可以使用 platform key 但不是从 priv-app 目录扫描;一个 priv-app 也必须满足自己的签名/allowlist 规则。调试权限问题时不能只看 isPrivileged() 或只看 platform signature。
3. PackageSetting
3.1 更新flags
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.java
public static @PackageManagerService.ScanFlags int adjustScanFlagsWithPackageSetting(
@PackageManagerService.ScanFlags int scanFlags,
PackageSetting pkgSetting, PackageSetting disabledPkgSetting, UserHandle user) {
final PackageSetting systemPkgSetting =
(scanFlags & SCAN_NEW_INSTALL) != 0 && disabledPkgSetting == null
&& pkgSetting != null && pkgSetting.isSystem()
? pkgSetting
: disabledPkgSetting;
if (systemPkgSetting != null) {
// updated system application, must at least have SCAN_AS_SYSTEM
scanFlags |= SCAN_AS_SYSTEM;
if ((systemPkgSetting.getPrivateFlags()
& ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
scanFlags |= SCAN_AS_PRIVILEGED;
}
if ((systemPkgSetting.getPrivateFlags()
& ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0) {
scanFlags |= SCAN_AS_VENDOR;
}
if ((systemPkgSetting.getPrivateFlags()
& ApplicationInfo.PRIVATE_FLAG_PRODUCT) != 0) {
scanFlags |= SCAN_AS_PRODUCT;
}
if ((systemPkgSetting.getPrivateFlags()
& ApplicationInfo.PRIVATE_FLAG_SYSTEM_EXT) != 0) {
scanFlags |= SCAN_AS_SYSTEM_EXT;
}
if ((systemPkgSetting.getPrivateFlags()
& ApplicationInfo.PRIVATE_FLAG_ODM) != 0) {
scanFlags |= SCAN_AS_ODM;
}
}
if (pkgSetting != null) {
final int userId = user == null ? 0 : user.getIdentifier();
if (pkgSetting.getInstantApp(userId)) {
scanFlags |= SCAN_AS_INSTANT_APP;
}
if (pkgSetting.getVirtualPreload(userId)) {
scanFlags |= SCAN_AS_VIRTUAL_PRELOAD;
}
}
return scanFlags;
}data 分区的 updated system app 没有从目录路径自动获得 system/privileged/vendor/product 等身份。adjustScanFlagsWithPackageSetting() 会从 active 或 disabled system PackageSetting 的 private flags 恢复这些属性,确保 /data/app 覆盖 /system/priv-app 时不会因为物理路径变化而丢失 privileged 身份。
3.2 Disabled读取
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
int pkgFlags = 0;
int pkgPrivateFlags = 0;
pkgFlags |= ApplicationInfo.FLAG_SYSTEM;
if (codePathStr.contains("/priv-app/")) {
pkgPrivateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
}
PackageSetting ps = new PackageSetting(name, realName, new File(codePathStr), pkgFlags,
pkgPrivateFlags, domainSetId)
.setLongVersionCode(versionCode)
.setTargetSdkVersion(targetSdkVersion)
.setScannedAsStoppedSystemApp(isScannedAsStoppedSystemApp);从旧 settings 读取 disabled system package 时,Settings 根据保存的 codePath 是否包含 /priv-app/ 恢复 PRIVATE_FLAG_PRIVILEGED。这只是持久化恢复的初始值;真正更新扫描时还要经过 adjustScanFlagsWithPackageSetting(),以当前 scan context 和旧 setting 合并。
3.3 状态写回
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
// If what we are scanning is a system (and possibly privileged) package,
// then make it so, regardless of whether it was previously installed only
// in the data partition. Reset first.
int newPkgFlags = pkgSetting.getFlags();
newPkgFlags &= ~ApplicationInfo.FLAG_SYSTEM;
newPkgFlags |= pkgFlags & ApplicationInfo.FLAG_SYSTEM;
pkgSetting.setFlags(newPkgFlags);
boolean wasRequiredForSystemUser = (pkgSetting.getPrivateFlags()
& ApplicationInfo.PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER) != 0;
if (wasRequiredForSystemUser) {
pkgPrivateFlags |= ApplicationInfo.PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER;
} else {
pkgPrivateFlags &= ~ApplicationInfo.PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER;
}
pkgSetting.setPrivateFlags(pkgPrivateFlags);Settings 更新 package state 时先清掉旧 FLAG_SYSTEM,再按本次扫描 policy 写入,避免 data/system 两次扫描叠加出错误标志。REQUIRED_FOR_SYSTEM_USER 则被单独保留/清理,说明 private flags 并非全部来自目录路径。
4. Privileged权限
4.1 标志与权限
SCAN_AS_PRIVILEGED 使 package 成为 privileged app,后续权限系统还要读取 privapp-permissions allowlist、签名和分区规则。Permission.isPrivileged() 判断的是 permission 的 protection level:
源码文件:frameworks/base/services/core/java/com/android/server/pm/permission/Permission.java
public boolean isPrivileged() {
return (mPermissionInfo.protectionLevel
& PermissionInfo.PROTECTION_FLAG_PRIVILEGED) != 0;
}
public boolean isVendorPrivileged() {
return (mPermissionInfo.protectionLevel
& PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED) != 0;
}package 是否 privileged 和 permission 是否 signature|privileged 是两条轴。前者来自扫描 flags/package state,后者来自 permission 定义;授予还需要 allowlist 与 policy owner 的共同判断。
4.2 Shared UID
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
private void assertPackageWithSharedUserIdIsPrivileged(AndroidPackage pkg)
throws PackageManagerException {
if (!AndroidPackageLegacyUtils.isPrivileged(pkg)
&& pkg.getSharedUserId() != null
&& !pkg.isLeavingSharedUser()) {
SharedUserSetting sharedUserSetting = null;
synchronized (mPm.mLock) {
try {
sharedUserSetting = mPm.mSettings.getSharedUserLPw(
pkg.getSharedUserId(), 0, 0, false);
} catch (PackageManagerException ignore) {
}
}
if (sharedUserSetting != null && sharedUserSetting.isPrivileged()) {
final PackageSetting platformPkgSetting;
synchronized (mPm.mLock) {
platformPkgSetting = mPm.mSettings.getPackageLPr("android");
}
if (!comparePackageSignatures(platformPkgSetting,
pkg.getSigningDetails())) {
throw PackageManagerException.ofInternalError(
"Apps that share a user with a privileged app must themselves be marked "
+ "as privileged. " + pkg.getPackageName(),
PackageManagerException.INTERNAL_ERROR_NOT_PRIV_SHARED_USER);
}
}
}
}shared UID group 中已有 privileged app 时,新成员不能只是普通 app;除非它使用 platform signature 的特殊豁免,否则扫描会失败。原因是同一 Linux UID 共享权限边界,若普通成员加入 privileged group,整个 UID 的权限语义会被削弱或混淆。
4.3 兼容调整
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
private @PackageManagerService.ScanFlags int adjustScanFlags(
@PackageManagerService.ScanFlags int scanFlags,
@Nullable PackageSetting existingPkgSetting,
@Nullable PackageSetting disabledPkgSetting, UserHandle user,
@NonNull AndroidPackage pkg) {
scanFlags = ScanPackageUtils.adjustScanFlagsWithPackageSetting(
scanFlags, existingPkgSetting, disabledPkgSetting, user);
final boolean skipVendorPrivilegeScan = ((scanFlags & SCAN_AS_VENDOR) != 0)
&& ScanPackageUtils.getVendorPartitionVersion() < 28;
if (((scanFlags & SCAN_AS_PRIVILEGED) == 0)
&& !AndroidPackageLegacyUtils.isPrivileged(pkg)
&& pkg.getSharedUserId() != null
&& !skipVendorPrivilegeScan
&& !pkg.isLeavingSharedUser()) {
// shared-user privileged compatibility handling
}
return scanFlags;
}这里说明 privileged 可能由两处进入:目录/旧 PackageSetting 恢复,或 shared-user 兼容逻辑根据已存在 group 调整。vendor partition version 小于 28 时还有兼容跳过条件,不能把“所有 shared UID 都强制 privileged”写成无条件结论。
5. 版本协调与失败
5.1 System判断
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
private Pair<ScanResult, Boolean> scanPackageForInitLI(ParsedPackage parsedPackage,
@ParsingPackageUtils.ParseFlags int parseFlags,
@PackageManagerService.ScanFlags int scanFlags,
@Nullable UserHandle user) throws PackageManagerException {
final boolean scanSystemPartition =
(parseFlags & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR) != 0;
final ScanRequest initialScanRequest = prepareInitialScanRequest(
parsedPackage, parseFlags, scanFlags, user, null, null);
final PackageSetting installedPkgSetting = initialScanRequest.mPkgSetting;
final PackageSetting originalPkgSetting = initialScanRequest.mOriginalPkgSetting;
final PackageSetting pkgSetting = originalPkgSetting == null
? installedPkgSetting : originalPkgSetting;
final boolean pkgAlreadyExists = pkgSetting != null;PARSE_IS_SYSTEM_DIR 是判断当前输入是否来自 system partition 的 parse 语境。它与 SCAN_AS_SYSTEM 相关但不相同:前者描述输入目录/解析来源,后者描述 package policy。更新系统 app 从 data 目录进入时,parse flags 可能没有 system-dir,但旧 disabled setting 仍能把它识别为 updated system package。
5.2 缺失恢复
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if (scanSystemPartition && !pkgAlreadyExists
&& mPm.mSettings.getDisabledSystemPkgLPr(disabledPkgName) != null) {
Slog.w(TAG, "Inconsistent package setting of updated system app for "
+ disabledPkgName + ". To recover it, enable the system app "
+ "and install it as non-updated system app.");
mPm.mSettings.removeDisabledSystemPackageLPw(disabledPkgName);
}
disabledPkgSetting = mPm.mSettings.getDisabledSystemPkgLPr(disabledPkgName);
isSystemPkgUpdated = disabledPkgSetting != null;如果扫描 system partition 时发现 package state 不在 active setting,但 disabled system package 还存在,PMS 认为 /data 更新状态与 system APK 不一致,会删除 disabled record 并恢复 system app。这个恢复发生在包注册前,避免后续把孤立的 disabled setting 当作有效更新基线。
5.3 更新扫描
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if (scanSystemPartition && isSystemPkgUpdated) {
final ScanRequest request = new ScanRequest(parsedPackage,
mPm.mSettings.getSharedUserSettingLPr(disabledPkgSetting),
null, disabledPkgSetting, initialScanRequest.mSharedUserSetting,
null, null, null, parseFlags, scanFlags,
initialScanRequest.mIsPlatformPackage, user, null,
initialScanRequest.mEnableAlignmentChecks);
ScanPackageUtils.applyPolicy(parsedPackage, scanFlags,
mPm.getPlatformPackage(), true);
final ScanResult scanResult = ScanPackageUtils.scanPackageOnly(
request, mPm.mInjector, mPm.mFactoryTest, -1L);
}系统分区发现更新系统 app 时,PMS 需要先用 disabled system setting 作为旧版本基线,检查签名、版本、shared UID 和 policy,再决定 active package 如何替换。scanPackageOnly() 只完成扫描计算,不代表已经把最终 state 写回;后续 addForInitLI()/commit 负责注册和替换。
5.4 失败差异
processParseResult() 已在 PMS017 展示:错误结果带 SCAN_AS_SYSTEM 时不会直接 removeCodePath();非 system data package 则会删除 invalid code path。这个差异是为了保护系统分区输入和清理用户数据分区坏包,不能将两者统一成“parse 失败就删 APK”。
6. 输出与验证
6.1 AppInfo flags
扫描结果最终可在 ApplicationInfo 观察:
源码文件:frameworks/base/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
int pkgWithoutStateFlags = flag(AndroidPackageLegacyUtils.isPrivileged(pkg),
ApplicationInfo.PRIVATE_FLAG_PRIVILEGED)
| flag(AndroidPackageLegacyUtils.isOem(pkg), ApplicationInfo.PRIVATE_FLAG_OEM)
| flag(AndroidPackageLegacyUtils.isVendor(pkg), ApplicationInfo.PRIVATE_FLAG_VENDOR)
| flag(AndroidPackageLegacyUtils.isProduct(pkg), ApplicationInfo.PRIVATE_FLAG_PRODUCT)
| flag(AndroidPackageLegacyUtils.isOdm(pkg), ApplicationInfo.PRIVATE_FLAG_ODM)
| flag(AndroidPackageLegacyUtils.isSystemExt(pkg),
ApplicationInfo.PRIVATE_FLAG_SYSTEM_EXT);ApplicationInfo.PRIVATE_FLAG_PRIVILEGED 是从 package policy 生成的派生输出。调试时应同时查看 ApplicationInfo.FLAG_SYSTEM、private flags、sourceDir/codePath 和 package state;只看到路径包含 priv-app 不能证明最终对象仍保留 privileged(更新、移除 shared UID、policy 清理都可能改变结果)。
6.2 dumpsys对照
dumpsys package <package> 的 package 条目通常能对照:
codePath:当前 active package path;flags/privateFlags:system、privileged、vendor/product 等派生状态;versionCode、firstInstallTime:PackageSetting 持久化字段;User X:下的 installed/stopped/hidden 等 per-user state;disabled system packages::被 data 更新遮蔽的 factory/system setting。
如果 /data/app 更新包显示 FLAG_SYSTEM 或 PRIVATE_FLAG_PRIVILEGED,应回到 adjustScanFlagsWithPackageSetting() 和 disabled setting,而不是猜测 data 路径本身带有 system 身份。
6.3 真实设备检查
建议按这条顺序验证:
dumpsys package <package>记录 codePath、flags/privateFlags 和 disabled system package;- 对同一包检查
/system/priv-app、/system/app或/data/app的实际来源; - 查看启动 log 中
Failed to scan、Failed to parse、shared-user privileged error 和 APEX 内 APK error; - 对 privileged permission 查看
privapp-permissionsallowlist 和 PermissionManager 日志; - OTA/update 场景比较 active package 与 disabled system package 的 version/path/signature;
- shared UID 场景检查 group 中所有成员的 privileged 标志和 platform signature。
7. 测试与证明范围
7.1 Settings测试
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1,
ApplicationInfo.FLAG_SYSTEM,
ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
assertThat(packageSetting.isSystem()).isTrue();
assertThat(packageSetting.isPrivileged()).isTrue();这类测试证明 PackageSetting flags 的写入/读取和 isPrivileged() 派生方法,不证明扫描目录会正确传入 SCAN_AS_PRIVILEGED。
7.2 ScanTests
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
final ApplicationInfo applicationInfo = PackageInfoUtils.generateApplicationInfo(
pkgSetting.getPkg(), 0, pkgSetting.getUserStateOrDefault(0), 0, pkgSetting);
assertBasicApplicationInfo(scanResult, applicationInfo);输入是已有 scan result 的 PackageSetting/AndroidPackage,动作是生成 ApplicationInfo,断言 path、ABI、flags 等派生字段。它覆盖对象转换,不覆盖 /system/priv-app 目录收集、更新 system app flags 恢复或 privileged permission allowlist。
7.3 集成测试
要证明本文主线,测试应使用可控 system/data package 输入:
- 同一 APK 分别以 app flags、priv-app flags 扫描,断言
isSystem相同而isPrivileged不同; - 更新 system priv-app 从 data 路径扫描,断言旧 disabled setting 恢复 privileged/vendor/product flags;
- shared UID 中加入非 privileged 成员,断言非 platform signature 时抛
INTERNAL_ERROR_NOT_PRIV_SHARED_USER; - compressed system package 断言 stub 标志;
- system package parse failure 不删除 code path,data package parse failure 删除 invalid code path;
- framework package 缺失时
scanSystemDirs()抛IllegalStateException。
这些测试分别证明 flags、版本协调、shared UID policy、stub 和失败 cleanup,不能由单个 PackageInfoUtils 测试替代。
8. 阅读路线
读一个 system/priv-app 扫描问题时,按以下顺序追源码:
- 从
InitAppsHelper.scanSystemDirs()确认目录、parse flags、scan flags 和 partition flag。 - 进入
ScanParams/InstallPackageHelper.installPackagesFromDir(),确认是否携带 APEX context、是否 drop cache。 - 进入
processParseResult()和addForInitLI(),记录 parse failure、registration failure、system/data cleanup 分支。 - 进入
ScanPackageUtils.applyPolicy(),逐项确认 system、privileged、partition、stub、direct boot 和 platform signature 属性。 - 若是更新 system app,进入
adjustScanFlagsWithPackageSetting()、disabled system setting 和scanPackageForInitLI()。 - 若涉及权限,分别检查 package privileged 标志、permission protection level、privapp allowlist 和 shared UID 一致性。
- 回到 Settings/PackageInfoUtils/dumpsys 对照最终 flags/privateFlags、path 和 per-user state。
- 最后用 ScanTests、Settings tests 和设备启动/OTA 实验区分“参数传递正确”“对象生成正确”“权限最终授予”。
小结
Android 17 的 system/priv-app 扫描是一条由目录角色和历史状态共同决定的 policy 链:
/system/app和/system/priv-app都通过SCAN_AS_SYSTEM成为 system app,只有后者额外带SCAN_AS_PRIVILEGED;framework 目录还带SCAN_NO_DEX。ScanPartition和partition.scanFlag把 vendor/product/system_ext/odm 等分区身份传入扫描;APEX 内 APK 还增加自己的 parse/scan context。ScanPackageUtils.applyPolicy()把 flags 转成 system、privileged、direct boot、stub、APEX、分区和 platform signature 属性;platform signed 不等于 privileged。- data 路径上的 updated system app 依赖 active/disabled
PackageSetting恢复 system、privileged 和分区 flags,不能按物理路径重新猜测身份。 - shared UID 中已有 privileged group 时,新成员必须满足 privileged/签名一致性,否则扫描失败;这是共享 Linux UID 权限边界的结果。
- system package、APEX 和 data package 的失败处理不同:核心 system/APEX 错误可能终止启动,普通 data 错误会清理 code path。
- 最终调试要同时对照目录、scan flags、ParsedPackage、PackageSetting、ApplicationInfo、privapp allowlist 和 dumpsys 输出;单看 path 或单看 private flag 都不足以证明权限已经生效。
下一篇 PMS019 将转入 /data/app 用户安装包扫描,重点分析 SCAN_REQUIRE_KNOWN、已卸载保留数据、安装恢复、invalid code path 清理和 system package update 与普通用户包的分叉。
