Skip to content

system与priv-app

解析 system、priv-app 扫描参数、特权标志、更新系统应用和权限边界的真实源码。

基于android-17.0.0_r1
AndroidPackageManagerServicesystem-apppriv-appScanPackageUtils源码阅读

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

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 flagsscan flags 额外项结果含义
overlaymSystemParseFlagspartition flagsystem overlay,后续执行 overlay policy
frameworkmSystemParseFlagsSCAN_NO_DEX、SCAN_AS_PRIVILEGEDframework 核心包,禁止普通 dex 扫描路径
priv-appmSystemParseFlagsSCAN_AS_PRIVILEGED、partition flagsystem + privileged app
appmSystemParseFlagspartition flagsystem 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 真实设备检查 ​

建议按这条顺序验证:

  1. dumpsys package <package> 记录 codePath、flags/privateFlags 和 disabled system package;
  2. 对同一包检查 /system/priv-app、/system/app 或 /data/app 的实际来源;
  3. 查看启动 log 中 Failed to scan、Failed to parse、shared-user privileged error 和 APEX 内 APK error;
  4. 对 privileged permission 查看 privapp-permissions allowlist 和 PermissionManager 日志;
  5. OTA/update 场景比较 active package 与 disabled system package 的 version/path/signature;
  6. shared UID 场景检查 group 中所有成员的 privileged 标志和 platform signature。

7. 测试与证明范围 ​

7.1 Settings测试 ​

源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java

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

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 扫描问题时,按以下顺序追源码:

  1. 从 InitAppsHelper.scanSystemDirs() 确认目录、parse flags、scan flags 和 partition flag。
  2. 进入 ScanParams/InstallPackageHelper.installPackagesFromDir(),确认是否携带 APEX context、是否 drop cache。
  3. 进入 processParseResult() 和 addForInitLI(),记录 parse failure、registration failure、system/data cleanup 分支。
  4. 进入 ScanPackageUtils.applyPolicy(),逐项确认 system、privileged、partition、stub、direct boot 和 platform signature 属性。
  5. 若是更新 system app,进入 adjustScanFlagsWithPackageSetting()、disabled system setting 和 scanPackageForInitLI()。
  6. 若涉及权限,分别检查 package privileged 标志、permission protection level、privapp allowlist 和 shared UID 一致性。
  7. 回到 Settings/PackageInfoUtils/dumpsys 对照最终 flags/privateFlags、path 和 per-user state。
  8. 最后用 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 与普通用户包的分叉。