MLS 多级安全
本文面向已经理解 SELinux type/domain 和 AVC 四元组的读者。建议先阅读 安全上下文、seapp_contexts 和 AVC Denial 日志。本文不把 MLS 当作军事分级概念列表,而是追踪 Android 17 如何生成 s0:c...、Zygote 在何时设置应用 context、内核如何比较 category 集合,以及为什么已有 TE allow 仍可能被 mlsconstrain 拒绝。
Android 17 的配置使用一个 sensitivity 和 1024 个 category。也就是说,大多数安全上下文都处于 s0,隔离信息主要由 category 集合承载;这更接近 MCS(Multi-Category Security)的使用方式。两个应用可以同属 untrusted_app domain,却因 category 不同而无法打开对方的 app_data_file。type 决定“是什么对象”,category 进一步决定“属于哪个应用/用户隔离集合”。
1. Android模型
1.1 构建常量
源码文件:system/sepolicy/build/soong/policy.go
const (
MlsSens = 1
MlsCats = 1024
PolicyVers = 30
)
rule.Command().Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
FlagWithArg("-D mls_num_sens=", strconv.Itoa(MlsSens)).
FlagWithArg("-D mls_num_cats=", strconv.Itoa(c.mlsCats()))Soong 把 sensitivity/category 数量作为 M4 定义注入 policy.conf。默认只有 s0,category 范围为 c0 到 c1023;module 可以通过 Mls_cats 改 category 数,但 sensitivity 固定为 1。它们在策略编译时生效,不是设备运行属性。
1.2 内核结构
源码文件:kernel/common/security/selinux/ss/mls_types.h
struct mls_level {
u32 sens; /* sensitivity */
struct ebitmap cat; /* category set */
};
struct mls_range {
struct mls_level level[2]; /* low == level[0], high == level[1] */
};
static inline int mls_level_eq(const struct mls_level *l1,
const struct mls_level *l2)
{
return ((l1->sens == l2->sens) &&
ebitmap_equal(&l1->cat, &l2->cat));
}
static inline int mls_level_dom(const struct mls_level *l1,
const struct mls_level *l2)
{
return ((l1->sens >= l2->sens) &&
ebitmap_contains(&l1->cat, &l2->cat, 0));
}一个 level 是 sensitivity 数值加 category bitmap;range 保存 low/high 两个 level。eq 要求 sensitivity 和 category 集合完全相等;dom 要求 sensitivity 不低且 category 是对方的超集。在 Android 的单 s0 配置里,支配关系主要退化为 category 集合包含关系。
1.3 判定图
2. 声明生成
2.1 M4生成器
源码文件:system/sepolicy/private/mls_macros
define(`decl_cats',`dnl
category c$1;
ifelse(`$1',`$2',,`decl_cats(incr($1),$2)')dnl
')
define(`gen_cats',`decl_cats(0,decr($1))')
define(`decl_sens',`dnl
sensitivity s$1;
ifelse(`$1',`$2',,`decl_sens(incr($1),$2)')dnl
')
define(`gen_dominance',`s$1 ifelse(`$1',`$2',,`gen_dominance(incr($1),$2)')')
define(`gen_sens',`
decl_sens(0,decr($1))
dominance { gen_dominance(0,decr($1)) }
')
define(`decl_levels',`dnl
level s$1:c0.c$3;
ifelse(`$1',`$2',,`decl_levels(incr($1),$2,$3)')dnl
')
define(`gen_levels',`decl_levels(0,decr($1),decr($2))')M4 递归生成 category、sensitivity、dominance 和 level 声明。输入 1,1024 时只声明 s0,并让 s0 的合法 category 上限覆盖 c0.c1023。这些宏只生成策略语言文本,真正的 bitmap 在 policy 编译/加载时建立。
2.2 调用点
源码文件:system/sepolicy/private/mls_decl
gen_sens(mls_num_sens)
gen_cats(mls_num_cats)
gen_levels(mls_num_sens,mls_num_cats)mls_decl 本身没有硬编码数字,所有规模来自 Soong M4 定义。users 文件再授权 Android SELinux user u 使用从 s0 到 mls_systemhigh 的范围,使系统生成的进程/对象 context 能通过内核 user-range 校验。
2.3 context输出
源码文件:kernel/common/security/selinux/ss/mls.c
void mls_sid_to_context(struct policydb *p, struct context *context,
char **scontext)
{
char *scontextp = *scontext;
if (!p->mls_enabled)
return;
*scontextp++ = ':';
for (int l = 0; l < 2; l++) {
strcpy(scontextp, sym_name(p, SYM_LEVELS,
context->range.level[l].sens - 1));
scontextp += strlen(scontextp);
/* The source's category-bitmap formatting loop is omitted here. */
if (l == 0 &&
mls_level_eq(&context->range.level[0],
&context->range.level[1]))
break;
if (l == 0)
*scontextp++ = '-';
}
*scontext = scontextp;
}这里省略的是源码中遍历 category bitmap、压缩连续 category 为 cX.cY 的循环。函数把内核 range 转成日志和用户空间看到的 :s0:c...;low/high 相等时只输出一个 level,不相等时用 - 分隔 range。
3. 应用标签
3.1 seapp契约
源码文件:system/sepolicy/private/seapp_contexts
levelFrom and level are used to determine the level (sensitivity + categories)
for MLS/MCS.
levelFrom=none omits the level.
levelFrom=app determines the level from the process UID.
levelFrom=user determines the level from the user ID.
levelFrom=all determines the level from both UID and user ID.
levelFrom=user is only supported for _app or _isolated UIDs.
levelFrom=app or levelFrom=all is only supported for _app UIDs.
level may be used to specify a fixed level for any UID.这是 level 生成的公开配置契约。app、user、all 指定参与 category 派生的身份维度;它没有给出具体 category 编码公式。当前 checkout 不包含 selinux_android_setcontext 的 libselinux 实现,因此本文不根据记忆声称某个 UID 必然映射到具体 cX,cY。
3.2 真实规则
源码文件:system/sepolicy/private/seapp_contexts
user=_isolated domain=isolated_app levelFrom=user
user=_sdksandbox domain=sdk_sandbox_34 type=sdk_sandbox_data_file levelFrom=all
user=_app seinfo=app_zygote domain=app_zygote levelFrom=user
user=_app seinfo=media domain=mediaprovider type=app_data_file levelFrom=user
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 domain=untrusted_app_25 type=app_data_file levelFrom=user新 untrusted app 规则通常使用 levelFrom=all,较旧兼容域可能只按 user 派生;isolated app 使用 user 维度。domain/type 选择和 level 选择发生在同一条 seapp 规则中,所以“同 domain 不同 category”是配置设计的一部分,而不是内核自动给所有进程随机分类。
3.3 配置校验
源码文件:system/sepolicy/tools/check_seapp.c
static bool validate_levelFrom(char *value,
const char *filename,
int lineno,
char **errmsg) {
if (strcasecmp(value, "none") && strcasecmp(value, "all") &&
strcasecmp(value, "app") && strcasecmp(value, "user")) {
*errmsg = "Expecting one of: \"none\", \"all\", \"app\" or \"user\"";
return false;
}
return true;
}构建工具只接受四个枚举值,非法字符串会在生成 seapp_contexts 时失败。它验证配置语法和部分 UID 约束,不证明运行时进程最终拿到的 category 与预期一致;后者需要查看 ps -Z 或 /proc/<pid>/attr/current。
3.4 Zygote消费者
源码文件: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));
}Zygote specialization 在 UID/GID、能力等状态准备完成后调用 libselinux,传入 uid、system_server 标志、seinfo 和进程名。libselinux 消费编译后的 seapp_contexts,选择 domain 与 level;失败会中止子进程 specialization,而不是继续使用 zygote context。
4. MLS约束
4.1 TE之后
mlsconstrain 是 policydb constraint,不是普通 allow。内核先计算 TE allow vector,再用 constraint expression 削减不满足条件的权限;所以 sesearch 查到 allow 仍可能出现 AVC。日志通常仍显示 source/target type、class、permission,但需要额外比较 level/category。
源码文件:system/sepolicy/private/mls
mlsconstrain process { transition dyntransition }
((h1 eq h2 and l1 eq l2) or t1 == mlstrustedsubject);
mlsconstrain process { getsched getsession getpgid getcap getattr ptrace share }
(l1 dom l2 or t1 == mlstrustedsubject);
mlsconstrain unix_stream_socket { connectto }
(l1 eq l2 or t1 == mlstrustedsubject or t2 == mlstrustedsubject);l1/h1 是 source low/high,l2/h2 是 target low/high,t1/t2 是 source/target type。受信 subject/object 是显式旁路,必须回到 attribute 成员验证,不能从进程名猜测。
4.2 App数据约束
源码文件:system/sepolicy/private/mls
mlsconstrain dir { open search getattr setattr rename add_name remove_name reparent rmdir }
(t2 != app_data_file_type or l1 dom l2 or t1 == mlstrustedsubject);
mlsconstrain { file sock_file } { open setattr unlink link rename }
((t2 != app_data_file_type and t2 != appdomain_tmpfs)
or l1 dom l2 or t1 == mlstrustedsubject);
mlsconstrain { lnk_file } { open setattr unlink link rename read }
((t2 != app_data_file_type or t2 == privapp_data_file)
or l1 eq l2 or t1 == mlstrustedsubject);对 app data 的关键门槛发生在 open/search/操作目录项时。普通 app data 要求 source low level 支配 object low level;symlink 更严格地要求等价,防止跨 category 跟随链接。注释明确说没有约束已打开 fd 的 read/write,这也是为什么排查要区分“打开失败”和“已有 fd I/O”。
4.3 其他文件约束
mlsconstrain { file lnk_file sock_file chr_file blk_file }
{ read getattr execute }
(t2 == app_data_file_type or t2 == appdomain_tmpfs
or l1 dom l2 or t1 == mlstrustedsubject or t2 == mlstrustedobject);
mlsconstrain { file lnk_file sock_file chr_file blk_file }
{ write setattr append unlink link rename }
(t2 == app_data_file_type or t2 == appdomain_tmpfs
or l1 eq l2 or t1 == mlstrustedsubject or t2 == mlstrustedobject);非 app-data 读取通常要求 dominance,写入要求 equivalence,除非主体或对象被标记为 MLS trusted。这里的 “no read up/no write down” 在 Android 单 sensitivity 下主要表现为 category 超集/等价判断。
4.4 Binder例外
源码文件:system/sepolicy/private/mls
# Presently commented out, as apps are expected to call one another.
#mlsconstrain binder call
# (l1 eq l2 or t1 == mlstrustedsubject or t2 == mlstrustedsubject);Android 没有启用 Binder call 的 category 等价约束,因为应用需要跨 category IPC;应用数据隔离主要落在文件/目录等对象上。不能从“不同 category 无法互访 app data”外推出“不同应用完全不能 Binder 通信”。
5. 内核执行
5.1 表达式求值
源码文件:kernel/common/security/selinux/ss/services.c
case CEXPR_L1L2:
l1 = &scontext->range.level[0];
l2 = &tcontext->range.level[0];
goto mls_ops;
case CEXPR_H1H2:
l1 = &scontext->range.level[1];
l2 = &tcontext->range.level[1];
goto mls_ops;
mls_ops:
switch (e->op) {
case CEXPR_EQ:
s[++sp] = mls_level_eq(l1, l2);
continue;
case CEXPR_DOM:
s[++sp] = mls_level_dom(l1, l2);
continue;
case CEXPR_DOMBY:
s[++sp] = mls_level_dom(l2, l1);
continue;
case CEXPR_INCOMP:
s[++sp] = mls_level_incomp(l2, l1);
continue;
}编译后的 constraint expression 是一棵栈式布尔表达式。CEXPR_L1L2 等节点选择 source/target range 的 level,再调用 eq/dom/domby/incomp;AND/OR/NOT 组合结果。表达式求值为 false 时,对应 permission 从 access vector 中移除。
5.2 range有效性
源码文件:kernel/common/security/selinux/ss/mls.c
int mls_level_isvalid(struct policydb *p, struct mls_level *l)
{
struct level_datum *levdatum;
if (!l->sens || l->sens > p->p_levels.nprim)
return 0;
levdatum = symtab_search(&p->p_levels,
sym_name(p, SYM_LEVELS, l->sens - 1));
if (!levdatum)
return 0;
return ebitmap_contains(&levdatum->level.cat, &l->cat,
p->p_cats.nprim);
}
int mls_range_isvalid(struct policydb *p, struct mls_range *r)
{
return mls_level_isvalid(p, &r->level[0]) &&
mls_level_isvalid(p, &r->level[1]) &&
mls_level_dom(&r->level[1], &r->level[0]);
}合法 level 必须引用已声明 sensitivity,category bitmap 不能超出该 level 与 policy 的范围;range 的 high 必须支配 low。无效 context 在 SID 转换/策略加载阶段失败,不会作为一个“奇怪但可运行”的标签存在。
5.3 user授权
mls_context_isvalid 对 object role 可直接接受有效 range;对进程 context 还会查 SELinux user 的授权 range,要求 user range 包含该 context range。Android 常用 user u 的 policy range 覆盖系统低到系统高,这为 seapp 派生的应用 category 提供合法空间。
6. 排查方法
6.1 先比较level
# Read-only: inspect process and object contexts including categories.
adb shell 'ps -AZ | grep <process-name>'
adb shell 'ls -Zd <app-data-path> <target-path>'
adb shell 'cat /proc/<pid>/attr/current'
# Read-only: query TE allow and trusted attributes from a policy binary.
searchpolicy <policy> --allow -s <source-type> -t <target-type> \
-c <class> -p <permission>
sepolicy-analyze <policy> attribute -r <source-type>若 TE allow 存在但 category 不满足 eq/dom,继续加相同 allow 没有作用。应确认 source process 和 app data directory 是否来自同一个 seapp rule/UID,是否发生错误 restorecon,或 source 是否本应属于 mlstrustedsubject。
6.2 常见误判
| 现象 | 常见误判 | 正确方向 |
|---|---|---|
同为 untrusted_app 仍拒绝 | domain 相同就应允许 | 比较 category 与 app_data type |
sesearch 找到 allow | SELinux 不应拒绝 | 检查 mlsconstrain/constraint |
| 不同应用可 Binder 通信 | MLS 失效 | Binder MLS constraint 未启用 |
s0 相同 | level 完全相等 | 还需比较 category bitmap |
修改 levelFrom 后失败 | 内核 bug | 先过 check_seapp UID/枚举校验 |
| category 显示范围 | sensitivity 更高 | cX.cY 是连续 category 压缩 |
6.3 修改闭环
修改 seapp levelFrom 会影响进程 domain context 和 app data context 的 category,风险高于普通 allow。至少验证:check_seapp 构建通过;Zygote specialization 成功;进程和数据目录 category 一致;同 UID/跨用户/isolated/sdk sandbox 场景没有回归;enforcing 下原访问与跨应用隔离都符合预期。
7. 读码练习
选择两个同属 untrusted_app 但 UID/用户不同的应用:
- 从
seapp_contexts找到各自匹配规则及levelFrom; - 用
ps -Z和ls -Z记录进程、数据目录的完整 category; - 找到一条 app data allow,并说明为什么它仍受
mlsconstrain限制; - 用
mls_level_dom的 bitmap 包含关系判断 source 是否支配 object; - 解释 Binder call 为什么不受同一 category 等价约束;
- 给出一个不破坏跨应用数据隔离的修复方向,而不是把 source 加进
mlstrustedsubject。
8. 源码导航
system/sepolicy/build/soong/policy.go:Android 的 sensitivity/category 构建常量。system/sepolicy/private/mls_macros、mls_decl:MLS 声明生成。system/sepolicy/private/seapp_contexts:应用 domain/type 与levelFrom契约。system/sepolicy/tools/check_seapp.c:levelFrom 枚举和规则合法性检查。frameworks/base/core/jni/com_android_internal_os_Zygote.cpp:应用 specialization 调用selinux_android_setcontext。system/sepolicy/private/mls:process/socket/file/app data 的 MLS constraints。kernel/common/security/selinux/ss/mls_types.h:level/range 数据结构和 eq/dom/incomp。kernel/common/security/selinux/ss/mls.c:context 字符串、level/range 有效性。kernel/common/security/selinux/ss/services.c:constraint expression 求值和 access vector 收敛。
