Skip to content

vendor与product扫描

解析 vendor、odm、oem、product、system_ext 分区的路径建模、扫描 flags、overlay 策略和状态恢复。

基于android-17.0.0_r1
AndroidPackageManagerServicevendorproductsystem_extoverlay源码阅读

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

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

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 的真实表格是:

分区apppriv-appoverlay
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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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对照 ​

text
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 真实启动验证 ​

要证明跨分区扫描主线,建议组合:

  1. 启动 trace 中 overlay 倒序、framework、priv-app/app 正序的参数列表;
  2. addForInitLI() 处理结果中的 partition flags;
  3. dumpsys package 的 package state 和 seInfo;
  4. overlay target SDK/signature 日志;
  5. OTA/update 时 code path 反向恢复的 scan flags;
  6. /oem/priv-app 路径不被识别为 privileged 的测试;
  7. APEX 内 APK 继承 vendor/product/system_ext flag 的扫描结果。

7. 阅读路线 ​

读一个 vendor/product/system_ext package 的扫描问题时,按下面顺序追:

  1. 先看 PackagePartitions.SYSTEM_PARTITIONS,确认该分区实际是否有 priv-app/overlay。
  2. 进入 ScanPartition.scanFlagForPartition(),记录 partition type 对应的 flag。
  3. 在 InitAppsHelper.scanSystemDirs() 中确认目录收集顺序、SCAN_AS_PRIVILEGED 和 APEX context。
  4. 进入 InstallPackageHelper/ScanPackageUtils.applyPolicy(),确认 ParsedPackage 的 system、privileged、partition、APEX 属性。
  5. 如果是 overlay,进入 assertOverlayIsValid(),区分预装、vendor/odm、非预装、target SDK、mutable 和签名分支。
  6. 如果是更新/恢复,进入 getSystemPackageScanFlags() 或 rescan flags 方法,检查 path containment 的反向结果。
  7. 进入 Settings、PackageInfoUtils 和 SELinuxMMAC,确认 PackageSetting、ApplicationInfo、seInfo 的最终状态。
  8. 用分区路径测试、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 和共享库结果如何在扫描过程中传递。