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.py | Policy.getMatchedSeInfo() |
seInfo | platform:targetSdkVersion=35:partition=system | SELinuxMMAC.getSeInfo() | PackageState、ApplicationInfo |
| 最终 domain | u:r:platform_app:s0 | libselinux 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 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
[@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.pemkeys.conf 的 section 名就是 XML 中的 @TAG;右侧路径在构建时通过环境变量解析到 PEM。ALL 表示该 tag 对当前 build variant 都可用。证书文件不是运行时由 Package Manager 读取的输入,它们在构建阶段被转换为 XML 中的十六进制证书摘要。
2.2 Soong变体
源码文件:system/sepolicy/mac_permissions/Android.bp
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
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
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
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
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
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
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
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
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
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
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
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
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
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
// 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 命令行的消费者代码。
// 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
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
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
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
@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
@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。
# 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,而不是笼统归因于“签名权限”。
