Skip to content

安全上下文

从 SID、进程与 inode 安全状态,到 file_contexts、seapp_contexts 和 proc 属性接口,追踪 Android 安全上下文的完整生命周期。

基于android-17.0.0_r1
AndroidSELinux安全上下文SIDseapp_contexts源码阅读

安全上下文 ​

本文面向已经读过 SELinux架构、DAC与MAC 和 LSM框架 的读者。前文已经说明 SELinux 需要 source context、target context、object class 和 permission;本文继续回答这些 context 从哪里来、如何在内核中表示、何时附着到进程或 inode、怎样通过 /proc 与 ls -Z 读出来,以及 Android 的 seapp_contexts 为什么会把同一个 UID 的应用映射到不同 domain。

本文不把 u:r:domain:s0 当成一串静态字符串来背诵,也不展开完整 TE、RBAC、MLS 规则语法。文章沿四条可以回到源码的主线组织:内核用 SID 和 security blob 保存状态;init 用 file_contexts 与 restorecon 给文件对象定标签;Zygote 用 seapp_contexts 输入选择应用进程 domain;用户空间通过 proc 属性和 selabel_lookup() 读取或查询这些状态。读完后,你应能区分“字符串解析成功”“SID 已分配”“对象已附着标签”“策略裁决已消费”四个时刻。

1. 四个表示 ​

同一个上下文在不同层次有不同表示,不能互相替换:

表示示例所有者主要消费者
上下文字符串u:r:system_server:s0libselinux、proc 接口、日志人和工具、配置文件
SID内核整数标识SELinux sidtab/stateAVC、hook、security server
cred/task security blobcred 中的 sid、osid、exec_sid,以及 task 的 AVC 目录缓存struct cred / task blobexec、capable、Binder、进程查询
inode security blobinode 的 sid、sclass、初始化状态struct inode / inode blobVFS、file hook、AVC

上下文字符串是外部可读格式,SID 是内核快速查找键。security_context_to_sid() 把字符串转换为 SID,security_sid_to_context() 做反向转换;如果只看到日志中的 context 字符串,不能直接推断内核中 SID 数值或对象 blob 已经初始化。

2. 内核状态 ​

2.1 task状态 ​

SELinux 把进程身份附着在 struct cred 的 LSM 复合 blob 中;struct task_struct 另有 task-local AVC 目录缓存。两者都属于 SELinux 的 LSM blob,但字段职责不同。

源码文件:kernel/common/security/selinux/include/objsec.h

相关结构:cred_security_struct、task_security_struct

c
struct cred_security_struct {
    u32 osid;
    u32 sid;
    u32 exec_sid;
    u32 create_sid;
    u32 keycreate_sid;
    u32 sockcreate_sid;
};

struct task_security_struct {
    struct {
        u32 sid;
        u32 seqno;
        unsigned int dir_spot;
        struct avdc_entry dir[TSEC_AVDC_DIR_SIZE];
        bool permissive_neveraudit;
    } avdcache;
};

cred_security_struct.sid 是当前 task 身份;osid 记录 exec 前身份,便于审计和域转换;exec_sid 是显式 exec context 的候选值;create_sid、keycreate_sid、sockcreate_sid 分别影响新建文件、key 和 socket 的标签。task_security_struct.avdcache 则缓存当前 task 的目录访问决策,不是第二份进程 domain。它们不是同时生效的多个 domain,只有相应 hook 使用某个字段时才参与状态转换。

源码文件:kernel/common/security/selinux/hooks.c

相关函数:cred_init_security、cred_sid

c
static void __init cred_init_security(void)
{
    struct cred_security_struct *crsec;

    crsec = selinux_cred(unrcu_pointer(current->real_cred));
    crsec->osid = crsec->sid = SECINITSID_KERNEL;
}

static inline u32 cred_sid(const struct cred *cred)
{
    const struct cred_security_struct *crsec;

    crsec = selinux_cred(cred);
    return crsec->sid;
}

系统最初的 task 使用 SECINITSID_KERNEL,不是 Android 用户空间最终看到的 u:r:init:s0。first-stage init 运行在 kernel domain,策略加载、restorecon(/system/bin/init) 和重新 exec 后,用户空间 init 才进入 Android init domain。把启动早期的 kernel SID 与 second-stage init context 混为一谈,会误判策略加载时机。

2.2 inode状态 ​

文件对象的标签附着在 inode security blob。inode 的 sclass 决定同一个 SID 在当前对象上按 file、dir、socket 等哪类权限解释。

源码文件:kernel/common/security/selinux/include/objsec.h

相关结构:inode_security_struct、file_security_struct

c
struct inode_security_struct {
    struct inode *inode;
    struct list_head list;
    u32 task_sid;
    u32 sid;
    u16 sclass;
    unsigned char initialized;
    spinlock_t lock;
};

struct file_security_struct {
    u32 sid;
    u32 fown_sid;
    u32 isid;
    u32 pseqno;
};

这里的 task_sid 是创建或初始化 inode 时关联的 task 身份,sid 才是该 inode 当前 target 标签。打开文件后,file_security_struct 保存打开者相关 SID、打开时 inode SID 和 policy sequence,用于后续 file permission 重验证。文件描述符继承不等于重新根据路径查标签。

2.3 sidtab ​

SID 到字符串的映射由 sidtab/security server 持有。访问控制路径只携带 SID,只有日志、proc 或用户空间 API 需要字符串时才转换。

源码文件:kernel/common/security/selinux/ss/services.c

相关函数:security_context_to_sid

c
int security_context_to_sid(
        const char *scontext, u32 scontext_len,
        u32 *sid, gfp_t gfp)
{
    return security_context_to_sid_core(
            scontext, scontext_len, sid,
            SECSID_NULL, gfp, 0);
}

策略未加载时,内部实现只接受初始 SID 字符串并回退到 SECINITSID_KERNEL;这不是任意字符串都已经成为有效 domain。策略加载完成后,sidtab 才按当前 policy 的 type/role/level 定义分配或查找 SID;字符串不合法、类型不存在或策略状态不允许时会返回错误。

3. 字段语义 ​

3.1 user与role ​

Android 常见上下文的 user 和 role 字段通常是 u、r 或 object_r。它们参与完整 SELinux context 解析,但 Android 日常 TE 授权主要围绕 type/domain 展开。

上下文userroletype/domainlevel
u:r:init:s0urinits0
u:r:system_server:s0ursystem_servers0
u:object_r:system_file:s0uobject_rsystem_files0
u:object_r:activity_service:s0uobject_ractivity_services0

进程上下文使用进程 role,文件、服务名和属性等客体通常使用 object_r。role 不是 Linux UID,也不是 Android user id;u:r:untrusted_app:s0:c512,c768 中的 u 不表示该进程属于 Android user 0。

3.2 type与domain ​

同一个 type 语法在主体和客体上承担不同角色:进程 type 称为 domain,文件或服务 type 是 target type。allow 规则以 type 为主键,role/user/level 仍可能通过约束参与最终判断。

源码文件:system/sepolicy/private/init.te

相关规则:init domain 与 capability

text
typeattribute init coredomain;
init_daemon_domain(init)

allow init self:global_capability_class_set {
  dac_override
  dac_read_search
  sys_admin
  sys_chroot
};

init_daemon_domain(init) 不只是声明字符串,它展开为 init 可执行文件、domain transition 和进程启动相关的多条规则。后续文章会专门展开 type、attribute 和宏,本文只需记住 domain 是运行中 task 的 type,而不是一个独立于 SID 的第二身份。

3.3 level与MCS ​

Android 通常使用 s0 sensitivity,并用 MCS categories 区分应用数据。level 的具体生成由 seapp_contexts 的 levelFrom 选择器决定:app 使用 app id,user 使用 Android user id,all 同时使用两者,none 不附加 level。

源码文件:system/sepolicy/private/seapp_contexts

相关规则:levelFrom 与应用 domain

text
user=_app seinfo=platform domain=platform_app \
    type=app_data_file levelFrom=user
user=_app isEphemeralApp=true domain=ephemeral_app \
    type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=37 domain=untrusted_app \
    type=app_data_file levelFrom=all

levelFrom 不是标签字符串中的固定文字,而是 libselinux 根据应用输入和 UID 计算 level 的指令。不同 Android user 的相同 app id 可以得到不同 level;同一 user 中的不同 app id 也可以通过 all 产生不同 categories。不要用一个示例 s0:c... 推断所有设备的 category 分配算法。

4. 文件标签 ​

4.1 规则来源 ​

file_contexts 的左侧是路径或正则,右侧是期望安全上下文。它描述“某路径应该是什么标签”,不等于磁盘 inode 已经被写入该标签。

源码文件:system/sepolicy/private/file_contexts

相关规则:关键 Android 路径

text
/system/bin/init             u:object_r:init_exec:s0
/system/bin/app_process32    u:object_r:zygote_exec:s0
/system/bin/app_process64    u:object_r:zygote_exec:s0
/system/bin/servicemanager   u:object_r:servicemanager_exec:s0
/dev/socket/zygote            u:object_r:zygote_socket:s0
/dev/socket/zygote_secondary  u:object_r:zygote_socket:s0
/dev/socket/property_service  u:object_r:property_socket:s0

路径命中数据库后,init 或其他调用者还要执行 selinux_android_restorecon(),把期望 context 应用到文件系统对象。对于 procfs、sysfs 等伪文件系统,标签可能来自 genfs contexts 或内核 filesystem labeling,而不是 ext4 xattr。

4.2 查询与重标 ​

init 为 file contexts 建立并缓存 selabel_handle,查询函数返回字符串;restorecon 则使用相同的数据库对对象执行重标。

源码文件:system/core/init/selabel.cpp

相关函数:SelabelInitialize、SelabelLookupFileContext

cpp
namespace {
selabel_handle* sehandle = nullptr;
}

void SelabelInitialize() {
    sehandle = selinux_android_file_context_handle();
    selinux_android_set_sehandle(sehandle);
}

bool SelabelLookupFileContext(
        const std::string& key, int type,
        std::string* result) {
    result->clear();

    if (!sehandle)
        return true;

    char* context;
    if (selabel_lookup(
            sehandle, &context, key.c_str(), type) != 0) {
        return false;
    }
    *result = context;
    free(context);
    return true;
}

selabel_lookup() 的 owner 是内存中的 label handle,消费者拿到的是期望字符串;它不会自动修改对象。selinux_android_restorecon() 由 init、ueventd、property service 等路径调用,才会把 lookup 结果与对象当前标签比较并修复。

4.3 初始化时机 ​

SELinux 在 first-stage init 仍处于 kernel domain 时就会为 /system/bin/init 执行 restorecon,随后重新 exec second-stage init。这样第二阶段进程才能在正确的 init domain 中处理 rc、属性和服务。

源码文件:system/core/init/selinux.cpp

相关函数:SetupSelinux

cpp
if (IsMicrodroid()) {
    LoadSelinuxPolicyMicrodroid();
} else {
    LoadSelinuxPolicyAndroid();
}

SelinuxSetEnforcement();

if (selinux_android_restorecon(
        "/system/bin/init", 0) == -1) {
    PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
}

const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));

生效时机是“策略已加载 + init 可执行文件有 init_exec 标签 + exec 完成”,不是 file_contexts 文件被打包进镜像的时刻。若 lookup 成功但 restorecon 失败,后续 domain transition 仍可能失败。

5. 应用上下文 ​

5.1 输入字段 ​

seapp_contexts 的匹配输入由 Android framework 和 Zygote 共同准备。Android 17 文件头明确列出 isSystemServer、user、seinfo、name、isPrivApp、minTargetSdkVersion、fromRunAs、isolated/sdk sandbox 等 selector。

源码文件:system/sepolicy/private/seapp_contexts

相关说明:selector 与输出字段

text
Input selectors: isSystemServer, isEphemeralApp, user, seinfo, name,
isPrivApp, minTargetSdkVersion, fromRunAs,
isIsolatedComputeApp, isSdkSandboxNext, isSdkSandboxAudit

Outputs: domain, type, levelFrom, level

这些输入不是全部来自包名。user 可能由 UID 映射为 system、_app、_isolated 或 _sdksandbox;seinfo 来自签名和 mac_permissions.xml;isPrivApp、target SDK 和包名由 PackageManager 传给进程启动链。少一个输入,可能命中更宽泛的规则。

5.2 匹配优先级 ​

文件注释给出严格优先级:system server 优先于普通进程;明确 selector 优先于未指定;更具体的 user/seinfo/name 优先于宽泛或前缀匹配;平台 contexts 文件优先于 vendor/odm 文件;最终按第一个匹配项输出。

源码文件:system/sepolicy/private/seapp_contexts

相关规则:system server、isolated 和普通应用

text
isSystemServer=true domain=system_server_startup

user=_isolated domain=isolated_app levelFrom=user
user=_isolated isIsolatedComputeApp=true \
    domain=isolated_compute_app levelFrom=user

user=_app seinfo=platform minTargetSdkVersion=37 \
    domain=platform_app type=app_data_file levelFrom=user
user=_app minTargetSdkVersion=37 \
    domain=untrusted_app type=app_data_file levelFrom=all

规则比较不是“文件中写在前面的永远优先”。解析器先按 selector specificity 排序,再寻找第一个匹配;因此新增一条更具体规则可能改变旧应用的 domain,即使它被追加在文件尾部。

5.3 neverallow ​

同一个 seapp_contexts 文件还包含 neverallow assertion,它们只在构建检查阶段使用,不会输出为设备运行时的匹配条目。

源码文件:system/sepolicy/private/seapp_contexts

相关规则:domain 归属约束

text
neverallow isSystemServer=false domain=system_server
neverallow isSystemServer=false domain=system_server_startup
neverallow user=((?!system).)* domain=system_app
neverallow user=_isolated domain=((?!isolated_app).)*
neverallow user=((?!_isolated).)* domain=isolated_app

这解释了“规则能匹配”与“策略能构建”是两件事。一个 selector 组合即使语法正确,只要违反 neverallow,构建应失败;反过来,构建通过也只说明静态约束满足,不说明运行时每个包都命中了预期 selector。

5.4 Zygote消费 ​

Zygote specialization 调用 selinux_android_setcontext(),把 UID、是否 system server、seInfo 和 nice name 交给 Android libselinux。system_server 还会显式设置固定 u:r:system_server:s0。

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

相关位置:SpecializeCommon

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));
}

if (is_system_server) {
    static const char* kSystemServerLabel =
            "u:r:system_server:s0";
    if (selinux_android_setcon(kSystemServerLabel) != 0) {
        fail_fn(CREATE_ERROR(
                "selinux_android_setcon(%s)",
                kSystemServerLabel));
    }
}

selinux_android_setcontext() 的返回成功只表示 libselinux 完成进程 context 设置;Zygote 还要继续执行 capability、seccomp、线程名和 post-fork hook。任何 context 设置失败都不能让子进程进入 ART/Java 入口。

图中 Linux UID/GID 和 SELinux task SID 是两条状态写入路径;seInfo 只是选择上下文的输入,不等于最终 domain 字符串。

6. 进程属性 ​

6.1 current属性 ​

传统 SELinux proc 接口通过 /proc/<pid>/attr/current 暴露 task 当前 context 字符串。内核实现先由 proc 文件读取入口调用 security 层,再由 SELinux 将 task SID 转成 context。

源码文件:kernel/common/security/selinux/hooks.c

相关函数:selinux_getprocattr

c
static int selinux_getprocattr(
        struct task_struct *p, const char *name, char **value)
{
    unsigned int attr = lsm_name_to_attr(name);
    int rc;

    if (attr) {
        rc = selinux_lsm_getattr(attr, p, value);
        if (rc != -EOPNOTSUPP)
            return rc;
    }

    return -EINVAL;
}

不同属性名由 lsm_name_to_attr() 映射后交给 selinux_lsm_getattr();current、exec 等具体字段读取发生在后者内部。它们都不是 Android package name,也不是 UID。工具读取 /proc/self/attr/current 时得到的字符串已经经过 SID 反向转换。

6.2 新属性API ​

LSM 通用系统调用还提供 lsm_get_self_attr/lsm_set_self_attr,调用统一 security 层。它与 SELinux 专有的 proc 属性接口有不同 ABI 和错误处理,不应混用。

源码文件:kernel/common/security/lsm_syscalls.c

相关函数:sys_lsm_get_self_attr

c
SYSCALL_DEFINE4(lsm_get_self_attr,
        unsigned int, attr,
        struct lsm_ctx __user *, ctx,
        u32 __user *, size, u32, flags)
{
    return security_getselfattr(
            attr, ctx, size, flags);
}

通用 API 返回的是一个或多个 LSM context 记录,调用方必须处理缓冲区大小和模块差异;id -Z 通常读取的是 SELinux 当前 context 的人类可读形式。看到同样的 u:r:... 字符串,不表示两个接口拥有相同的调用路径。

6.3 读取命令 ​

bash
# 查看当前 shell 的 SELinux context 与 Linux UID/GID。
adb shell 'id; id -Z'

# 对比进程当前 context、exec 候选 context 和安全属性文件。
adb shell 'cat /proc/self/attr/current'
adb shell 'cat /proc/self/attr/exec'

# 查看关键 Android 进程以及文件对象的标签。
adb shell 'ps -AZ | grep -E "init|servicemanager|zygote|system_server"'
adb shell 'ls -lZ /system/bin/init /system/bin/app_process64 /dev/socket/zygote'

attr/exec 在没有显式 exec context 时可能为空或返回错误,不能把它当成当前 domain 的第二份副本。ls -Z 显示的是对象标签,ps -Z 显示的是 task 标签,两者的 type 可能同名,也可能完全不同。

7. 状态转换 ​

7.1 Exec转换 ​

execve 可能触发 domain transition。SELinux hook 在 bprm_* 生命周期中读取当前 task、可执行文件 inode 和候选 transition SID,最后提交新 credentials。

源码文件:kernel/common/security/selinux/hooks.c

相关函数:selinux_bprm_committing_creds、selinux_bprm_committed_creds

c
static void selinux_bprm_committing_creds(
        const struct linux_binprm *bprm)
{
    struct cred_security_struct *new_crsec;

    new_crsec = selinux_cred(bprm->cred);
    if (new_crsec->sid == new_crsec->osid)
        return;

    // ... 根据新旧 SID 清理无权继承的文件与资源限制。
}

static void selinux_bprm_committed_creds(
        const struct linux_binprm *bprm)
{
    const struct cred_security_struct *crsec =
            selinux_cred(current_cred());

    if (crsec->sid == crsec->osid)
        return;

    // ... 根据新旧 SID 清理信号状态,并唤醒等待父进程。
}

上面保留了真实函数签名和状态判断,并用注释概括较长的清理分支;真正的 transition SID 计算发生在更早的 bprm credentials hook。阅读 exec 问题时要把“查找新 domain”“提交 credentials”“进程开始执行新映像”分开,不要只搜 setcon。

7.2 创建对象 ​

新 inode 的 SID 可能来自父目录、文件系统 mount 选项、task create_sid 或 security_transition_sid()。因此新建文件的标签不一定等于创建者当前 domain,也不一定等于父目录标签。

源码文件:kernel/common/security/selinux/hooks.c

相关函数:selinux_determine_inode_label

c
static int selinux_determine_inode_label(
        const struct cred_security_struct *crsec,
        struct inode *dir, const struct qstr *name,
        u16 tclass, u32 *new_isid)
{
    const struct superblock_security_struct *sbsec =
            selinux_superblock(dir->i_sb);

    if ((sbsec->flags & SE_SBINITIALIZED) &&
            sbsec->behavior == SECURITY_FS_USE_MNTPOINT) {
        *new_isid = sbsec->mntpoint_sid;
    } else if ((sbsec->flags & SBLABEL_MNT) &&
            crsec->create_sid) {
        *new_isid = crsec->create_sid;
    } else {
        const struct inode_security_struct *dsec =
                inode_security(dir);
        return security_transition_sid(
                crsec->sid, dsec->sid, tclass,
                name, new_isid);
    }

    return 0;
}

这段代码是解释 type_transition 的入口之一:策略转换函数接收创建者 SID、父目录 SID、对象 class 和名字,计算新对象 SID。对象标签的 owner 是 inode security state,不是创建它的 Java API。

7.3 策略重载 ​

策略序列改变后,旧 inode/file 状态需要重新验证。selinux_file_open() 保存 pseqno,后续访问可以发现 policy 已更新并重新做权限检查;因此 context 字符串相同也不代表旧的 AVC 决策仍然有效。

源码文件:kernel/common/security/selinux/hooks.c

相关函数:selinux_file_open

c
static int selinux_file_open(struct file *file)
{
    struct file_security_struct *fsec;
    struct inode_security_struct *isec;

    fsec = selinux_file(file);
    isec = inode_security(file_inode(file));
    fsec->isid = isec->sid;
    fsec->pseqno = avc_policy_seqno();

    /* Recheck after saving inode SID and policy sequence. */
    return file_path_has_perm(
            file->f_cred, file, open_file_to_av(file));
}

8. 失败边界 ​

现象状态检查可能 owner不应直接下的结论
id -Z 读取失败proc attr ABI、task SID、SELinux 是否启用kernel SELinux/proc不能说明文件标签错误
ls -Z 有值但访问被拒绝inode target SID/class、mode/ACL、source SIDVFS/SELinux有 context 不代表有 allow
file_contexts 有条目但文件标签不对lookup 产物、restorecon 是否执行、伪文件系统类型init/ueventd/filesystem配置存在不代表 inode 已重标
应用 domain 与预期不同UID、seinfo、包名、privileged、target SDK、规则优先级PackageManager/Zygote/libselinux不能只改 domain allow
exec 后 domain 未切换可执行文件 label、transition 规则、bprm hook、权限SELinux exec pathsetcon 不是所有 transition 的入口
policy 更新后行为仍旧policy sequence、AVC/cache、file stateSELinux kernel state不能假设旧 file descriptor 永远沿用旧决策
同一服务名不同进程看到不同结果servicemanager source SID、service_contexts target、LSM orderBinder/servicemanager不能只查服务实现类

排查顺序应是:先读主体 task context,再读客体 context;确认标签来源和生效时机;确认 object class 与 permission;最后才查 allow/neverallow。安全上下文是访问判断的输入,不是访问成功的证明。

9. 反向测试 ​

9.1 LSM属性测试 ​

内核 LSM selftest 通过错误指针、容量不足和非法 flags 验证通用属性 API 的 ABI 边界。

源码文件:kernel/common/tools/testing/selftests/lsm/lsm_list_modules_test.c

相关测试:size_null_lsm_list_modules、size_too_small_lsm_list_modules、flags_set_lsm_list_modules

c
TEST(size_null_lsm_list_modules)
{
    const long page_size = sysconf(_SC_PAGESIZE);
    __u64 *syscall_lsms = calloc(page_size, 1);

    ASSERT_NE(NULL, syscall_lsms);
    errno = 0;
    ASSERT_EQ(-1, lsm_list_modules(
            syscall_lsms, NULL, 0));
    ASSERT_EQ(EFAULT, errno);
    free(syscall_lsms);
}

TEST(flags_set_lsm_list_modules)
{
    const long page_size = sysconf(_SC_PAGESIZE);
    __u64 *syscall_lsms = calloc(page_size, 1);
    __u32 size = page_size;

    errno = 0;
    ASSERT_EQ(-1, lsm_list_modules(
            syscall_lsms, &size, 7));
    ASSERT_EQ(EINVAL, errno);
    free(syscall_lsms);
}

第一个测试用空 size 指针验证内核不会写入非法用户地址;第二个用非零 flags 验证保留字段不会被静默接受。它们证明的是接口边界,不是 SELinux context 匹配规则。

9.2 活动列表测试 ​

correct_lsm_list_modules 把 syscall 返回的 LSM ID 映射到名称,再与 /sys/kernel/security/lsm 文本逐项比较。这个测试直接反向验证“注册状态”和“用户可见模块列表”是否一致。

源码文件:kernel/common/tools/testing/selftests/lsm/lsm_list_modules_test.c

相关测试:correct_lsm_list_modules

c
count = lsm_list_modules(syscall_lsms, &size, 0);
ASSERT_LE(1, count);
cp = sysfs_lsms;
for (i = 0; i < count; i++) {
    switch (syscall_lsms[i]) {
    case LSM_ID_CAPABILITY:
        name = "capability";
        break;
    case LSM_ID_SELINUX:
        name = "selinux";
        break;
    default:
        name = "INVALID";
        break;
    }
    ASSERT_EQ(0, strncmp(cp, name, strlen(name)));
    cp += strlen(name) + 1;
}

测试没有要求模块列表必须固定为某个字符串;它根据当前内核返回值验证 ID/name 映射。因此不同 CONFIG_LSM 或 lsm= 顺序可以产生不同合法结果,文章中的静态模块清单不能替代设备检查。

9.3 UID测试 ​

Android libcutils 单元测试验证 user id 与 app id 的数学合成,为理解应用 context 的 user 输入提供反向证据。

源码文件:system/core/libcutils/multiuser_test.cpp

相关测试:MultiuserTest.TestMerge

cpp
TEST(MultiuserTest, TestMerge) {
    EXPECT_EQ(10000U, multiuser_get_uid(0, 10000));
    EXPECT_EQ(1000000U, multiuser_get_uid(10, 0));
    EXPECT_EQ(1010000U, multiuser_get_uid(10, 10000));
}

该测试证明 seapp_contexts 看到的 Android user/UID 输入可以区分 user 0 与 user 10;它没有证明某个 app 一定匹配 untrusted_app,因为 seinfo、包名、target SDK 和 selector specificity 仍由进程启动链提供。

10. 源码导航 ​

bash
# 从 task SID 和 inode SID 开始,查找 context 的内核表示。
rg -n 'task_security_struct|inode_security_struct|cred_sid|security_context_to_sid|security_sid_to_context' \
  kernel/common/security/selinux/include/objsec.h \
  kernel/common/security/selinux/hooks.c \
  kernel/common/security/selinux/ss/services.c

确认 SID 的持有者后,再切换到文件标签的生产和生效路径:

bash
# 查找文件路径如何映射到 context,以及 init 在何时 restorecon。
rg -n 'SelabelInitialize|SelabelLookupFileContext|selinux_android_restorecon|file_contexts' \
  system/core/init/selabel.cpp \
  system/core/init/selinux.cpp \
  system/sepolicy/private/file_contexts

如果问题只出现在应用进程,再检查 seapp selector 的优先级和 Zygote 写入点:

bash
# 跟踪应用 seapp selector、优先级、neverallow 与 Zygote context 设置。
rg -n 'Input selectors|Precedence|neverallow|levelFrom|selinux_android_setcontext' \
  system/sepolicy/private/seapp_contexts \
  frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

最后确认用户空间可见属性与内核通用 LSM 接口是否来自同一活动模块集合:

bash
# 比较 proc 属性、LSM 通用属性和设备可见标签。
rg -n 'selinux_getprocattr|lsm_get_self_attr|procattr|securityfs/lsm' \
  kernel/common/security/selinux/hooks.c \
  kernel/common/security/lsm_syscalls.c \
  kernel/common/tools/testing/selftests/lsm

完成源码侧比对后,再用只读命令观察设备当前的主体、客体和活动模块状态:

bash
# 设备侧同时观察主体、客体和活动模块;这些命令只读系统状态。
adb shell 'id -Z; cat /proc/self/attr/current'
adb shell 'cat /sys/kernel/security/lsm'
adb shell 'ps -AZ | grep -E "zygote|system_server"'
adb shell 'ls -lZ /system/bin/init /dev/socket/zygote'

11. 闭环复述 ​

  1. 从 u:r:system_server:s0 开始,复述字符串如何对应 SID、task blob 和 current_sid(),再指出哪些代码把它交给 AVC。
  2. 从 /system/bin/init 的 file_contexts 条目开始,追到 selabel_lookup()、restorecon()、execv() 和 second-stage init domain,说明配置存在、对象已标注和进程已切换的区别。
  3. 给定一个普通应用的 UID、seinfo、包名和 target SDK,按 seapp_contexts precedence 判断可能命中的 domain/type/levelFrom,并指出缺少哪个输入时无法下结论。
  4. 解释为什么 id -Z、ps -Z 和 ls -Z 分别观察 task、进程列表和 inode/对象;再给出一个“标签正确但访问仍拒绝”的 class/permission 诊断路径。
  5. 使用 lsm_list_modules 测试的 EFAULT、EINVAL 和 ID/name 对照断言,说明它验证的是框架 ABI 和活动模块状态,而不是某条 Android allow 规则。

安全上下文的核心不是四个字段本身,而是它们在生命周期中的交接:配置文件生产字符串,libselinux 把字符串映射为 SID,LSM blob 把 SID 附着到 task/inode,hook 再用 source SID、target SID、class 和 permission 做裁决。掌握这些交接点后,id -Z、seapp_contexts、restorecon 和 AVC 日志才会落回真实源码,而不是停留在标签格式的记忆。