Skip to content

MLS 多级安全

从 Android 的单一 sensitivity、1024 个 category、seapp levelFrom、内核 MLS 比较和 mlsconstrain 出发,解释同域应用的数据隔离。

基于android-17.0.0_r1
AndroidSELinuxMLSMCS应用隔离源码阅读

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

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

c
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

yaml
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

yaml
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

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

text
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

text
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

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

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

text
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

text
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 其他文件约束 ​

text
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

yaml
# 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

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

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 ​

sh
# 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 找到 allowSELinux 不应拒绝检查 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/用户不同的应用:

  1. 从 seapp_contexts 找到各自匹配规则及 levelFrom;
  2. 用 ps -Z 和 ls -Z 记录进程、数据目录的完整 category;
  3. 找到一条 app data allow,并说明为什么它仍受 mlsconstrain 限制;
  4. 用 mls_level_dom 的 bitmap 包含关系判断 source 是否支配 object;
  5. 解释 Binder call 为什么不受同一 category 等价约束;
  6. 给出一个不破坏跨应用数据隔离的修复方向,而不是把 source 加进 mlstrustedsubject。

8. 源码导航 ​

  1. system/sepolicy/build/soong/policy.go:Android 的 sensitivity/category 构建常量。
  2. system/sepolicy/private/mls_macros、mls_decl:MLS 声明生成。
  3. system/sepolicy/private/seapp_contexts:应用 domain/type 与 levelFrom 契约。
  4. system/sepolicy/tools/check_seapp.c:levelFrom 枚举和规则合法性检查。
  5. frameworks/base/core/jni/com_android_internal_os_Zygote.cpp:应用 specialization 调用 selinux_android_setcontext。
  6. system/sepolicy/private/mls:process/socket/file/app data 的 MLS constraints。
  7. kernel/common/security/selinux/ss/mls_types.h:level/range 数据结构和 eq/dom/incomp。
  8. kernel/common/security/selinux/ss/mls.c:context 字符串、level/range 有效性。
  9. kernel/common/security/selinux/ss/services.c:constraint expression 求值和 access vector 收敛。