Skip to content

MAC Permissions

追踪 mac_permissions.xml 从证书匹配、分区构建到 seinfo、seapp_contexts、Zygote 和 app data 的完整链路。

基于android-17.0.0_r1
AndroidSELinuxmac_permissionsseinfoPackageManager源码阅读

MAC Permissions ​

本文面向已经读过 安全上下文、seapp_contexts 和 Keystore2 Key Contexts 的读者。这里的 MAC permissions 是 Android Package Manager 使用的一套“证书集合 → seinfo 字符串”规则;它不直接产生 u:r:platform_app:s0,也不等价于 Linux 文件权限。

本文要解决的是一个可追踪问题:一个 APK 安装后,签名证书如何经过 mac_permissions.xml,变成保存于 Package Manager 状态中的 seInfo,又如何在创建 app data 和启动进程时被不同消费者使用。文章不展开 APK 签名格式、所有 seapp_contexts 排序规则或每个应用域的 TE 权限;只在这些对象决定当前链路时引用它们。

1. 标签边界 ​

1.1 三个字符串 ​

同一应用在源码中会出现三个容易混淆的值:

值例子产生者消费者
证书摘要3082...APK signing details / insertkeys.pyPolicy.getMatchedSeInfo()
seInfoplatform:targetSdkVersion=35:partition=systemSELinuxMMAC.getSeInfo()PackageState、ApplicationInfo
最终 domainu:r:platform_app:s0libselinux seapp_context_lookup()Zygote 创建的进程

mac_permissions.xml 只负责中间值 seinfo。seapp_contexts 再结合 user、包名、是否特权、target SDK 等 selector 选择 domain 和 app data type。由此可见,“平台签名应用进入 platform_app”不是单文件结论:至少还需要包的 UID/user、包名、privileged 状态和 seapp_contexts 规则共同满足。

图中的 E 是共享状态,G/H/I 是三个不同消费者:libselinux 使用完整 selector 决定 domain;Installer 使用同一个字符串创建目录;Zygote 使用完整字符串设置进程启动参数。三者都读取 seInfo,但不会互相替代。

1.2 文件形态 ​

Android 17 platform 文件只有 6 个全局 signer:@PLATFORM、@SDK_SANDBOX、@BLUETOOTH、@MEDIA、@NETWORK_STACK、@NFC。vendor 文件只声明一个 platform signer。<package> 和多证书 <cert> 是解析器支持的语法边界,不代表当前 platform 文件一定使用了这些形式。

源码文件:system/sepolicy/private/mac_permissions.xml

xml
<?xml version="1.0" encoding="utf-8"?>
<policy>
    <!-- The signer certificate is replaced from keys.conf during the build. -->
    <signer signature="@PLATFORM">
        <seinfo value="platform" />
    </signer>
    <signer signature="@SDK_SANDBOX">
        <seinfo value="sdk_sandbox" />
    </signer>
    <signer signature="@BLUETOOTH">
        <seinfo value="bluetooth" />
    </signer>
    <signer signature="@MEDIA">
        <seinfo value="media" />
    </signer>
    <signer signature="@NETWORK_STACK">
        <seinfo value="network_stack" />
    </signer>
    <signer signature="@NFC">
        <seinfo value="nfc" />
    </signer>
</policy>

2. 构建输入 ​

2.1 keys.conf ​

源码文件:system/sepolicy/private/keys.conf

ini
[@PLATFORM]
ALL : $DEFAULT_SYSTEM_DEV_CERTIFICATE/platform.x509.pem

[@SDK_SANDBOX]
ALL : $MAINLINE_SEPOLICY_DEV_CERTIFICATES/sdk_sandbox.x509.pem

[@BLUETOOTH]
ALL : $MAINLINE_BLUETOOTH_SEPOLICY_DEV_CERTIFICATES/bluetooth.x509.pem

[@NETWORK_STACK]
ALL : $MAINLINE_SEPOLICY_DEV_CERTIFICATES/networkstack.x509.pem

[@NFC]
ALL : $MAINLINE_NFC_SEPOLICY_DEV_CERTIFICATES/nfc.x509.pem

keys.conf 的 section 名就是 XML 中的 @TAG;右侧路径在构建时通过环境变量解析到 PEM。ALL 表示该 tag 对当前 build variant 都可用。证书文件不是运行时由 Package Manager 读取的输入,它们在构建阶段被转换为 XML 中的十六进制证书摘要。

2.2 Soong变体 ​

源码文件:system/sepolicy/mac_permissions/Android.bp

make
se_build_files {
    name: "keys.conf",
    srcs: ["keys.conf"], // Partition-specific inputs are selected by the filegroup.
}

se_build_files {
    name: "mac_permissions.xml",
    srcs: ["mac_permissions.xml"],
}

mac_permissions {
    name: "plat_mac_permissions.xml",
    keys: [
        ":keys.conf{.plat_private}",
        ":keys.conf{.system_ext_private}",
        ":keys.conf{.product_private}",
    ],
    srcs: [":mac_permissions.xml{.plat_private}"],
}

mac_permissions {
    name: "vendor_mac_permissions.xml",
    keys: [
        ":keys.conf{.plat_vendor}",
        ":keys.conf{.vendor}",
        ":keys.conf{.reqd_mask}",
    ],
    srcs: [
        ":mac_permissions.xml{.plat_vendor}",
        ":mac_permissions.xml{.vendor}",
        ":mac_permissions.xml{.reqd_mask}",
    ],
    vendor: true,
}

system_ext、product 和 odm 也有独立 module。每个 module 的 srcs 和 keys 都是分区 variant 的组合;这使 vendor XML 可以自包含 platform signer,而不依赖运行时加载 system 文件来解释 vendor APK 的签名。

2.3 插入证书 ​

源码文件:system/sepolicy/build/soong/mac_permissions.go

go
var (
    // This list must stay in sync with the certificate names in keys.conf.
    AllPlatformKeys = []string{
        "platform", "sdk_sandbox", "media", "networkstack",
        "shared", "testkey", "bluetooth", "nfc",
    }
)

func (m *macPermissionsModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
    platformKeys := getAllKeyPaths(ctx, ctx.Config().DefaultAppCertificateDir(ctx))
    mainlineKeys := getAllKeyPaths(ctx, ctx.Config().MainlineSepolicyDevCertificatesDir(ctx))
    bluetoothKeys := getAllKeyPaths(
        ctx, ctx.Config().MainlineBluetoothSepolicyDevCertificatesDir(ctx))
    keys := android.PathsForModuleSrc(ctx, m.properties.Keys)
    srcs := android.PathsForModuleSrc(ctx, m.properties.Srcs)
    // M4 expands keys.conf before insertkeys replaces XML @TAG values.
    // ...
}

真正的规则链是 m4(keys.conf) → insertkeys(keys.conf, mac_permissions.xml) → 分区安装。getAllKeyPaths() 为默认 system certificate、Mainline certificate 和 Bluetooth certificate 建立 implicit dependencies,因此证书文件变化会触发 XML 重新生成。insertkeys.py 的输出带有 AUTOGENERATED FILE DO NOT MODIFY,说明设备上安装的 XML 不是手工编辑源。

源码文件:system/sepolicy/tools/insertkeys.py

python
class ReplaceTags(handler.ContentHandler):
    def startElement(self, tag, attrs):
        self._out.write('<' + tag)
        for (name, value) in attrs.items():
            if name == "signature" and value in self._keyMap:
                # Replace @TAG with the lowercase hex certificate digest.
                for key in self._keyMap[value].getBase16Keys():
                    self._out.write(' %s="%s"' % (name, saxutils.escape(key)))
            else:
                self._out.write(' %s="%s"' % (name, saxutils.escape(value)))
        if tag in self.TAGS_WITH_CHILDREN:
            self._out.write('>')
        else:
            self._out.write('/>')

insertkeys.py 不改变 XML 结构,只替换 signature 属性;如果 tag 找不到,属性会原样输出,后续运行时匹配便不会得到该证书。脚本还强制每个 PEM 只包含一个证书,因为 Package Manager 的 policy stanza 以证书集合比较,但这个生成工具不允许一个 tag 隐含多个证书。

3. 解析器 ​

3.1 分区文件加载 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
private static final List<Policy> sPolicies = new ArrayList<>();
// This flag and the list lock policy load and lookup together.
private static boolean sPolicyRead;
private static final List<File> sMacPermissions = new ArrayList<>();

static {
    sMacPermissions.add(new File(
            Environment.getRootDirectory(), "/etc/selinux/plat_mac_permissions.xml"));

    File systemExt = new File(Environment.getSystemExtDirectory(),
            "/etc/selinux/system_ext_mac_permissions.xml");
    if (systemExt.exists()) {
        sMacPermissions.add(systemExt);
    }

    File product = new File(Environment.getProductDirectory(),
            "/etc/selinux/product_mac_permissions.xml");
    if (product.exists()) {
        sMacPermissions.add(product);
    }

    File vendor = new File(Environment.getVendorDirectory(),
            "/etc/selinux/vendor_mac_permissions.xml");
    if (vendor.exists()) {
        sMacPermissions.add(vendor);
    }
}

platform 文件是必需输入;system_ext、product、vendor 和 odm 按文件是否存在加入列表。这里的顺序决定跨分区 policy 列表的初始顺序,但随后 PolicyComparator 会把 package refinement 排在 global seinfo 前面。缺少可选文件不会直接失败,XML 结构错误或 I/O 错误才会让加载返回 false。

3.2 原子装载 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
public static boolean readInstallPolicy() {
    synchronized (sPolicies) {
        if (sPolicyRead) {
            return true;
        }
    }

    // Parse into a temporary list so readers never observe partial policy.
    List<Policy> policies = new ArrayList<>();
    XmlPullParser parser = Xml.newPullParser();
    for (File macPermission : sMacPermissions) {
        FileReader policyFile = null;
        try {
            policyFile = new FileReader(macPermission);
            parser.setInput(policyFile);
            parser.nextTag();
            parser.require(XmlPullParser.START_TAG, null, "policy");
            while (parser.next() != XmlPullParser.END_TAG) {
                if (parser.getEventType() == XmlPullParser.START_TAG
                        && "signer".equals(parser.getName())) {
                    policies.add(readSignerOrThrow(parser));
                } else if (parser.getEventType() == XmlPullParser.START_TAG) {
                    skip(parser);
                }
            }
        } catch (IllegalStateException | IllegalArgumentException |
                 XmlPullParserException | IOException ex) {
            Slog.e(TAG, "Exception while parsing " + macPermission, ex);
            return false;
        } finally {
            IoUtils.closeQuietly(policyFile);
        }
    }

    PolicyComparator policySort = new PolicyComparator();
    Collections.sort(policies, policySort);
    synchronized (sPolicies) {
        sPolicies.clear();
        sPolicies.addAll(policies);
        sPolicyRead = true;
    }
    return true;
}

源码中的解析列表是临时对象,只有所有文件成功解析、排序完成后才替换共享 sPolicies 并置 sPolicyRead=true。所以失败不会留下“前半个 XML 已生效”的状态;调用方必须处理 false。该方法没有热更新入口,正常运行期间的读取只在首次成功加载后短路返回。

3.3 signer结构 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
private static Policy readSignerOrThrow(XmlPullParser parser)
        throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, null, "signer");
    Policy.PolicyBuilder pb = new Policy.PolicyBuilder();

    String cert = parser.getAttributeValue(null, "signature");
    if (cert != null) {
        pb.addSignature(cert);
    }

    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        switch (parser.getName()) {
            case "seinfo":
                pb.setGlobalSeinfoOrThrow(parser.getAttributeValue(null, "value"));
                readSeinfo(parser);
                break;
            case "package":
                if (ignoreMacPermissionsPackage()) {
                    // Compatibility flag can deliberately ignore package refinements.
                    skip(parser);
                } else {
                    readPackageOrThrow(parser, pb);
                }
                break;
            case "cert":
                pb.addSignature(parser.getAttributeValue(null, "signature"));
                readCert(parser);
                break;
            default:
                skip(parser);
        }
    }
    return pb.build();
}

一个 signer 可以把证书放在 signature 属性,也可以用多个 <cert> 子元素;解析器把它们加入同一个 Set<Signature>。<seinfo> 与 <package> 是互斥结构:前者给 signer 一个全局值,后者建立包名到 seinfo 的局部映射。ignore_mac_permissions_package 打开时,package stanza 被跳过而不是报错,这会改变匹配结果但不改变全局 signer。

4. 匹配算法 ​

4.1 Policy对象 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
final class Policy {
    private final String mSeinfo;
    private final Set<Signature> mCerts;
    private final Map<String, String> mPkgMap;

    public boolean hasInnerPackages() {
        return !mPkgMap.isEmpty();
    }

    public String getMatchedSeInfo(AndroidPackage pkg) {
        Signature[] certs = mCerts.toArray(new Signature[0]);
        if (pkg.getSigningDetails() != SigningDetails.UNKNOWN
                && !Signature.areExactMatch(pkg.getSigningDetails(), certs)) {
            // A single rotated platform certificate may still be accepted.
            if (certs.length > 1 || !pkg.getSigningDetails().hasCertificate(certs[0])) {
                return null;
            }
        }

        String seinfoValue = mPkgMap.get(pkg.getPackageName());
        if (seinfoValue != null) {
            return seinfoValue;
        }
        return mSeinfo;
    }
}

匹配先比较证书集合,再查精确包名,最后回退到该 signer 的全局 seinfo。证书轮换只在“policy 只有一个 cert 且 APK 包含该 cert”时有兼容路径;多证书 policy 必须精确集合匹配。getMatchedSeInfo() 返回 null 表示此 policy 不匹配,不能把 null 当成 default,因为 default 是外层 getSeInfo() 在全部 policy 都不匹配时才补上的。

4.2 排序与重复 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
final class PolicyComparator implements Comparator<Policy> {
    private boolean duplicateFound;

    @Override
    public int compare(Policy p1, Policy p2) {
        // Package-specific rules precede global signer rules.
        if (p1.hasInnerPackages() != p2.hasInnerPackages()) {
            return p1.hasInnerPackages() ? -1 : 1;
        }

        if (p1.getSignatures().equals(p2.getSignatures())) {
            if (p1.hasGlobalSeinfo()
                    || !Collections.disjoint(
                            p1.getInnerPackages().keySet(), p2.getInnerPackages().keySet())) {
                duplicateFound = true;
                Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1);
            }
        }
        return 0;
    }
}

排序的实际作用是让同一证书的 package refinement 先于 global seinfo;重复检测只记录并打印错误,readInstallPolicy() 仍会把排序后的列表装入共享状态。排障时要同时看“是否加载成功”和“日志是否报告 duplicate”,不能把 duplicate 日志自动理解成加载失败。

4.3 Builder不变量 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
public PolicyBuilder setGlobalSeinfoOrThrow(String seinfo) {
    if (!validateValue(seinfo)) {
        throw new IllegalArgumentException("Invalid seinfo value " + seinfo);
    }
    if (mSeinfo != null && !mSeinfo.equals(seinfo)) {
        throw new IllegalStateException("Duplicate seinfo tag found");
    }
    mSeinfo = seinfo;
    return this;
}

public PolicyBuilder addInnerPackageMapOrThrow(String pkgName, String seinfo) {
    if (!validateValue(pkgName) || !validateValue(seinfo)) {
        throw new IllegalArgumentException("Invalid package or seinfo value");
    }
    String old = mPkgMap.get(pkgName);
    if (old != null && !old.equals(seinfo)) {
        throw new IllegalStateException("Conflicting seinfo value found");
    }
    mPkgMap.put(pkgName, seinfo);
    return this;
}

public Policy build() {
    Policy p = new Policy(this);
    if (p.mCerts.isEmpty()) {
        throw new IllegalStateException("Missing certs with signer tag");
    }
    if (!(p.mSeinfo == null ^ p.mPkgMap.isEmpty())) {
        throw new IllegalStateException("Only seinfo XOR package tags are allowed");
    }
    return p;
}

validateValue() 只接受非空、由字母数字下划线和点组成的字符串;冒号被保留给运行时 seInfo selector,因此不能写进 XML 的 seinfo value。builder 在构造完成前强制检查“至少一个 cert”和“全局 seinfo 与 package map 二选一”,这让后续匹配逻辑可以假设 policy 结构有效。

5. seInfo生成 ​

5.1 基础值与后缀 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
public static String getSeInfo(
        PackageState packageState, AndroidPackage pkg,
        boolean isPrivileged, int targetSdkVersion) {
    String seInfo = null;
    synchronized (sPolicies) {
        if (sPolicyRead) {
            for (Policy policy : sPolicies) {
                seInfo = policy.getMatchedSeInfo(pkg);
                if (seInfo != null) {
                    break;
                }
            }
        }
    }

    if (seInfo == null) {
        seInfo = DEFAULT_SEINFO;
    }
    if (isPrivileged) {
        seInfo += PRIVILEGED_APP_STR;
    }
    seInfo += TARGETSDKVERSION_STR + targetSdkVersion;

    String partition = getPartition(packageState);
    if (!partition.isEmpty()) {
        seInfo += PARTITION_STR + partition;
    }
    return seInfo;
}

最终字符串至少含有 targetSdkVersion 后缀;预装分区还会追加 partition。privapp 不是证书匹配结果,而是 PackageState.isPrivileged() 和 shared user 状态的派生属性。比如一个 platform 签名但未预装到 privileged 路径的 APK,可能仍得到 platform 基础值,却不会自动得到 :privapp。

5.2 SDK与共享UID ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
private static int getTargetSdkVersionForSeInfo(
        AndroidPackage pkg, SharedUserApi sharedUser, PlatformCompat compatibility) {
    if (sharedUser != null && sharedUser.getPackages().size() != 0) {
        // A shared UID must use one selector value for all its packages.
        return sharedUser.getSeInfoTargetSdkVersion();
    }

    ApplicationInfo appInfo = AndroidPackageUtils.generateAppInfoWithoutState(pkg);
    if (compatibility.isChangeEnabledAndReportInternal(
            SELINUX_LATEST_CHANGES, appInfo, false)) {
        return Math.max(Build.VERSION_CODES.CUR_DEVELOPMENT, pkg.getTargetSdkVersion());
    } else if (compatibility.isChangeEnabledAndReportInternal(
            SELINUX_R_CHANGES, appInfo, false)) {
        return Math.max(Build.VERSION_CODES.R, pkg.getTargetSdkVersion());
    }
    return pkg.getTargetSdkVersion();
}

shared UID 的 target SDK 由 shared user owner 维护,而不是每次按单个 APK 计算;否则同一 UID 的进程可能进入不同 seapp domain。兼容变更开关可以把一个旧 target SDK 应用提升到 R 或当前开发版本的 selector,但不改变 XML 中的基础 seinfo。

5.3 分区后缀 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
private static String getPartition(PackageState state) {
    if (state.isSystemExt()) return "system_ext";
    if (state.isProduct()) return "product";
    if (state.isVendor()) return "vendor";
    if (state.isOem()) return "oem";
    if (state.isOdm()) return "odm";
    if (state.isSystem()) return "system";
    return "";
}

分区判断有固定优先顺序;状态同时标记多个分区时,先命中的分支决定后缀。普通 data APK 返回空分区,不会得到 :partition=。该后缀供 seapp_contexts 的 platform/vendor 优先级和兼容策略使用,不是文件系统 xattr。

6. 运行消费者 ​

6.1 PackageState写入 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.java

java
pkgSetting.getTransientState().setSeInfo(
        SELinuxMMAC.getSeInfo(
                pkgSetting,
                parsedPackage,
                sharedUserSetting,
                injector.getCompatibility()));

扫描或安装阶段先计算并写入 PackageSetting 的 transient state。后续 PackageInfoUtils 会把同一值复制到 ApplicationInfo.seInfo,而不是重新解析 XML。更新 APK 时如果签名、包名、特权状态或 target SDK 改变,Package Manager 需要重新计算并比较新旧值。

6.2 app data ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/AppDataHelper.java

java
final String seInfoUser;
synchronized (mPm.mLock) {
    seInfoUser = SELinuxUtil.getSeinfoUser(ps.readUserState(userId));
}

String pkgSeInfo = ps.getSeInfo();
Preconditions.checkNotNull(pkgSeInfo);

final String seInfo = pkgSeInfo + seInfoUser;
final CreateAppDataArgs args = Installer.buildCreateAppDataArgs(
        volumeUuid, packageName, userId, flags, appId, seInfo,
        targetSdkVersion, usesSdk, pccId);

app data 的 seInfo 是包级值加上用户级 selector;它与进程启动时的 seInfoUser 组合逻辑相关但不等同。Installer 依据该字符串选择目录的 SELinux context,因此“进程 domain 正确”不能单独证明 /data/user/... 的 type 正确。

6.3 Zygote参数 ​

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java、frameworks/base/core/java/android/os/ZygoteProcess.java

java
// ProcessList.java: the selector order is part of the wire contract.
if (selinuxInputSelector()) {
    return app.info.seInfo + extraInfo + TextUtils.emptyIfNull(app.info.seInfoUser);
} else {
    return app.info.seInfo
            + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser)
            + extraInfo;
}

ProcessList 先组合 selector;下一段是 ZygoteProcess 将组合结果编码到 Binder/zygote 命令行的消费者代码。

java
// ZygoteProcess.java: the composed value becomes a command-line argument.
if (seInfo != null) {
    argsForZygote.add("--seinfo=" + seInfo);
}

Zygote 收到的是完整 selector 字符串,而非 XML 里的单词 platform。Zygote native specialization 再把它交给 libselinux 的 seapp_context_lookup(),后者依据 seapp_contexts 输出进程 domain/type。SDK sandbox、audit/next restrictions 等额外 selector 由 ProcessList 在这个阶段追加。

7. 更新与失败 ​

7.1 启动加载失败 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

java
if (!SELinuxMMAC.readInstallPolicy()) {
    throw new RuntimeException("Unable to load SELinux MMAC policy");
}

Package Manager 在初始化期间要求 policy 成功加载。解析错误、非法证书字符串、重复冲突结构或文件读取失败会在 readInstallPolicy() 返回 false;这与“某个 APK 没匹配到 signer”不同,后者是正常路径并最终使用 default。

7.2 兼容变更回算 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

java
String oldSeInfo = packageState.getSeInfo();
String newSeInfo = SELinuxMMAC.getSeInfo(
        packageState, pkg, sharedUser, mInjector.getCompatibility());

if (!newSeInfo.equals(oldSeInfo)) {
    commitPackageStateMutation(null, packageName,
            state -> state.setOverrideSeInfo(newSeInfo));
    mAppDataHelper.prepareAppDataAfterInstallLIF(pkg);
}

compat change listener 发现 selector 变化后,先更新 package state,再重新准备 app data。它不会直接杀掉已有进程;进程下次启动时才会通过新的 seInfo 进入 Zygote。由此产生一个生效时机边界:PackageState 更新是同步状态变化,app data 准备是后续 Installer 工作,运行中进程的 domain 不会被原地改写。

7.3 进程启动失败清理 ​

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

java
try {
    String seInfo = updateSeInfo(app);
    return startProcessLocked(
            hostingRecord, entryPoint, app, uid, gids, runtimeFlags,
            zygotePolicyFlags, mountExternal, seInfo, requiredAbi,
            instructionSet, invokeWith, startUptime, startElapsedTime);
} catch (RuntimeException e) {
    Slog.e(ActivityManagerService.TAG,
            "Failure starting process " + app.processName, e);
    // Remove bookkeeping after a failed start; the package is force-stopped.
    mService.forceStopPackageLocked(
            app.info.packageName, UserHandle.getAppId(app.uid),
            false, false, true, false, false, false, app.userId, "start failure");
    return false;
}

如果 Zygote 因 selector、domain 匹配或其他启动参数抛出异常,ProcessList 清理启动 bookkeeping 并 force-stop 包。这个恢复路径只处理进程启动失败,不会回滚已经完成的 XML 构建或 PackageState 计算;下一次启动仍会使用同一 seInfo,除非 policy/state 被修正。

8. 测试与验证 ​

8.1 seInfo后缀测试 ​

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

java
@Test
public void getSeInfoOptInToR() {
    var packageState = new PackageStateBuilder(Build.VERSION_CODES.P).build();
    when(mMockCompatibility.isChangeEnabledAndReportInternal(
            eq(SELinuxMMAC.SELINUX_R_CHANGES),
            argThat(argument -> argument.packageName.equals(packageState.getPackageName())),
            eq(false))).thenReturn(true);

    assertThat(
            SELinuxMMAC.getSeInfo(
                    packageState, packageState.getAndroidPackage(), null, mMockCompatibility),
            is("default:targetSdkVersion=" + Build.VERSION_CODES.R));
}

输入是 target SDK 为 P 的 mock package,兼容服务对 R change 返回 true;断言检查结果被提升为 default:targetSdkVersion=R。它证明兼容开关影响后缀计算,不证明签名匹配,也不证明最终 domain 一定是某个具体 app domain。

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

java
@Test
public void getSeInfoPreinstalledToVendor() {
    var packageState = new PackageStateBuilder(Build.VERSION_CODES.CUR_DEVELOPMENT)
            .setSystem(true).setVendor(true).build();
    when(mMockCompatibility.isChangeEnabledAndReportInternal(
            eq(SELinuxMMAC.SELINUX_LATEST_CHANGES), Mockito.any(), eq(false)))
            .thenReturn(true);

    assertThat(
            SELinuxMMAC.getSeInfo(
                    packageState, packageState.getAndroidPackage(), null, mMockCompatibility),
            containsString(":partition=vendor"));
}

输入是标记为 vendor 的 PackageState;断言只要求返回值含 :partition=vendor。它验证分区后缀的选择,不验证 XML 是否有 vendor signer,也不验证 app data 或 Zygote 已经消费该值。

8.2 测试边界 ​

SELinuxMMAC 的测试使用 PackageStateBuilder 和 Mockito 构造 package,而不是读取设备上的真实 XML;因此它适合验证 getSeInfo() 的兼容、privapp 和 partition 组合,却不能替代构建产物检查。真实 XML 的证书替换应检查 insertkeys 输出是否仍含 @TAG,运行时则检查 Package Manager 日志和 ApplicationInfo.seInfo。

sh
# Inspect the generated XML; unresolved @TAG means insertkeys did not replace a key.
adb shell 'grep -n "signature=" /system/etc/selinux/plat_mac_permissions.xml'

# Inspect Package Manager's stored selector for a package.
adb shell 'dumpsys package <package.name> | grep -E "seinfo|seInfo"'

# Inspect the process context after Zygote has consumed the selector.
adb shell 'ps -AZ | grep <package.name>'

# Compare the app-data label with the process result; they are separate consumers.
adb shell 'ls -Zd /data/user/0/<package.name>'

排查时按“XML 是否生成 → signer 是否匹配 → seInfo 后缀是否正确 → seapp selector 是否命中 → app data/process 是否分别正确”的顺序进行。default 只说明没有 policy stanza 返回值,不说明 seapp_contexts 一定选择 untrusted_app;后者还取决于 user、privileged、target SDK 和包名规则。

9. 关系收束 ​

mac_permissions.xml 的安全边界可以用四个 owner 概括:构建系统拥有证书摘要替换,SELinuxMMAC 拥有 policy 列表和 seInfo 计算,PackageState 拥有已计算的包级状态,libselinux 拥有最终 selector 到 domain/type 的解析。每个 owner 的失败都不同:构建失败阻止产物更新,解析失败阻止 Package Manager 初始化,匹配失败回退 default,seapp lookup 失败则在进程或 app data 阶段暴露。

读者可以用一个平台签名 APK 复述完整路径:@PLATFORM 在构建时变成证书摘要;安装扫描调用 SELinuxMMAC.getSeInfo() 得到 platform:targetSdkVersion=...;PackageState 将其提供给 AppDataHelper 和 ProcessList;ProcessList 追加用户/SDK selector 后传入 --seinfo;libselinux 再按 seapp_contexts 选择最终 domain。若把 APK 换成同证书但不同包名、不同 target SDK 或不同安装分区,应能指出变化发生在 policy match、后缀生成还是 seapp consumer,而不是笼统归因于“签名权限”。