vendor与product扫描
本文面向已经了解 PMS 启动扫描入口、希望继续沿源码定位分区包状态的读者;读完后应能从一个 APK 的 code path 找到对应的 ScanPartition、扫描 flags、overlay policy、持久化状态和 SELinux seInfo 消费点。Android 17 的 /vendor、/odm、/oem、/product 和 /system_ext 不是简单追加到 /system 后面的目录。PMS 先在 PackagePartitions 中定义每个分区可以拥有的 app、priv-app、overlay 目录,再由 ScanPartition 把 partition type 转成 SCAN_AS_VENDOR、SCAN_AS_PRODUCT 等 flags。之后这些 flags 会进入 ParsedPackage、PackageSetting、ApplicationInfo、SELinux seInfo 和 overlay 校验。
这篇文章专门处理多分区带来的状态问题:
/oem/priv-app为什么在 Android 17 的模型中根本不存在;- 分区列表的 generic→specific 顺序如何影响 overlay 与 package 扫描;
- APEX 内 APK 为什么继承原始分区 flag;
- PMS 如何从 code path 反向恢复 system、privileged 和 partition flags;
- vendor/odm overlay 的 target SDK 规则为何与非预装 overlay 不同;
targetName、target package 和 reference package 的签名检查由谁执行;- 分区身份如何进入 SELinux
seInfo,以及为什么 platform signature 不等于 privileged; - 测试如何证明路径模型、flags、overlay policy 和恢复逻辑的不同部分。
建议先阅读 系统包扫描 了解启动阶段,scanDirTracedLI 了解单目录提交,system与priv-app 了解系统应用身份,以及 data-app扫描 了解用户包恢复。本文把焦点放在“分区来源如何成为 package policy”,不重复这些文章的基础流程。
1. 分区模型
1.1 分区顺序
源码文件:frameworks/base/core/java/android/content/pm/PackagePartitions.java
/**
* Exposes {@link #SYSTEM_PARTITIONS} which represents the partitions in which application packages
* can be installed. The partitions are ordered from most generic (lowest priority) to most specific
* (greatest priority).
*/
public class PackagePartitions {
public static final int PARTITION_SYSTEM = 0;
public static final int PARTITION_VENDOR = 1;
public static final int PARTITION_ODM = 2;
public static final int PARTITION_OEM = 3;
public static final int PARTITION_PRODUCT = 4;
public static final int PARTITION_SYSTEM_EXT = 5;源码把分区排列为 system、vendor、odm、oem、product、system_ext,并明确说明顺序是从 generic/低优先级到 specific/高优先级。这个顺序被 getOrderedPartitions() 原样保留,随后 InitAppsHelper 对 overlay 反向收集,对 app/priv-app 正向收集。
1.2 子目录模型
源码文件:frameworks/base/core/java/android/content/pm/PackagePartitions.java
private static final ArrayList<SystemPartition> SYSTEM_PARTITIONS =
new ArrayList<>(Arrays.asList(
new SystemPartition(Environment.getRootDirectory(),
PARTITION_SYSTEM, Partition.PARTITION_NAME_SYSTEM,
true /* containsPrivApp */, false /* containsOverlay */),
new SystemPartition(Environment.getVendorDirectory(),
PARTITION_VENDOR, Partition.PARTITION_NAME_VENDOR,
true /* containsPrivApp */, true /* containsOverlay */),
new SystemPartition(Environment.getOdmDirectory(),
PARTITION_ODM, Partition.PARTITION_NAME_ODM,
true /* containsPrivApp */, true /* containsOverlay */),
new SystemPartition(Environment.getOemDirectory(),
PARTITION_OEM, Partition.PARTITION_NAME_OEM,
false /* containsPrivApp */, true /* containsOverlay */),
new SystemPartition(Environment.getProductDirectory(),
PARTITION_PRODUCT, Partition.PARTITION_NAME_PRODUCT,
true /* containsPrivApp */, true /* containsOverlay */),
new SystemPartition(Environment.getSystemExtDirectory(),
PARTITION_SYSTEM_EXT, Partition.PARTITION_NAME_SYSTEM_EXT,
true /* containsPrivApp */, true /* containsOverlay */)));每个 SystemPartition 都有 app 目录;是否有 priv-app 和 overlay 由构造参数决定。Android 17 的真实表格是:
| 分区 | app | priv-app | overlay |
|---|---|---|---|
| system | 有 | 有 | 无 |
| vendor | 有 | 有 | 有 |
| odm | 有 | 有 | 有 |
| oem | 有 | 无 | 有 |
| product | 有 | 有 | 有 |
| system_ext | 有 | 有 | 有 |
因此 /oem/priv-app 不会通过 getPrivAppFolder() 进入扫描参数列表。文章或调试脚本如果把所有分区都假定为“app + priv-app + overlay”,会直接得到错误的扫描结论。
1.3 懒 canonical 化
源码文件:frameworks/base/core/java/android/content/pm/PackagePartitions.java
private SystemPartition(@NonNull File folder, @PartitionType int type, String name,
boolean containsPrivApp, boolean containsOverlay) {
this.type = type;
this.mName = name;
this.mFolder = new DeferredCanonicalFile(folder);
this.mAppFolder = new DeferredCanonicalFile(folder, "app");
this.mPrivAppFolder = containsPrivApp
? new DeferredCanonicalFile(folder, "priv-app") : null;
this.mOverlayFolder = containsOverlay
? new DeferredCanonicalFile(folder, "overlay") : null;
this.mNonConicalFolder = folder;
}分区构造时不是立即调用 File.getCanonicalFile()。源码注释指出,延迟 canonical 化是为了避免进程在没有正确 SELinux policy 时访问不应访问的目录。实际调用 getAppFolder()、containsFile() 等 API 时才解析 canonical path。
1.4 路径包含
源码文件:frameworks/base/core/java/android/content/pm/PackagePartitions.java
public boolean containsFile(@NonNull File file) {
return FileUtils.contains(mFolder.getFile(), canonicalize(file));
}
public boolean containsPrivApp(@NonNull File scanFile) {
return mPrivAppFolder != null
&& FileUtils.contains(mPrivAppFolder.getFile(), canonicalize(scanFile));
}
public boolean containsApp(@NonNull File scanFile) {
return mAppFolder != null
&& FileUtils.contains(mAppFolder.getFile(), canonicalize(scanFile));
}
public boolean containsOverlay(@NonNull File scanFile) {
return mOverlayFolder != null
&& FileUtils.contains(mOverlayFolder.getFile(), canonicalize(scanFile));
}这些 API 用 FileUtils.contains() 做目录包含判断,而不是手工拼字符串。它们同时处理 canonical path 和“某分区没有该子目录”的 null 情况。PMS 的分区身份来自这个路径模型,后续 flag 恢复也依赖它。
2. ScanPartition
2.1 类型映射
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPartition.java
public ScanPartition(@NonNull PackagePartitions.SystemPartition partition) {
super(partition);
scanFlag = scanFlagForPartition(partition);
apexInfo = null;
}
private static int scanFlagForPartition(PackagePartitions.SystemPartition partition) {
switch (partition.type) {
case PackagePartitions.PARTITION_SYSTEM:
return 0;
case PackagePartitions.PARTITION_VENDOR:
return SCAN_AS_VENDOR;
case PackagePartitions.PARTITION_ODM:
return SCAN_AS_ODM;
case PackagePartitions.PARTITION_OEM:
return SCAN_AS_OEM;
case PackagePartitions.PARTITION_PRODUCT:
return SCAN_AS_PRODUCT;
case PackagePartitions.PARTITION_SYSTEM_EXT:
return SCAN_AS_SYSTEM_EXT;
default:
throw new IllegalStateException("Unable to determine scan flag for "
+ partition.getFolder());
}
}system 分区以 0 作为额外 flag;其他分区各有一个独立 bit。它们不会替换 SCAN_AS_SYSTEM,而是和 system、booting、initial、privileged 等通用 flags 组合。
2.2 APEX继承
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPartition.java
public ScanPartition(@NonNull File folder, @NonNull ScanPartition original,
@Nullable ApexManager.ActiveApexInfo apexInfo) {
super(folder, original);
var scanFlags = original.scanFlag;
this.apexInfo = apexInfo;
if (apexInfo != null) {
// ScanPartitionUtils.isApkInUpdatedApex() relies on this combination.
scanFlags |= SCAN_AS_APK_IN_APEX;
if (apexInfo.isFactory) {
scanFlags |= SCAN_AS_FACTORY;
}
if (apexInfo.activeApexChanged) {
scanFlags |= SCAN_DROP_CACHE;
}
}
this.scanFlag = scanFlags;
}APEX 派生分区先复制原始分区 flag,再增加 APEX 语义:
SCAN_AS_APK_IN_APEX:输入是 APEX 内 APK;SCAN_AS_FACTORY:当前 APEX 是 factory 版本;SCAN_DROP_CACHE:active APEX 变化,需要丢弃对应 parser cache。
所以 /product 中的 APEX 内 APK 不是只带 SCAN_AS_APK_IN_APEX,它还继承 SCAN_AS_PRODUCT。
2.3 PMS反向识别
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
@ScanFlags
int getSystemPackageScanFlags(File codePath) {
List<ScanPartition> dirsToScanAsSystem =
mInitAppsHelper.getDirsToScanAsSystem();
@PackageManagerService.ScanFlags int scanFlags = SCAN_AS_SYSTEM;
for (int i = dirsToScanAsSystem.size() - 1; i >= 0; i--) {
ScanPartition partition = dirsToScanAsSystem.get(i);
if (partition.containsFile(codePath)) {
scanFlags |= partition.scanFlag;
if (partition.containsPrivApp(codePath)) {
scanFlags |= SCAN_AS_PRIVILEGED;
}
break;
}
}
return scanFlags;
}更新或重扫描时,PMS 从 code path 反向恢复分区 flags:默认先是 system,再根据 path 所属 partition 加 vendor/product/system_ext 等 bit,最后判断是否位于 priv-app。这个方法遍历的是 mInitAppsHelper.getDirsToScanAsSystem(),因此 APEX 派生分区也能参与识别。
2.4 重扫描 flags
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
Pair<Integer, Integer> getSystemPackageRescanFlagsAndReparseFlags(
File scanFile, int systemScanFlags, int systemParseFlags) {
List<ScanPartition> dirsToScanAsSystem =
mInitAppsHelper.getDirsToScanAsSystem();
@ParsingPackageUtils.ParseFlags int reparseFlags = 0;
@PackageManagerService.ScanFlags int rescanFlags = 0;
for (int i1 = dirsToScanAsSystem.size() - 1; i1 >= 0; i1--) {
final ScanPartition partition = dirsToScanAsSystem.get(i1);
if (partition.containsPrivApp(scanFile)) {
reparseFlags = systemParseFlags;
rescanFlags = systemScanFlags | SCAN_AS_PRIVILEGED
| partition.scanFlag;
break;
}
if (partition.containsApp(scanFile)) {
reparseFlags = systemParseFlags;
rescanFlags = systemScanFlags | partition.scanFlag;
break;
}
}
return new Pair<>(rescanFlags, reparseFlags);
}这个重载同时返回 reparse flags 和 rescan flags。它优先识别 priv-app,再识别 app;如果路径不在任何已知 app/priv-app 目录中,两个结果保持 0,调用方必须把它当作未识别路径处理,而不能默认它是 system app。
3. 收集顺序
3.1 Overlay倒序
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java
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);
}PackagePartitions 是 generic 到 specific,overlay 却从列表尾部向前收集。这个顺序使更 specific 分区的 overlay 先加入参数列表,但最终资源优先级还要结合 OverlayConfig/OMS 的 priority 和 partition order,不能把“先收集”简单等同于“必然覆盖”。
3.2 framework与app
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java
File frameworkDir = new File(Environment.getRootDirectory(), "framework");
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);
}framework 在所有分区 app/priv-app 之前加入;应用目录按 partition 列表正序加入。每个分区的 priv-app 在同一分区 app 之前。参数列表保存这个优先级,真正的 parse 可以并行,但 system package 注册必须按有序结果执行。
3.3 APEX与分区组合
活跃 APEX 通过 InitAppsHelper.getApexScanPartitions() 映射到原始分区,再以派生 ScanPartition 参加 overlay/app/priv-app 参数收集。一个 APEX 内 APK 的最终 scan context 由三部分组成:通用 system flags、原始 partition flag、APEX-specific flags。调试 APEX 内 package 时必须把这三组分开看。
4. Overlay策略
4.1 系统分区overlay
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
private void assertOverlayIsValid(AndroidPackage pkg,
@ParsingPackageUtils.ParseFlags int parseFlags,
@PackageManagerService.ScanFlags int scanFlags) throws PackageManagerException {
if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
// System overlay: first scan or update to a system overlay.
if ((parseFlags & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR) == 0) {
// This must be an update to a system overlay.
if (!mPm.isOverlayMutable(pkg.getPackageName())) {
throw PackageManagerException.ofInternalError("Overlay "
+ pkg.getPackageName() + " is static and cannot be upgraded.",
PackageManagerException.INTERNAL_ERROR_SYSTEM_OVERLAY_STATIC);
}
} else if ((scanFlags & (SCAN_AS_VENDOR | SCAN_AS_ODM)) != 0) {
if (pkg.getTargetSdkVersion() < ScanPackageUtils.getVendorPartitionVersion()) {
Slog.w(TAG, "System overlay " + pkg.getPackageName()
+ " targets an SDK below the required SDK level of vendor overlays");
}
} else if (pkg.getTargetSdkVersion() < Build.VERSION.SDK_INT) {
Slog.w(TAG, "System overlay " + pkg.getPackageName()
+ " targets an SDK below the required SDK level of system overlays");
}
}
}system overlay 分支先看 SCAN_AS_SYSTEM,再看 PARSE_IS_SYSTEM_DIR:
- system-dir 首次扫描按 system/vendor/odm 目标 SDK 规则检查;
- vendor/odm 使用
ro.vndk.version解析出的 vendor partition version; - 其他 system overlay 使用
Build.VERSION.SDK_INT; - data 更新的 static system overlay 如果不可 mutable,直接抛内部错误。
Android 17 当前低 target SDK 路径是 warning,并注明未来可能升级为 install error,不能把 warning 误写成当前硬失败。
4.2 Vendor版本
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.java
public static int getVendorPartitionVersion() {
final String version = SystemProperties.get("ro.vndk.version");
if (!version.isEmpty()) {
try {
return Integer.parseInt(version);
} catch (NumberFormatException ignore) {
if (ArrayUtils.contains(Build.VERSION.ACTIVE_CODENAMES, version)) {
return Build.VERSION_CODES.CUR_DEVELOPMENT;
}
}
}
return Build.VERSION_CODES.P;
}ro.vndk.version 可能是数字,也可能是当前 active codename;无法解析时回退到 P。这个值只服务于 vendor/odm overlay target SDK policy,不是所有 vendor app 的 target SDK,也不是 product/system_ext 的统一最低版本。
4.3 非预装 overlay
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if ((scanFlags & SCAN_AS_SYSTEM) == 0) {
// A non-preloaded overlay must target Q or later, or be platform signed.
if (pkg.getTargetSdkVersion() < Build.VERSION_CODES.Q) {
final PackageSetting platformPkgSetting;
synchronized (mPm.mLock) {
platformPkgSetting = mPm.mSettings.getPackageLPr("android");
}
if (!comparePackageSignatures(platformPkgSetting, pkg.getSigningDetails())) {
throw PackageManagerException.ofInternalError("Overlay "
+ pkg.getPackageName()
+ " must target Q or later, or be signed with the platform certificate",
PackageManagerException.INTERNAL_ERROR_OVERLAY_LOW_TARGET_SDK);
}
}
}非预装 overlay 低于 Q 时,只有 platform certificate 才能通过。这个分支和预装 vendor/product overlay 的规则不同:预装 overlay 依赖分区和 system scan context,非预装 overlay 依赖 target SDK/platform signature。
4.4 targetName签名
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if (pkg.getOverlayTargetOverlayableName() == null) {
final PackageSetting targetPkgSetting;
synchronized (mPm.mLock) {
targetPkgSetting = mPm.mSettings.getPackageLPr(pkg.getOverlayTarget());
}
if (targetPkgSetting != null
&& !comparePackageSignatures(targetPkgSetting, pkg.getSigningDetails())) {
if (mPm.mOverlayConfigSignaturePackage == null) {
throw PackageManagerException.ofInternalError("Overlay "
+ pkg.getPackageName() + " and target " + pkg.getOverlayTarget()
+ " signed with different certificates",
PackageManagerException.INTERNAL_ERROR_OVERLAY_SIGNATURE1);
}
final PackageSetting refPkgSetting;
synchronized (mPm.mLock) {
refPkgSetting = mPm.mSettings.getPackageLPr(
mPm.mOverlayConfigSignaturePackage);
}
if (!comparePackageSignatures(refPkgSetting, pkg.getSigningDetails())) {
throw PackageManagerException.ofInternalError("Overlay "
+ pkg.getPackageName() + " signed with a different certificate",
PackageManagerException.INTERNAL_ERROR_OVERLAY_SIGNATURE2);
}
}
}没有 overlay targetName 时,overlay 需要和 target package 或 SystemConfig 指定的 reference package 使用兼容签名。分区 flag 只决定进入哪种 overlay policy,不会替代 target/reference 的签名检查。
5. 状态与SELinux
5.1 分区状态写入
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.java
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);这些 setter 先改变 ParsedPackage,之后 PackageSetting 和 PackageInfoUtils 才能把它们转成持久化/查询侧字段。SCAN_AS_VENDOR 不等于 SCAN_AS_SYSTEM,但在启动系统分区扫描中二者通常一起出现;读取某个运行时 package state 时要分别检查。
5.2 SELinux分区
源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java
if (isPrivileged) {
seInfo += PRIVILEGED_APP_STR;
}
seInfo += TARGETSDKVERSION_STR + targetSdkVersion;
String partition = getPartition(packageState);
if (!partition.isEmpty()) {
seInfo += PARTITION_STR + partition;
}PMS 生成的 seInfo 可能同时包含 privileged、target SDK 和 partition 标签。vendor/product/system_ext package 的 SELinux 进程域因此不能只靠 isSystem() 判断;需要把 PackageState 的分区身份、privileged 状态和 target SDK 一起追到 SELinuxMMAC.getSeInfo()。
5.3 签名与privileged
源码文件: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() 是两个独立结果。一个 /product/app 包可能 platform signed 但不在 priv-app;一个 /vendor/priv-app 包仍须通过 privileged permission allowlist 和签名/分区规则。调试权限时必须同时检查 package private flags、permission protection level 和 allowlist。
5.4 设置写回
源码文件: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);
pkgSetting.setPrivateFlags(pkgPrivateFlags);Settings 更新时先清除旧 FLAG_SYSTEM,再按本次扫描 policy 写入,避免同一 package 在不同分区或 system/data 两次扫描后累积错误身份。private flags 也包含 privileged/vendor/product/system_ext 等分区结果,最终被 ApplicationInfo 派生使用。
6. 测试与诊断
6.1 分区路径测试
测试文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
@Test
public void testPartitions() {
String[] partitions = {"system", "vendor", "odm", "oem", "product", "system_ext"};
String[] appdir = {"app", "priv-app"};
for (int i = 0; i < partitions.length; i++) {
final ScanPartition scanPartition =
PackageManagerService.SYSTEM_PARTITIONS.get(i);
for (int j = 0; j < appdir.length; j++) {
File path = new File(String.format("%s/%s/A.apk", partitions[i], appdir[j]));
Assert.assertEquals(j == 1 && i != 3,
scanPartition.containsPrivApp(path));
final int scanFlag = scanPartition.scanFlag;
Assert.assertEquals(i == 1,
scanFlag == PackageManagerService.SCAN_AS_VENDOR);
Assert.assertEquals(i == 2,
scanFlag == PackageManagerService.SCAN_AS_ODM);
Assert.assertEquals(i == 3,
scanFlag == PackageManagerService.SCAN_AS_OEM);
Assert.assertEquals(i == 4,
scanFlag == PackageManagerService.SCAN_AS_PRODUCT);
Assert.assertEquals(i == 5,
scanFlag == PackageManagerService.SCAN_AS_SYSTEM_EXT);
}
}
}输入是六个分区的 app/priv-app 伪路径。断言证明 /oem/priv-app 不被识别为 priv-app,且其他分区的 scanFlag 映射正确。它没有证明目录真实存在、APK 能解析或 policy 已写入 PackageSetting。
6.2 Overlay测试
overlay 测试应把输入拆成以下场景:
- system/vendor/odm 预装 overlay,target SDK 低于当前要求,断言当前代码只 warning;
- 非预装 overlay 低于 Q 且非 platform signed,断言
INTERNAL_ERROR_OVERLAY_LOW_TARGET_SDK; - data 更新 static system overlay 且
isOverlayMutable()为 false,断言INTERNAL_ERROR_SYSTEM_OVERLAY_STATIC; - targetName 缺失且 target/reference package 签名都不匹配,断言对应 overlay signature error。
测试必须检查异常类型/错误码与 warning 的差异,不能只断言“overlay 失败”。
6.3 重扫描测试
对 getSystemPackageScanFlags() 和 getSystemPackageRescanFlagsAndReparseFlags(),输入应覆盖 system app、vendor priv-app、product app、system_ext priv-app、APEX 内 APK 和未知路径。断言 flags 包含 system、对应 partition、必要时 privileged、APEX/factory/drop-cache;未知路径应保持未识别结果。
6.4 dumpsys对照
adb shell dumpsys package <package-name>
adb shell dumpsys package apex
adb shell logcat -s PackageManager PackageManagerService对同一个 vendor/product/system_ext package,至少对照:
- codePath 和实际分区;
FLAG_SYSTEM、PRIVATE_FLAG_VENDOR/PRODUCT/SYSTEM_EXT/PRIVILEGED;seInfo中的 partition/target SDK/privileged 标记;- overlay target、签名错误或 target SDK warning;
- APEX 内 package 的 module、active/factory 状态;
- updated system app 的 active/disabled setting。
路径、private flag、SELinux 和 overlay policy 是不同 owner 的输出,不能互相替代。
6.5 真实启动验证
要证明跨分区扫描主线,建议组合:
- 启动 trace 中 overlay 倒序、framework、priv-app/app 正序的参数列表;
addForInitLI()处理结果中的 partition flags;dumpsys package的 package state 和seInfo;- overlay target SDK/signature 日志;
- OTA/update 时 code path 反向恢复的 scan flags;
/oem/priv-app路径不被识别为 privileged 的测试;- APEX 内 APK 继承 vendor/product/system_ext flag 的扫描结果。
7. 阅读路线
读一个 vendor/product/system_ext package 的扫描问题时,按下面顺序追:
- 先看
PackagePartitions.SYSTEM_PARTITIONS,确认该分区实际是否有 priv-app/overlay。 - 进入
ScanPartition.scanFlagForPartition(),记录 partition type 对应的 flag。 - 在
InitAppsHelper.scanSystemDirs()中确认目录收集顺序、SCAN_AS_PRIVILEGED和 APEX context。 - 进入
InstallPackageHelper/ScanPackageUtils.applyPolicy(),确认 ParsedPackage 的 system、privileged、partition、APEX 属性。 - 如果是 overlay,进入
assertOverlayIsValid(),区分预装、vendor/odm、非预装、target SDK、mutable 和签名分支。 - 如果是更新/恢复,进入
getSystemPackageScanFlags()或 rescan flags 方法,检查 path containment 的反向结果。 - 进入 Settings、PackageInfoUtils 和 SELinuxMMAC,确认 PackageSetting、ApplicationInfo、seInfo 的最终状态。
- 用分区路径测试、overlay policy 测试、dumpsys 和启动日志分别验证“路径模型、flags、权限策略、运行时安全域”。
8. 分区链路
Android 17 的多分区扫描把路径来源、package policy 和运行时安全状态连成一条链:
PackagePartitions定义分区顺序和子目录能力;/oem没有 priv-app,不能按统一目录模板推断。ScanPartition将 vendor、odm、oem、product、system_ext 转成 partition flags;APEX 内 APK 继承原始分区 flag,再附加 APEX/factory/cache 语义。- PMS 对 overlay 倒序收集,对 framework 和 app/priv-app 正序收集;解析可以并行,状态注册保留参数列表顺序。
applyPolicy()将 scan flags 写入 ParsedPackage 的 system、privileged、partition、APEX 和 platform signature 属性;platform signature 与 privileged 是独立结果。- system/vendor/odm overlay 的 target SDK、非预装 overlay 的 Q/platform signature、mutable static overlay 和 target/reference signature 属于不同 policy 分支,当前 Android 17 的 warning 不能写成硬失败。
- PMS 能通过 canonical path containment 反向恢复 system、privileged 和分区 flags,更新/重扫描不依赖字符串前缀猜测。
- 分区/private flags 会影响 PackageSetting、ApplicationInfo 和 SELinux
seInfo;最终诊断必须同时对照 codePath、flags、seInfo、overlay policy 和 APEX 状态。 - 路径测试只能证明模型,overlay 测试只能证明 policy,dumpsys 只能证明当前状态;完整结论需要把源码阶段、日志、状态和测试范围串起来。
后续的 ScanRequest 与 ScanResult 专题会把本文看到的扫描输入和输出收束到一次请求,继续分析旧 PackageSetting、disabled system setting、shared user、签名、flags、ABI 和共享库结果如何在扫描过程中传递。
