Seapp Contexts
本文面向已经读过 安全上下文、Type与Attribute、File Contexts 和 Property Contexts 的读者。前文解释了文件路径、属性名如何映射到安全 Context;本篇处理 App 特有的映射:同一个 UID、seinfo、包名和安装分区,怎样分别产生进程 domain 与数据目录 type,并附加多用户 MLS/MCS categories。
seapp_contexts 不是 PackageManager 的应用分类表,也不是 Zygote 的进程启动清单。它是一组有优先级的输入选择器和输出字段,由两个不同消费者使用:Zygote 子进程请求 SEAPP_DOMAIN,installd/libselinux 为 App data restorecon 请求 SEAPP_TYPE。把这两条链混在一起,会得到“进程域正确但数据目录标签错误”或“改了 type 却期待进程 domain 改变”之类错误判断。
本文以 Android 17 android-17.0.0_r1 为基线。读完后,读者应能从 PackageManager 生成的 seinfo 追到 AMS、Zygote、libselinux 的第一条匹配规则,再从 installd 追到 App data xattr;还能解释 target SDK、priv-app、instant app、SDK sandbox、PCC、多用户 category、分区来源和 enforcing/permissive 分支分别在哪里生效。
1. 两条输出链
1.1 进程Domain
Zygote fork 出子进程后调用 selinux_android_setcontext()。libselinux 以 SEAPP_DOMAIN 查询规则,只接受包含 domain= 的匹配项,把当前 Context 的 type 字段替换为目标 domain,随后通过 setcon() 动态切换。
源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
const char* se_info_ptr = se_info.has_value()
? se_info.value().c_str() : nullptr;
if (selinux_android_setcontext(uid, is_system_server,
se_info_ptr, nice_name_ptr) == -1) {
fail_fn(CREATE_ERROR(
"selinux_android_setcontext(%d, %d, \"%s\", \"%s\") failed",
uid, is_system_server, se_info_ptr, nice_name_ptr));
}这里的 nice_name_ptr 同时充当 seapp 的 pkgname 输入。普通 App 通常是包名或进程 nice name;若规则指定 name= 而调用方没有提供名字,该规则不会匹配。
1.2 数据目录Type
App data 的标签不能仅由 /data/user/... 路径正则决定,因为不同应用需要不同 type 和 categories。libselinux 先用 file contexts 得到基础 Context,再对 App data 路径调用 seapp_context_lookup(SEAPP_TYPE, ...),只消费包含 type= 的规则。
源码文件:external/selinux/libselinux/src/android/android_device.c
ctx = context_new(orig_ctx_str);
if (!ctx)
goto err;
rc = seapp_context_lookup(SEAPP_TYPE,
info ? info->uid : uid,
false,
info ? info->seinfo : seinfo,
info ? info->name : pkgname,
ctx);
if (rc < 0)
goto err;
ctx_str = context_str(ctx);
if (!ctx_str)
goto err;
if (!strcmp(ctx_str, orig_ctx_str))
goto out;
rc = security_check_context(ctx_str);
if (rc < 0)
goto err;
freecon(*secontextp);
*secontextp = strdup(ctx_str);SEAPP_TYPE 不改变 user、role 或已有基础 Context 的其他字段,只替换 type,并按规则设置 range。随后 restorecon 才把结果写入 security.selinux xattr。
1.3 共享输入
两条链共享这些输入:
| 输入 | 所有者 | 作用 |
|---|---|---|
| UID | AMS、installd 调用参数 | 计算 Android user、appId 和特殊 UID 类别 |
isSystemServer | Zygote 调用参数 | 只在进程 domain 查询中区分 system_server |
seinfo | PackageManager 生成并传递 | 基础标签、priv/instant/SDK 等修饰符、target SDK、分区 |
| package/name | 进程启动参数或 packages.list | 参与 name= 精确/前缀选择 |
| 当前 Context | Zygote 当前进程或 file contexts 基础结果 | 提供要被替换的 type/range 之外字段 |
2. 输入向量
2.1 选择器全集
平台文件头是 Android 17 最直接的格式契约。所有已指定 selector 必须同时匹配;未指定字段不是统一意义上的 false,部分布尔字段有默认 false,isEphemeralApp 和 isPrivApp 则另外记录“是否指定”。
源码文件:system/sepolicy/private/seapp_contexts
# Input selectors:
# isSystemServer (boolean)
# isEphemeralApp (boolean)
# user (string)
# seinfo (string)
# name (string)
# isPrivApp (boolean)
# minTargetSdkVersion (unsigned integer)
# fromRunAs (boolean)
# isIsolatedComputeApp (boolean)
# isSdkSandboxNext (boolean)
# isSdkSandboxAudit (boolean)
#
# Outputs:
# domain (string)
# type (string)
# levelFrom (string; one of none, all, app, or user)
# level (string)domain= 缺失的规则在 domain 查询中跳过;type= 缺失的规则在 data type 查询中跳过。同一输入 selector 可以理论上由不同规则分别提供 domain/type,但完全重复的 selector 会被 loader 和 checkseapp 拒绝,因此通常一条规则同时写两种输出。
2.2 UID分类
libselinux 先把多用户 UID 拆成 userid 和 appid。小于 AID_APP_START 的固定 UID 通过 getpwuid(appid) 得到 system、radio 等用户名;普通 App、SDK sandbox、PCC component 和 isolated UID 映射为特殊 user 字符串。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
userid = uid / AID_USER_OFFSET;
appid = uid % AID_USER_OFFSET;
if (appid < AID_APP_START) {
pwd = seapp_getpwuid(appid);
if (!pwd)
goto err;
username = pwd->pw_name;
} else if (appid < AID_SDK_SANDBOX_PROCESS_START) {
username = "_app";
appid -= AID_APP_START;
} else if (appid < AID_PCC_COMPONENT_PROCESS_START) {
username = "_sdksandbox";
appid -= AID_SDK_SANDBOX_PROCESS_START;
} else if (appid <= AID_PCC_COMPONENT_PROCESS_END) {
username = "_pcc_component";
} else {
username = "_isolated";
appid -= AID_ISOLATED_START;
}
if (appid >= CAT_MAPPING_MAX_ID || userid >= CAT_MAPPING_MAX_ID)
goto err;注意 category 计算使用的是归一化后的 appid。普通 App UID 10001 被转换为 appid 1;user 10 的同一个 App UID 是 1010001,仍得到 appid 1,但 userid 变为 10。
2.3 Seinfo编码
PackageManager 提供的 seinfo 不是一个简单枚举。第一个冒号前是 base,如 default、platform、media;后续 token 编码 priv-app、instant app、isolated compute、SDK sandbox 模式、run-as、target SDK 和安装分区,并由 complete 截止。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
for (token = strtok_r(local_seinfo, ":", &saved_colon_ptr);
token;
token = strtok_r(NULL, ":", &saved_colon_ptr)) {
if (first) {
strncpy(info->base, token, SEINFO_BUFSIZ);
first = false;
continue;
}
if (!strcmp(token, "privapp")) {
info->is |= IS_PRIV_APP;
continue;
}
if (!strcmp(token, "ephemeralapp")) {
info->is |= IS_EPHEMERAL_APP;
continue;
}
if (!strcmp(token, "isolatedComputeApp")) {
info->is |= IS_ISOLATED_COMPUTE_APP;
continue;
}
if (!strcmp(token, "isSdkSandboxAudit")) {
info->is |= IS_SDK_SANDBOX_AUDIT;
continue;
}
if (!strcmp(token, "isSdkSandboxNext")) {
info->is |= IS_SDK_SANDBOX_NEXT;
continue;
}
if (!strcmp(token, "fromRunAs")) {
info->is |= IS_FROM_RUN_AS;
continue;
}
/* targetSdkVersion=、partition= 和 complete 分支省略。 */
}未知 token 只记录 warning 并忽略;缺少 targetSdkVersion= 的值或 seinfo 长度达到 buffer 上限则返回错误。seapp_contexts 的 seinfo= selector 只比较 info.base,修饰符由各自 boolean/SDK/partition 逻辑使用。
2.4 包名匹配
user= 与 name= 都允许末尾 * 前缀匹配,并且字符串比较不区分大小写。只有末尾星号有前缀语义;规则中的 com.google.android.gms.* 会匹配以该文本开头的名字,而 * 不提供通用正则表达式。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
if (cur->name.str) {
if (!pkgname)
continue;
if (cur->name.is_prefix) {
if (strncasecmp(pkgname, cur->name.str,
cur->name.len - 1))
continue;
} else {
if (strcasecmp(pkgname, cur->name.str))
continue;
}
}这里与 checkseapp neverallow 中的 PCRE 不是同一种语言:普通 rule 的 name= 只支持固定值/末尾星号,neverallow selector 才支持完整 PCRE。
3. Seinfo来源
3.1 签名策略
SELinuxMMAC 依次加载 platform、system_ext、product、vendor、odm 的 mac_permissions.xml。它将签名与可选包名 refinement 转换为 Policy,排序后保存到共享列表;任何 XML 解析失败都会使整个加载返回 false,不保留部分结果。
源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java
private static final List<Policy> sPolicies = new ArrayList<>();
private static boolean sPolicyRead;
static {
sMacPermissions.add(new File(
Environment.getRootDirectory(),
"/etc/selinux/plat_mac_permissions.xml"));
final File systemExt = new File(
Environment.getSystemExtDirectory(),
"/etc/selinux/system_ext_mac_permissions.xml");
if (systemExt.exists()) sMacPermissions.add(systemExt);
final File product = new File(
Environment.getProductDirectory(),
"/etc/selinux/product_mac_permissions.xml");
if (product.exists()) sMacPermissions.add(product);
final File vendor = new File(
Environment.getVendorDirectory(),
"/etc/selinux/vendor_mac_permissions.xml");
if (vendor.exists()) sMacPermissions.add(vendor);
final File odm = new File(
Environment.getOdmDirectory(),
"/etc/selinux/odm_mac_permissions.xml");
if (odm.exists()) sMacPermissions.add(odm);
}mac permissions 的 XML 语法由后续专题负责;本篇只追踪其产物 seinfo 怎样成为 seapp selector 输入。
3.2 修饰符拼接
getSeInfo() 先按签名/包名选择 base,找不到时使用 default;随后附加 :privapp、:targetSdkVersion=N 和可选的 :partition=...。
源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java
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 += ":privapp";
seInfo += ":targetSdkVersion=" + targetSdkVersion;
String partition = getPartition(packageState);
if (!partition.isEmpty()) {
seInfo += ":partition=" + partition;
}
return seInfo;isPrivileged 来自 PackageState 或 shared user。安装在 /system/app 并不自动等于 priv-app;selector 使用的是 PackageManager 计算后写入 seinfo 的 bit。
3.3 Shared UID
共享 UID 的所有包必须落在同一 SELinux domain。getTargetSdkVersionForSeInfo() 使用 shared user 保存的最低 target SDK,选择较旧、限制较少的 domain,并刻意不在安装/更新时立即降低该值,直到下次启动重新计算,避免同一 shared UID 的进程在运行期间改变分类。
源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java
if ((sharedUser != null) && (sharedUser.getPackages().size() != 0)) {
return sharedUser.getSeInfoTargetSdkVersion();
}
final 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();因此 minTargetSdkVersion= 匹配的输入不一定是 manifest 原始 target SDK;兼容性 change 和 shared UID 都能调整它。
3.4 User后缀
每用户状态通过 seInfoUser 追加 instant-app 标志和终止 token。普通安装返回 :complete,instant app 返回 :ephemeralapp:complete。
源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/SELinuxUtil.java
public static String getSeinfoUser(PackageUserState userState) {
if (userState.isInstantApp()) {
return ":ephemeralapp:complete";
}
return ":complete";
}complete 使 native parser 停止处理后续 selector。ProcessList 在 selinuxInputSelector() feature flag 开启时,会把 SDK sandbox 的附加 selector 放在 seInfoUser 之前,避免被 complete 截断。
源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java
String extraInfo = "";
if (app.isSdkSandbox) {
if (getProcessListSettingsListener()
.applySdkSandboxRestrictionsNext()) {
extraInfo = APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS;
} else if (selinuxSdkSandboxAudit()
&& getProcessListSettingsListener()
.applySdkSandboxRestrictionsAudit()) {
extraInfo = APPLY_SDK_SANDBOX_AUDIT_RESTRICTIONS;
}
}
if (selinuxInputSelector()) {
return app.info.seInfo + extraInfo
+ TextUtils.emptyIfNull(app.info.seInfoUser);
} else {
return app.info.seInfo
+ TextUtils.emptyIfNull(app.info.seInfoUser)
+ extraInfo;
}这条条件路径说明 seinfo token 顺序属于协议的一部分,不是日志展示格式。
4. 规则装载
4.1 分区路径
libselinux 为五个分区各定义正式路径和根目录兼容路径。每个分区只选第一个存在的 alternative,同时记录该文件属于 platform 侧还是 vendor 侧。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
static const path_alts_t seapp_context_paths = { .paths = {
{
"/system/etc/selinux/plat_seapp_contexts",
"/plat_seapp_contexts"
},
{
"/system_ext/etc/selinux/system_ext_seapp_contexts",
"/system_ext_seapp_contexts"
},
{
"/product/etc/selinux/product_seapp_contexts",
"/product_seapp_contexts"
},
{
"/vendor/etc/selinux/vendor_seapp_contexts",
"/vendor_seapp_contexts"
},
{
"/odm/etc/selinux/odm_seapp_contexts",
"/odm_seapp_contexts"
}
}, .partitions = {
"system", "system_ext", "product", "vendor", "odm"
}};4.2 一次初始化
默认加载由 pthread_once 保护。Zygote 在 fork 前显式调用初始化,普通 lookup 也会兜底调用一次;运行时只有显式 selinux_android_seapp_context_reload() 才重新读文件。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
static void seapp_context_init(void)
{
selinux_android_seapp_context_reload();
}
static pthread_once_t seapp_once = PTHREAD_ONCE_INIT;
void selinux_android_seapp_context_init(void)
{
__selinux_once(seapp_once, seapp_context_init);
}
int seapp_context_lookup(enum seapp_kind kind, uid_t uid,
bool isSystemServer, const char *seinfo,
const char *pkgname, context_t ctx)
{
selinux_android_seapp_context_init();
return seapp_context_lookup_internal(kind, uid, isSystemServer,
seinfo, pkgname, ctx);
}源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
// security_getenforce is not allowed on app process. Initialize and cache
// the value before zygote forks.
gIsSecurityEnforced = security_getenforce();
selinux_android_seapp_context_init();预加载避免每个 App 子进程首次读取 contexts 文件,也确保 fork 后受限 domain 不需要自行打开这些策略文件。
4.3 解析失败
loader 先统计所有非空、非注释行,再为每行分配结构体。未知 key、重复字段、非法 bool、非法 target SDK、seinfo 中包含冒号、错误 levelFrom 或没有 name 对应专用 seinfo 的不安全配置都会释放全部已加载规则并返回失败。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
if (!cur->isPrivApp && cur->name.str &&
(!cur->seinfo || !strcmp(cur->seinfo, "default"))) {
selinux_log(SELINUX_ERROR,
"%s: No specific seinfo value specified with name=\"%s\", "
"on line %u: insecure configuration!\n",
seapp_contexts_files[i], cur->name.str, lineno);
free_seapp_context(cur);
goto err;
}
cur->partition = seapp_contexts_partitions[i];
seapp_contexts[nspec++] = cur;专门按包名给非 priv-app 分配 domain 时必须有非 default seinfo,这阻止任意使用同一包名的普通签名应用获得专用域。
5. 排序优先级
5.1 Comparator
加载完成后,libselinux 对所有分区 entries 统一 qsort()。文件中的行号不是运行时匹配优先级;比较器依次考虑 system server、ephemeral 是否指定、user specificity、seinfo、name specificity、priv 是否指定、较高 target SDK、run-as 和 platform/vendor 来源。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
static int seapp_context_cmp(const void *A, const void *B)
{
const struct seapp_context *s1 =
*(const struct seapp_context *const *)A;
const struct seapp_context *s2 =
*(const struct seapp_context *const *)B;
if (s1->isSystemServer != s2->isSystemServer)
return s1->isSystemServer ? -1 : 1;
if (s1->isEphemeralAppSet != s2->isEphemeralAppSet)
return s1->isEphemeralAppSet ? -1 : 1;
if (s1->user.str && !s2->user.str) return -1;
if (!s1->user.str && s2->user.str) return 1;
if (s1->user.str) {
if (s1->user.is_prefix != s2->user.is_prefix)
return s2->user.is_prefix ? -1 : 1;
if (s1->user.is_prefix && s1->user.len != s2->user.len)
return s1->user.len > s2->user.len ? -1 : 1;
}
/* seinfo、name、isPrivApp 的相同模式省略。 */
if (s1->minTargetSdkVersion != s2->minTargetSdkVersion)
return s1->minTargetSdkVersion > s2->minTargetSdkVersion ? -1 : 1;
if (s1->fromRunAs != s2->fromRunAs)
return s1->fromRunAs ? -1 : 1;
bool p1 = is_platform(s1->partition);
bool p2 = is_platform(s2->partition);
if (p1 != p2) return p1 ? -1 : 1;
return 0;
}isIsolatedComputeApp、SDK sandbox next/audit 没有出现在 comparator 的独立层级中;它们默认 false 并在 lookup 阶段严格比较。若两条规则仅在这些 boolean 上不同,比较器认为优先级相同,但它们不是 duplicate,因为 duplicate 检查会继续比较这些字段。
5.2 固定与前缀
固定 user/name 比带星号前缀优先;两个前缀之间更长者优先。例如 name=com.google.android.gms 在 name=com.google.android.gms.* 之前,后者又在没有 name selector 的 priv-app catch-all 之前。
5.3 平台边界
system、system_ext、product 被视为 platform,vendor、odm 被视为 vendor。只有前面所有 selector priority 都相同时,platform entry 才优先。这不是“platform 永远覆盖 vendor”;更具体的 vendor selector 仍可能排在宽泛 platform rule 前,但预装分区校验和 coredomain 检查会限制其可用输出。
5.4 重复规则
排序后 loader 在相同优先级的连续区间中比较完整 input selectors。user、seinfo、name、priv、ephemeral、isolated compute、SDK audit/next 相同即判为 duplicate,整个加载失败。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
bool dup =
(!s1->user.str || !strcmp(s1->user.str, s2->user.str)) &&
(!s1->seinfo || !strcmp(s1->seinfo, s2->seinfo)) &&
(!s1->name.str || !strcmp(s1->name.str, s2->name.str)) &&
(!s1->isPrivAppSet || s1->isPrivApp == s2->isPrivApp) &&
(!s1->isEphemeralAppSet ||
s1->isEphemeralApp == s2->isEphemeralApp) &&
(s1->isIsolatedComputeApp == s2->isIsolatedComputeApp) &&
(s1->isSdkSandboxAudit == s2->isSdkSandboxAudit) &&
(s1->isSdkSandboxNext == s2->isSdkSandboxNext);
if (dup) {
selinux_log(SELINUX_ERROR,
"seapp_contexts: Duplicated entry\n");
goto err_no_log;
}6. 构建链路
6.1 分区Module
Soong 为五个分区分别生成 seapp contexts。平台 module 只依赖 platform policy,确保它不依赖其他分区;system_ext/product/vendor/odm 使用完整 precompiled policy。后续分区还把前面分区的 seapp files 作为 neverallow_files 输入。
源码文件:system/sepolicy/contexts/Android.bp
se_policy_binary {
name: "plat_precompiled_sepolicy",
srcs: [":plat_sepolicy.cil"],
installable: false,
}
seapp_contexts {
name: "plat_seapp_contexts",
defaults: ["contexts_flags_defaults"],
srcs: [":seapp_contexts_files{.plat_private}"],
sepolicy: ":plat_precompiled_sepolicy",
}
seapp_contexts {
name: "vendor_seapp_contexts",
defaults: ["contexts_flags_defaults"],
srcs: [
":seapp_contexts_files{.plat_vendor}",
":seapp_contexts_files{.vendor}",
":seapp_contexts_files{.reqd_mask}",
],
neverallow_files: [
":seapp_contexts_files{.plat_private}",
":seapp_contexts_files{.system_ext_private}",
":seapp_contexts_files{.product_private}",
],
soc_specific: true,
sepolicy: ":precompiled_sepolicy",
}6.2 三步生成
buildSeappContexts() 的三步是:对 neverallow sources 做 m4 并提取行首 neverallow;对当前分区 contexts 做通用 m4;调用 checkseapp 校验并输出过滤后的 contexts。
源码文件:system/sepolicy/build/soong/selinux_contexts.go
func (m *selinuxContextsModule) buildSeappContexts(
ctx android.ModuleContext, inputs android.Paths) android.Path {
neverallowFile := pathForModuleOut(ctx, "neverallow")
ret := pathForModuleOut(ctx, "checkseapp", m.stem())
flags := m.getBuildFlags(ctx)
m4NeverallowFile := pathForModuleOut(ctx, "neverallow.m4out")
rule := android.NewRuleBuilder(pctx, ctx)
rule.Command().
Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
Flag("--fatal-warnings").
FlagForEachArg("-D", ctx.DeviceConfig().SepolicyM4Defs()).
Flags(flagsToM4Macros(flags)).
Inputs(android.PathsForModuleSrc(
ctx, m.seappProperties.Neverallow_files)).
FlagWithOutput("> ", m4NeverallowFile)
rule.Command().
Text("( grep").Flag("-ihe").Text("'^neverallow'").
Input(m4NeverallowFile).Text(">").Output(neverallowFile).
Text("|| true )")
builtCtx := m.buildGeneralContexts(ctx, inputs)
checkCmd := rule.Command().BuiltTool("checkseapp").
FlagWithInput("-p ", android.PathForModuleSrc(
ctx, proptools.String(m.seappProperties.Sepolicy))).
FlagWithOutput("-o ", ret).
Input(builtCtx).
Input(neverallowFile)
if m.shouldCheckCoredomain(ctx) {
checkCmd.Flag("-c")
}
rule.Build("seapp_contexts", "Building seapp_contexts: "+m.Name())
return ret
}checkseapp 输出不包含 neverallow 行。运行时 libselinux 只读取真实 rule;neverallow 只在构建期约束这些输出。
6.3 Checkseapp字段
工具维护一张 input/output key 表。domain 必须是 policy 中存在的 type;type 还必须具有 app_data_file_type;固定 level 必须通过 policy 的 MLS 校验。
源码文件:system/sepolicy/tools/check_seapp.c
key_map rules[] = {
{ .name = "isSystemServer", .dir = dir_in,
.fn_validate = validate_bool },
{ .name = "isEphemeralApp", .dir = dir_in,
.fn_validate = validate_bool },
{ .name = "user", .dir = dir_in },
{ .name = "seinfo", .dir = dir_in },
{ .name = "name", .dir = dir_in },
{ .name = "isPrivApp", .dir = dir_in,
.fn_validate = validate_bool },
{ .name = "minTargetSdkVersion", .dir = dir_in,
.fn_validate = validate_uint },
{ .name = "fromRunAs", .dir = dir_in,
.fn_validate = validate_bool },
{ .name = "isIsolatedComputeApp", .dir = dir_in,
.fn_validate = validate_bool },
{ .name = "isSdkSandboxAudit", .dir = dir_in,
.fn_validate = validate_bool },
{ .name = "isSdkSandboxNext", .dir = dir_in,
.fn_validate = validate_bool },
{ .name = "domain", .dir = dir_out,
.fn_validate = validate_domain },
{ .name = "type", .dir = dir_out,
.fn_validate = validate_type },
{ .name = "levelFrom", .dir = dir_out,
.fn_validate = validate_levelFrom },
{ .name = "level", .dir = dir_out,
.fn_validate = validate_selinux_level },
};6.4 Type约束
源码文件:system/sepolicy/tools/check_seapp.c
static bool validate_type(char *value, const char *filename,
int lineno, char **errmsg) {
type_datum_t *type_dat = find_type(pol.db, value, TYPE_TYPE);
if (!type_dat) {
*errmsg = "Expecting a valid SELinux type";
return false;
}
type_datum_t *attrib_dat = find_type(
pol.db, "app_data_file_type", TYPE_ATTRIB);
if (!attrib_dat) return true;
if (!type_has_attribute(pol.db, type_dat, attrib_dat)) {
*errmsg = "Missing required attribute app_data_file_type";
return false;
}
return true;
}因此 type=system_data_file 即使是合法 file type,也不能用于 seapp App data 输出,除非它属于 app_data_file_type。
6.5 Vendor核心域
对 vendor/odm module,产品配置要求检查时,Soong 给 checkseapp 增加 -c。工具发现输出 domain 带 coredomain attribute 时记录 violation,并明确要求把条目移到 system/system_ext/product 或移除该 attribute。
7. 匹配循环
7.1 全字段AND
runtime 按排序后的数组遍历,每个已指定 selector 都是 continue gate。target SDK 的方向是“应用值大于等于规则 minimum”;boolean default false 字段必须与 seinfo bit 完全相等。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
for (i = 0; i < nspec; i++) {
cur = seapp_contexts[i];
if (cur->isSystemServer != isSystemServer)
continue;
if (cur->isEphemeralAppSet &&
cur->isEphemeralApp !=
((info.is & IS_EPHEMERAL_APP) != 0))
continue;
/* user、seinfo、name 匹配省略。 */
if (cur->isPrivAppSet &&
cur->isPrivApp != ((info.is & IS_PRIV_APP) != 0))
continue;
if (cur->minTargetSdkVersion > info.targetSdkVersion)
continue;
if (cur->fromRunAs != ((info.is & IS_FROM_RUN_AS) != 0))
continue;
if (cur->isIsolatedComputeApp !=
((info.is & IS_ISOLATED_COMPUTE_APP) != 0))
continue;
if (cur->isSdkSandboxAudit !=
((info.is & IS_SDK_SANDBOX_AUDIT) != 0))
continue;
if (cur->isSdkSandboxNext !=
((info.is & IS_SDK_SANDBOX_NEXT) != 0))
continue;7.2 输出过滤
同一遍历根据 kind 跳过缺失输出,匹配后只替换 domain 或 type,再处理 range。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
if (kind == SEAPP_TYPE && !cur->type)
continue;
else if (kind == SEAPP_DOMAIN && !cur->domain)
continue;
if (kind == SEAPP_TYPE) {
if (context_type_set(ctx, cur->type))
goto oom;
} else if (kind == SEAPP_DOMAIN) {
if (context_type_set(ctx, cur->domain))
goto oom;
}
if (cur->levelFrom != LEVELFROM_NONE) {
int res = set_range_from_level(ctx, cur->levelFrom,
userid, appid);
if (res != 0) return res;
} else if (cur->level) {
if (context_range_set(ctx, cur->level))
goto oom;
}
break;
}levelFrom 优先于固定 level:只有 levelFrom=none 时才应用 level=。parser 的 enum 初始值就是 NONE,所以没有 levelFrom 的 rule 仍可写固定 level。
7.3 无匹配差异
domain 查询没有匹配时,在 enforcing 模式返回错误,避免 App 保留 Zygote domain;permissive 模式记录错误但返回成功。type 查询没有匹配时没有这段 fatal 分支,Context 保持 file contexts 给出的基础 type。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
if (kind == SEAPP_DOMAIN && i == nspec) {
selinux_log(SELINUX_ERROR,
"%s: No match for app with uid %d, seinfo %s, name %s\n",
__FUNCTION__, uid, seinfo, pkgname);
if (security_getenforce() == 1)
goto err;
}
return 0;这个不对称是故障定位的重要边界:进程 domain 缺失会阻止 enforcing 设备启动该子进程;App data type 缺失可能表现为目录保留通用 app_data_file 基础标签,而不是立刻中止。
7.4 分区警告
预装在 system/system_ext/product 的 App 若匹配到 vendor/odm seapp rule,runtime 当前只记录 warning,源码带有“修复违规后改为 error”的 TODO。构建期 coredomain 检查和 seapp neverallow 仍可能更早阻止危险配置。
8. MLS Categories
8.1 四种模式
levelFrom 把归一化 appid 和 Android userid 编码到四个 category 区间:App 低 8 位进入 c0-c255,高 8 位进入 c256-c511;user 低 8 位进入 c512-c767,高 8 位进入 c768-c1023。
源码文件:external/selinux/libselinux/src/android/android_seapp.c
switch (levelFrom) {
case LEVELFROM_NONE:
strncpy(level, "s0", sizeof level);
break;
case LEVELFROM_APP:
snprintf(level, sizeof level, "s0:c%u,c%u",
appid & 0xff,
256 + (appid >> 8 & 0xff));
break;
case LEVELFROM_USER:
snprintf(level, sizeof level, "s0:c%u,c%u",
512 + (userid & 0xff),
768 + (userid >> 8 & 0xff));
break;
case LEVELFROM_ALL:
snprintf(level, sizeof level, "s0:c%u,c%u,c%u,c%u",
appid & 0xff,
256 + (appid >> 8 & 0xff),
512 + (userid & 0xff),
768 + (userid >> 8 & 0xff));
break;
default:
return -1;
}
return context_range_set(ctx, level) ? -2 : 0;8.2 计算示例
普通 App UID 10045:userid=0,归一化 appid=45。
| levelFrom | 结果 |
|---|---|
none | s0 |
app | s0:c45,c256 |
user | s0:c512,c768 |
all | s0:c45,c256,c512,c768 |
同一 App 在 Android user 10 的 UID 为 1010045,app categories 不变,user categories 变为 c522,c768。这使同一包的不同用户数据目录可由 MLS/MCS 隔离。
8.3 支持边界
平台文件明确限制:levelFrom=user 只支持 _app 或 _isolated;levelFrom=app/all 只支持 _app。当前规则对 SDK sandbox 和 PCC 使用 levelFrom=all,其 UID 分类由新范围扩展;阅读历史规则时不能把旧注释边界直接外推到 Android 17 的新增 UID 类型,应以当前 loader、测试和生成规则为准。
8.4 溢出边界
CAT_MAPPING_MAX_ID 为 2^16。userid 或归一化 appid 超出可唯一编码范围时 lookup 返回错误,避免 category 碰撞。
9. 进程切换
9.1 AMS输入
AMS 在启动前要求 ApplicationInfo.seInfoUser 已生成,调用 updateSeInfo() 合并 per-package 与 per-user token,再把结果保存进 ProcessRecord 的 start params。异步启动时同一字符串随 pending start 继续传递。
源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java
if (TextUtils.isEmpty(app.info.seInfoUser)) {
Slog.wtf(ActivityManagerService.TAG, "SELinux tag not defined",
new IllegalStateException("SELinux tag not defined for "
+ app.info.packageName + " (uid " + app.uid + ")"));
}
String seInfo = updateSeInfo(app);
return startProcessLocked(hostingRecord, "android.app.ActivityThread",
app, uid, gids, runtimeFlags, zygotePolicyFlags,
mountExternal, seInfo, requiredAbi, instructionSet,
invokeWith, startUptime, startElapsedTime);9.2 Zygote协议
Process.start() 把 seInfo 传给 ZygoteProcess,Zygote command parser 通过 --seinfo= 保存到 ZygoteArguments.mSeInfo,fork 路径再传给 native specialize。
源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteArguments.java
} else if (arg.startsWith("--seinfo=")) {
if (mSeInfoSpecified) {
throw new IllegalArgumentException("Duplicate arg specified");
}
mSeInfoSpecified = true;
mSeInfo = getAssignmentValue(arg);
}源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java
pid = Zygote.forkAndSpecialize(
parsedArgs.mUid, parsedArgs.mGid, parsedArgs.mGids,
parsedArgs.mRuntimeFlags, rlimits,
parsedArgs.mMountExternal, parsedArgs.mSeInfo,
parsedArgs.mNiceName, fdsToClose, fdsToIgnore,
parsedArgs.mStartChildZygote,
parsedArgs.mInstructionSet, parsedArgs.mAppDataDir,
parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList,
parsedArgs.mAllowlistedDataInfoList,
parsedArgs.mBindMountAppDataDirs,
parsedArgs.mBindMountAppStorageDirs,
parsedArgs.mBindMountSyspropOverrides);9.3 Setcon提交
libselinux 从当前 Context 构造可修改对象,执行 domain lookup,检查生成的完整 Context 是否被 policy 接受;只有字符串实际变化才调用 selinux_android_setcon()。
源码文件:external/selinux/libselinux/src/android/android_device.c
rc = getcon(&orig_ctx_str);
if (rc)
goto err;
ctx = context_new(orig_ctx_str);
if (!ctx)
goto oom;
rc = seapp_context_lookup(SEAPP_DOMAIN, uid, isSystemServer,
seinfo, pkgname, ctx);
if (rc < 0)
goto err;
ctx_str = context_str(ctx);
if (!ctx_str)
goto oom;
rc = security_check_context(ctx_str);
if (rc < 0)
goto err;
if (strcmp(ctx_str, orig_ctx_str)) {
rc = selinux_android_setcon(ctx_str);
if (rc < 0)
goto err;
}selinux_android_setcon() 在 setcon() 后重新初始化 system properties 映射,避免子进程继续保留旧 domain 打开的属性文件 mmap。
源码文件:external/selinux/libselinux/src/android/android_device.c
int selinux_android_setcon(const char *con)
{
int ret = setcon(con);
if (ret)
return ret;
/* 关闭旧属性映射,并按新 domain 的访问权限重新初始化。 */
return __system_properties_init();
}9.4 动态切换权限
Zygote 的 setcon() 不是 exec transition。policy 必须允许 zygote 对自身 setcurrent,并允许到 appdomain、system_server_startup、webview_zygote、app_zygote 的 dyntransition;neverallow 阻止跳到其他 domain。
源码文件:system/sepolicy/private/zygote.te
allow zygote self:process setcurrent;
allow zygote system_server_startup:process dyntransition;
allow zygote appdomain:process dyntransition;
allow zygote webview_zygote:process dyntransition;
allow zygote app_zygote:process dyntransition;
neverallow zygote ~{
appdomain
system_server_startup
webview_zygote
app_zygote
}:process dyntransition;9.5 系统服务阶段
Android 17 的 seapp rule 先把 isSystemServer=true 映射到 system_server_startup。native specialize 后,Zygote 又显式 setcon("u:r:system_server:s0"),所以 system_server 存在启动准备域与最终运行域两个阶段。
源码文件:system/sepolicy/private/seapp_contexts
isSystemServer=true domain=system_server_startup源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
if (is_system_server) {
env->CallStaticVoidMethod(gZygoteClass,
gCallPostForkSystemServerHooks,
runtime_flags);
if (env->ExceptionCheck()) {
fail_fn("Error calling post fork system server hooks.");
}
static const char* kSystemServerLabel = "u:r:system_server:s0";
if (selinux_android_setcon(kSystemServerLabel) != 0) {
fail_fn(CREATE_ERROR("selinux_android_setcon(%s)",
kSystemServerLabel));
}
}10. 数据目录
10.1 Installd入口
PackageManager 通过 installd AIDL 传入 packageName、userId、appId、seInfo 和 target SDK。installd 计算多用户 uid,创建 CE/DE 目录、cache 和 code_cache,然后调用 lazy restorecon。
源码文件:frameworks/native/cmds/installd/InstalldNativeService.cpp
static binder::Status createAppDataDirs(
const std::string& path, int32_t uid, int32_t gid,
int32_t previousUid, int32_t cacheGid,
const std::string& seInfo, mode_t targetMode,
long projectIdApp, long projectIdCache) {
struct stat st{};
bool parent_dir_exists = stat(path.c_str(), &st) == 0;
if (prepare_app_dir(path, targetMode, uid, gid, projectIdApp) ||
prepare_app_cache_dir(path, "cache", 02771,
uid, cacheGid, projectIdCache) ||
prepare_app_cache_dir(path, "code_cache", 02771,
uid, cacheGid, projectIdCache)) {
return error("Failed to prepare " + path);
}
if (restorecon_app_data_lazy(path, seInfo, uid,
parent_dir_exists)) {
return error("Failed to restorecon " + path);
}
/* 新建 cache/code_cache 的补充 restorecon 分支省略。 */
return ok();
}10.2 Seapp覆盖
restorecon 先用 file contexts 查路径,发现属于 App data 后改用 package-aware lookup。这样目录结构仍由 file contexts 提供基础 Context,而具体应用 type/range 由 seapp 决定。
源码文件:external/selinux/libselinux/src/android/android_device.c
if (selabel_lookup(fc_sehandle, &secontext,
pathname, sb->st_mode) < 0)
return 0;
if (lgetfilecon(pathname, &oldsecontext) < 0)
goto err;
if (is_app_data_path(pathname)) {
if (pkgdir_selabel_lookup(pathname, seinfo,
uid, &secontext) < 0)
goto err;
}
if (strcmp(oldsecontext, secontext) != 0 && !nochange) {
if (lsetfilecon(pathname, secontext) < 0)
goto err;
}10.3 包列表回退
调用方未显式传 seinfo 时,libselinux 从 package list 哈希表按包名找到 appId 和 seinfo,再把路径中的 userid 合成为完整 UID。找不到 package 信息时返回错误,不会猜测 default。
源码文件:external/selinux/libselinux/src/android/android_device.c
if (!seinfo) {
info = package_info_lookup(pkgname);
if (!info) {
selinux_log(SELINUX_WARNING,
"Could not look up information for package %s, "
"cannot restorecon %s.\n",
pkgname, pathname);
free(pkgname);
return -1;
}
info->uid += userid_from_path * AID_USER_OFFSET;
}10.4 Lazy恢复
新目录递归 restorecon;已有目录先只标顶层并比较 before/after,只有标签变化或上次操作中断时才递归。seapp rule 更新后,顶层 Context 变化就是触发整个 App tree 修复的信号。
源码文件:frameworks/native/cmds/installd/InstalldNativeService.cpp
if (!existing) {
if (selinux_android_restorecon_pkgdir(
path.c_str(), seInfo.c_str(), uid,
SELINUX_ANDROID_RESTORECON_RECURSE) < 0) {
return -1;
}
return 0;
}
bool inProgress = getRestoreconInProgress(path);
std::string before, after;
if (!inProgress) {
before = lgetfilecon(path);
if (selinux_android_restorecon_pkgdir(
path.c_str(), seInfo.c_str(), uid, 0) < 0) {
return -1;
}
after = lgetfilecon(path);
}
if (inProgress || before != after) {
/* 用 user.restorecon_in_progress 保护递归恢复;实现省略。 */
}11. 当前规则
11.1 System与特殊UID
Android 17 平台规则包括 system_server startup、固定 system UID、isolated、SDK sandbox 和 PCC。这里不是完整清单,而是用于说明 selector 组合的真实条目。
源码文件:system/sepolicy/private/seapp_contexts
isSystemServer=true domain=system_server_startup
user=system seinfo=platform domain=system_app type=system_app_data_file
user=_isolated domain=isolated_app levelFrom=user
user=_isolated isIsolatedComputeApp=true domain=isolated_compute_app levelFrom=user
user=_sdksandbox domain=sdk_sandbox_34 type=sdk_sandbox_data_file levelFrom=all
user=_sdksandbox isSdkSandboxNext=true domain=sdk_sandbox_next type=sdk_sandbox_data_file levelFrom=all
user=_sdksandbox isSdkSandboxAudit=true domain=sdk_sandbox_audit type=sdk_sandbox_data_file levelFrom=all
user=_pcc_component domain=pcc_component type=pcc_data_file levelFrom=all11.2 平台与特权应用
Android 17 正在按 target SDK 将 platform/priv-app 切换到新 domain。target 37 进入 platform_app/priv_app,更早版本进入带 _36 后缀的兼容 domain。
源码文件:system/sepolicy/private/seapp_contexts
user=_app minTargetSdkVersion=37 seinfo=platform domain=platform_app type=app_data_file levelFrom=user
user=_app seinfo=platform domain=platform_app_36 type=app_data_file levelFrom=user
user=_app minTargetSdkVersion=37 isPrivApp=true domain=priv_app type=privapp_data_file levelFrom=user
user=_app isPrivApp=true domain=priv_app_36 type=privapp_data_file levelFrom=user由于 comparator 让更高 minTargetSdkVersion 优先,target 37 先命中新 rule;target 36 跳过后落到兼容 rule。不能按旧版本记忆把所有 platform/priv app 都归入同一个 domain。
11.3 Untrusted分级
源码文件:system/sepolicy/private/seapp_contexts
user=_app minTargetSdkVersion=37 domain=untrusted_app type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=34 domain=untrusted_app_34 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=32 domain=untrusted_app_32 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=30 domain=untrusted_app_30 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=29 domain=untrusted_app_29 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=28 domain=untrusted_app_27 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=26 domain=untrusted_app_27 type=app_data_file levelFrom=user
user=_app domain=untrusted_app_25 type=app_data_file levelFrom=user最后一条是 catch-all。target 28 与 26 虽然 domain 相同,range 不同:前者 levelFrom=all,后者 levelFrom=user。只看进程 type 会漏掉 App category 隔离变化。
11.4 Run-as
run-as 通过 seinfo token fromRunAs 进入 runas_app,并在 target 28 分界选择 all/user range。runas policy 需要 setcurrent、到非 system app domain 的 dyntransition,以及读取 seapp contexts 的权限。
源码文件:system/sepolicy/private/seapp_contexts
user=_app minTargetSdkVersion=28 fromRunAs=true domain=runas_app levelFrom=all
user=_app fromRunAs=true domain=runas_app levelFrom=user12. Neverallow
12.1 Seapp断言
seapp 文件自己的 neverallow 支持 PCRE,检查 selector/output 组合。例如 system_server domain 只能由 system server 获得,system_app 只能用于 system UID,ephemeral app 必须进入 ephemeral_app。
源码文件:system/sepolicy/private/seapp_contexts
neverallow isSystemServer=false domain=system_server
neverallow isSystemServer=false domain=system_server_startup
neverallow user=((?!system).)* domain=system_app
neverallow user=((?!system).)* type=system_app_data_file
neverallow isEphemeralApp=true domain=((?!ephemeral_app).)*
neverallow user=_isolated isIsolatedComputeApp=true domain=((?!isolated_compute_app).)*12.2 Regex匹配
checkseapp 为 neverallow value 自动加 ^ 和 $,使用 PCRE2 DOTALL 编译。普通 rule 仍不支持这些正则。
源码文件:system/sepolicy/tools/check_seapp.c
size = strlen(km->data) + 3;
anchored = alloca(size);
sprintf(anchored, "^%s$", km->data);
km->regex.compiled = pcre2_compile(
(PCRE2_SPTR)anchored,
PCRE2_ZERO_TERMINATED,
PCRE2_DOTALL,
errcode, erroff, NULL);12.3 Null选择器
neverallow 使用 key="" 表示 rule 未指定该 selector。检查器遍历真实 rule,若该 key 不存在就把 null condition 计为匹配;这使断言能够禁止“没有 seinfo 的命名规则”。
源码文件:system/sepolicy/tools/check_seapp.c
bool is_null_check = !strcmp(nrule->data, "\"\"");
for (i = 0; i < rm->length; i++) {
rule = &(rm->m[i]);
if (!strcmp(rule->name, nrule->name)) {
is_null_check = false;
if (match_regex(nrule, rule)) cnt++;
}
}
if (is_null_check) cnt++;13. 失败路径
13.1 构建失败
| 现象 | 首查位置 | 含义 |
|---|---|---|
| unknown/duplicate key | check_seapp.c::rule_map_new | 字段名错误或同一行重复字段 |
| duplicate input selector | rule_add / runtime duplicate check | 两条规则无法确定唯一输出 |
| invalid domain | validate_domain | domain 不存在于 compiled policy |
| missing app_data_file_type | validate_type | data type 不是允许的 App data 类型 |
| neverallow violation | rule_map_validate | selector/output 组合违反结构约束 |
| vendor coredomain | validate_domain with -c | vendor/odm 试图分配平台核心域 |
13.2 启动失败
domain lookup 没有匹配、seinfo 无法解析、category 超界、生成 Context 非法或 setcon 被 dyntransition 拒绝,都会让 native specialize 调用 fail_fn。AMS 捕获进程启动 RuntimeException 后清理 pending bookkeeping,并 force-stop package。
源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java
} catch (RuntimeException e) {
Slog.e(ActivityManagerService.TAG,
"Failure starting process " + app.processName, e);
mService.forceStopPackageLocked(
app.info.packageName,
UserHandle.getAppId(app.uid),
false, false, true, false, false, false,
app.userId, "start failure");
return false;
}13.3 数据恢复失败
App data 路径解析失败、packages.list 找不到包、type lookup 失败、生成 Context 非法或 xattr 写入失败,都会使 installd 返回 Failed to restorecon。这不会通过修改进程 domain 自动恢复;需要检查 data type 和 range 输出。
13.4 分区不一致
system 预装 App 命中 vendor policy 当前会产生 warning。若同时涉及 coredomain,构建期 -c 可能直接失败。排查时应同时记录 App 安装分区与命中 rule 的来源分区。
14. 测试证据
14.1 双输出测试
libselinux 单元测试用同一条 rule 分别查询 SEAPP_DOMAIN 与 SEAPP_TYPE。UID 10001 被归类为 _app、appid 1、userid 0,levelFrom=user 生成 c512,c768。
源码文件:external/selinux/libselinux/src/android/android_unittest.cpp
int ret = LoadSeAppContexts(
"user=_app seinfo=platform domain=platform_app "
"type=app_data_file levelFrom=user\n");
EXPECT_EQ(ret, 0);
context_t ctx = context_new("u:r:unknown");
ret = seapp_context_lookup_internal(
SEAPP_DOMAIN, 10001, false,
"platform", "com.android.test1", ctx);
EXPECT_EQ(ret, 0);
EXPECT_STREQ(context_str(ctx),
"u:r:platform_app:s0:c512,c768");
context_free(ctx);
ctx = context_new("u:r:unknown_data_file");
ret = seapp_context_lookup_internal(
SEAPP_TYPE, 10001, false,
"platform", "com.android.test1", ctx);
EXPECT_EQ(ret, 0);
EXPECT_STREQ(context_str(ctx),
"u:r:app_data_file:s0:c512,c768");它证明同一 selector 的两种 kind 只替换各自输出,并共享 range;没有覆盖 Zygote setcon 或磁盘 xattr。
14.2 Boolean默认值
测试为 priv/ephemeral 指定 false rule、特殊 boolean 的显式 false rule和 catch-all。isEphemeralApp 的“已指定 false”比 isPrivApp=false 优先;而默认 false 的 isolated/SDK/run-as 显式写 false 会与 catch-all 构成 duplicate,loader 返回 -1。
源码文件:external/selinux/libselinux/src/android/android_unittest.cpp
int ret = LoadSeAppContexts(
"user=_app isPrivApp=false domain=noprivapp "
"type=x levelFrom=user\n"
"user=_app isEphemeralApp=false domain=noephemeralapp "
"type=x levelFrom=user\n"
"user=_app domain=catchall_app type=x levelFrom=user\n");
EXPECT_EQ(ret, 0);
ExpectContextForSeInfo(
"default:partition=system:complete",
"u:r:noephemeralapp:s0:c512,c768");
/* isIsolatedComputeApp=false + catch-all 被断言为 duplicate。 */这个测试解释了为何 comparator 对 isEphemeralAppSet 有独立优先级,而部分其他 boolean 没有 set-state 字段。
14.3 Seinfo解析
源码文件:external/selinux/libselinux/src/android/android_unittest.cpp
string seinfo =
"default:privapp:targetSdkVersion=10000:partition=system:complete";
int ret = parse_seinfo(seinfo.c_str(), &info);
EXPECT_EQ(ret, 0);
EXPECT_STREQ(info.base, "default");
EXPECT_EQ(info.targetSdkVersion, 10000);
EXPECT_EQ(info.is, IS_PRIV_APP);
EXPECT_EQ(info.isPreinstalledApp, true);
EXPECT_STREQ(info.partition, "system");
seinfo = "default:targetSdkVersion:complete";
EXPECT_EQ(parse_seinfo(seinfo.c_str(), &info), -1);它覆盖正常 token 与缺少 = 值的错误输入;不证明 PackageManager 拼接顺序。
14.4 PM测试
SELinuxMMACTest mock PackageState 和 PlatformCompat,断言 compat opt-in 会抬高 target SDK token,预装分区会追加 partition,非预装 App 不追加。
源码文件:frameworks/base/services/tests/servicestests/src/com/android/server/pm/SELinuxMMACTest.java
assertThat(SELinuxMMAC.getSeInfo(
packageState, packageState.getAndroidPackage(),
null, mMockCompatibility),
is("default:targetSdkVersion=" + LATEST_OPT_IN_VERSION));
assertThat(SELinuxMMAC.getSeInfo(
systemPackage, systemPackage.getAndroidPackage(),
null, mMockCompatibility),
containsString(":partition=system"));15. 验证路径
15.1 构建检查
# 初始化 Android 构建环境并选择实际产品。
source build/envsetup.sh
lunch PRODUCT-userdebug
# 构建五个分区的 seapp 产物;每个目标都会运行 checkseapp。
m plat_seapp_contexts system_ext_seapp_contexts \
product_seapp_contexts vendor_seapp_contexts odm_seapp_contexts若失败,先读 checkseapp 的文件/行号、invalid type、neverallow 或 coredomain 消息。不能用调整文本行顺序修复 runtime precedence,因为 loader 会重新排序。
15.2 单元测试
# libselinux 测试覆盖 seapp loader、lookup、boolean 和 seinfo parser。
atest libselinux_test
# PackageManager 测试覆盖 target SDK compat 与安装分区 token。
atest SELinuxMMACTest测试目标需要出现在所选产品/测试配置中。第一组不启动真实 App,第二组不执行 native lookup;二者结合仍不能替代设备闭环。
15.3 设备观察
# 查 PackageManager 保存的 seinfo、uid、targetSdk 与安装路径。
adb shell dumpsys package PACKAGE_NAME | \
rg 'userId=|seinfo=|targetSdk=|codePath='
# 查进程 domain 与完整 MLS range。
adb shell ps -AZ | rg 'PACKAGE_NAME|PROCESS_NAME'
# 查 CE/DE App data 顶层 type 与 range。
adb shell ls -Zd /data/user/USER_ID/PACKAGE_NAME
adb shell ls -Zd /data/user_de/USER_ID/PACKAGE_NAMEdumpsys package 字段随版本和权限变化;输出缺少 seinfo 时应改查 PackageManager debug 输出或 packages.list,而不是推断为 default。ps -AZ 只能观察已启动进程,ls -Zd 需要足够权限。
15.4 Category实验
# 用与 set_range_from_level 相同的位运算复算 UID 1010045。
uid=1010045
userid=$((uid / 100000))
appid=$((uid % 100000 - 10000))
printf 's0:c%d,c%d,c%d,c%d\n' \
"$((appid & 255))" \
"$((256 + ((appid >> 8) & 255)))" \
"$((512 + (userid & 255)))" \
"$((768 + ((userid >> 8) & 255)))"预期输出是 s0:c45,c256,c522,c768。这只复算 category 公式,不证明某条 rule 实际选择了 levelFrom=all。
16. 源码导航
| 问题 | 首选源码 | 关键符号 |
|---|---|---|
| 平台 selector 与当前规则 | system/sepolicy/private/seapp_contexts | 文件头契约、真实 entries、neverallow |
| 分区 module 如何生成 | system/sepolicy/contexts/Android.bp | plat/vendor/odm_seapp_contexts |
| M4 与 checkseapp 如何串联 | system/sepolicy/build/soong/selinux_contexts.go | buildSeappContexts |
| 输出 type/domain 如何校验 | system/sepolicy/tools/check_seapp.c | validate_type、validate_domain |
| runtime 如何读五个分区 | external/selinux/libselinux/src/android/android_seapp.c | seapp_context_paths、reload |
| 优先级如何计算 | 同上 | seapp_context_cmp |
| seinfo 怎样解析 | 同上 | parse_seinfo |
| UID 怎样分类 | 同上 | seapp_context_lookup_internal |
| MLS categories 怎样计算 | 同上 | set_range_from_level |
| Zygote 怎样切换 domain | external/selinux/libselinux/src/android/android_device.c | selinux_android_setcontext |
| Java 怎样传递 seinfo | frameworks/base/services/core/java/com/android/server/am/ProcessList.java | updateSeInfo、startProcess |
| seinfo base 从哪里来 | frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java | readInstallPolicy、getSeInfo |
| App data 怎样选择 type | external/selinux/libselinux/src/android/android_device.c | pkgdir_selabel_lookup |
| Installd 何时提交标签 | frameworks/native/cmds/installd/InstalldNativeService.cpp | createAppDataDirs、lazy restorecon |
| Zygote 动态切换权限 | system/sepolicy/private/zygote.te | setcurrent、dyntransition |
给定一个 target 34、普通签名、非 priv、安装在 userdata 的 App,完整复述应包括:PackageManager 生成 default:targetSdkVersion=34 并追加 per-user complete;AMS 把它和 UID、包名传给 Zygote;libselinux 将 UID 分类为 _app,按排序命中 minTargetSdkVersion=34 的 untrusted_app_34 rule,计算 levelFrom=all 后 setcon;installd 为 CE/DE 目录再次以 SEAPP_TYPE 命中同一 rule,把基础 file Context 改为 app_data_file 和相同 categories。任何一侧缺少匹配,都必须按各自的 domain/type 失败语义处理,不能用另一侧“看起来正确”代替验证。
