Skip to content

LSM框架

沿 common kernel 源码理解 LSM 模块发现、顺序选择、hook 注册、静态调用和 SELinux 接入。

基于android-17.0.0_r1
AndroidSELinuxLSMLinux内核源码阅读

LSM框架 ​

本文面向已经读过 SELinux架构 和 DAC与MAC 的读者。前两篇建立了策略、标签和一次访问裁决的概念,本文继续下钻到 Linux 内核的连接层:LSM(Linux Security Module)如何发现可用安全模块,如何决定初始化顺序,模块怎样把回调注册到统一 hook,VFS 或 Binder 入口如何调用这些回调,以及 SELinux 的状态 blob 为什么必须在 hook 注册前完成布局。

本文使用 Android 17 common kernel checkout 的固定提交和 AOSP android-17.0.0_r1 源码。它不把 LSM 说成“SELinux 的另一个名字”:LSM 是内核安全框架,SELinux 是其中一个带有独占主模块标志的实现;capability、BPF LSM、Landlock、LoadPin 等也可以进入同一框架。读完后,你应能从 security_init() 追到 selinux_init(),从一个 security_*() 包装函数追到 call_int_hook(),并解释模块顺序、返回值和 blob 生命周期对最终行为的影响。

1. 框架边界 ​

LSM 框架拥有的是“回调如何被组织和调用”的状态,不拥有具体策略。一个完整请求至少经过以下对象:

对象所有者状态消费者
lsm_info每个内置 LSM名称、顺序、flags、init、blob 需求security_init()
security_hook_list模块hook 函数、所属 lsm_id、静态调用槽security_add_hooks()、hook wrapper
lsm_static_calls_tableLSM 框架每个 hook 的静态调用槽数组call_int_hook()/call_void_hook()
lsm_blob_sizesLSM 框架汇总cred/inode/file/task 等对象的复合 blob 大小对象分配和模块访问
lsm_idlistLSM 框架已初始化模块的数值 IDlsm_list_modules 系统调用
SELinux state/AVCSELinuxenforcing、策略、SID、缓存SELinux hook

这张边界表解释了三个常见误解。第一,security_inode_permission() 是框架入口,不是 SELinux 决策本身。第二,security_add_hooks() 注册的是回调地址和所属模块,不是 allow 规则。第三,模块的 init() 返回成功只说明初始化函数完成,不表示每个访问 hook 都会允许请求。

图中的返回值是框架级短路机制:对返回整数的 hook,框架使用模块定义的默认返回值作为“尚未决定”,遇到第一个不同于默认值的返回值就跳到出口。具体模块顺序和默认值由内核源码决定,不能从“LSM 会依次检查”这句概括中推导出全部行为。

2. 模块发现 ​

2.1 模块描述 ​

LSM 模块通过 DEFINE_LSM(name) 放入 .lsm_info.init 段。框架启动时遍历这段内核链接器数组,读取每个模块的名字、顺序类别、启用指针、初始化函数和对象 blob 需求。

源码文件:kernel/common/include/linux/lsm_hooks.h

相关定义:struct lsm_id、struct lsm_info、DEFINE_LSM

c
struct lsm_id {
    const char *name;
    u64 id;
};

struct lsm_info {
    const char *name;
    enum lsm_order order;
    unsigned long flags;
    int *enabled;
    int (*init)(void);
    struct lsm_blob_sizes *blobs;
};

#define DEFINE_LSM(lsm)                         \
    static struct lsm_info __lsm_##lsm          \
        __used __section(".lsm_info.init")     \
        __aligned(sizeof(unsigned long))

lsm_info 和 lsm_id 不是一回事。前者描述“如何初始化模块”,后者标识“已经注册的模块及其公开 ID”。只有模块的 init() 调用 security_add_hooks() 后,相关 hook 才进入静态调用表,lsm_idlist 才记录该模块。

2.2 配置顺序 ​

Android 17 common kernel 的 CONFIG_LSM 默认值包含 capability、SELinux 以及其他可选模块;内核命令行 lsm= 可以选择顺序。security= 是旧的 legacy major LSM 选择,若同时给出 lsm=,后者优先。

源码文件:kernel/common/security/Kconfig

相关配置:CONFIG_LSM

text
config LSM
    string "Ordered list of enabled LSMs"
    depends on SECURITY
    default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,ipe,bpf"
    help
      A comma-separated list of LSMs, in initialization order.
      Any LSMs left off this list, except for those with order
      LSM_ORDER_FIRST and LSM_ORDER_LAST, ... will be ignored.
      This can be controlled at boot with the "lsm=" parameter.

配置字符串是编译期默认值,启动参数是运行时选择值。某个名称不在内核中时,解析器只记录“未编译进内核并忽略”;某个已编译模块没有出现在最终顺序时,则会被禁用。因而设备上的 /sys/kernel/security/lsm 反映的是实际启用结果,不应只根据 Kconfig 猜测。

2.3 顺序解析 ​

security_init() 首先记录 legacy security=、编译期 CONFIG_LSM 和命令行 lsm=,然后由 ordered_lsm_init() 解析。解析器强制 LSM_ORDER_FIRST 模块在最前,LSM_ORDER_LAST 模块在最后,中间模块按逗号列表排列。

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

相关函数:security_init、ordered_lsm_init、ordered_lsm_parse

c
int __init security_init(void)
{
    struct lsm_info *lsm;

    init_debug("legacy security=%s\n",
               chosen_major_lsm ? : " *unspecified*");
    init_debug("  CONFIG_LSM=%s\n", builtin_lsm_order);
    init_debug("boot arg lsm=%s\n",
               chosen_lsm_order ? : " *unspecified*");

    for (lsm = __start_early_lsm_info;
         lsm < __end_early_lsm_info; lsm++) {
        if (lsm->enabled)
            lsm_append(lsm->name, &lsm_names);
    }

    ordered_lsm_init();
    return 0;
}

static void __init ordered_lsm_init(void)
{
    struct lsm_info **lsm;

    if (chosen_lsm_order) {
        if (chosen_major_lsm) {
            pr_warn("security=%s is ignored because it is superseded by lsm=%s\n",
                    chosen_major_lsm, chosen_lsm_order);
            chosen_major_lsm = NULL;
        }
        ordered_lsm_parse(chosen_lsm_order, "cmdline");
    } else {
        ordered_lsm_parse(builtin_lsm_order, "builtin");
    }

    for (lsm = ordered_lsms; *lsm; lsm++)
        prepare_lsm(*lsm);

    report_lsm_order();
    for (lsm = ordered_lsms; *lsm; lsm++)
        initialize_lsm(*lsm);
}

这段代码的生效顺序有两个层次:ordered_lsm_parse() 决定哪些模块和什么顺序;prepare_lsm() 先计算是否启用并汇总 blob 大小;initialize_lsm() 最后才调用模块 init()。因此模块 init() 运行时,复合对象 blob 的布局已经确定。

2.4 独占模块 ​

SELinux 的 DEFINE_LSM(selinux) 带有 LSM_FLAG_LEGACY_MAJOR | LSM_FLAG_EXCLUSIVE。prepare_lsm() 遇到第一个独占模块后记录 exclusive,后续另一个独占模块会被 lsm_allowed() 拒绝。

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

相关函数:lsm_allowed、prepare_lsm

c
static bool __init lsm_allowed(struct lsm_info *lsm)
{
    if (!is_enabled(lsm))
        return false;

    if ((lsm->flags & LSM_FLAG_EXCLUSIVE) && exclusive) {
        init_debug("exclusive disabled: %s\n", lsm->name);
        return false;
    }

    return true;
}

static void __init prepare_lsm(struct lsm_info *lsm)
{
    int enabled = lsm_allowed(lsm);
    set_enabled(lsm, enabled);

    if (enabled) {
        if ((lsm->flags & LSM_FLAG_EXCLUSIVE) && !exclusive)
            exclusive = lsm;
        lsm_set_blob_sizes(lsm->blobs);
    }
}

“SELinux 是唯一 LSM”不是框架规则;更准确的说法是,SELinux 在 Android 17 配置中作为独占 legacy major LSM 参与选择,而其他非独占模块仍可能同时启用。读取 /sys/kernel/security/lsm 或 lsm_list_modules 才能确认设备实际启用了哪些模块。

图中 lsm_info 数组是静态模块描述,lsm_idlist 是初始化后可被查询的活动模块列表。一个模块被编译进内核但被 lsm= 排除时,不会进入后者。

3. Hook注册 ​

3.1 Hook结构 ​

security_hook_list 包含函数联合体、所属 lsm_id 和静态调用槽指针。LSM_HOOK_INIT(NAME, HOOK) 将某个 hook 名称映射到 static_calls_table.NAME,并保存模块回调地址。

源码文件:kernel/common/include/linux/lsm_hooks.h

相关定义:struct security_hook_list、LSM_HOOK_INIT

c
struct security_hook_list {
    struct lsm_static_call *scalls;
    union security_list_options hook;
    const struct lsm_id *lsmid;
} __randomize_layout;

#define LSM_HOOK_INIT(NAME, HOOK)        \
    {                                    \
        .scalls = static_calls_table.NAME, \
        .hook = { .NAME = HOOK }         \
    }

lsm_static_calls_table 为每种 hook 预留 MAX_LSM_COUNT 个槽。这样框架不必为每个访问点维护一个传统链表遍历;初始化时把实际模块回调填入槽,运行时通过 static call 和 static branch 直接跳过没有回调的槽。

3.2 SELinux数组 ​

SELinux 在 selinux_hooks[] 中按资源和生命周期组织回调。数组标记为 __ro_after_init,并由源码注释要求把普通、cloning、allocating hook 按顺序分组,避免某个 hook 读取尚未由另一个 hook 分配的 blob。

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

相关位置:selinux_lsmid、selinux_hooks

c
static const struct lsm_id selinux_lsmid = {
    .name = "selinux",
    .id = LSM_ID_SELINUX,
};

static struct security_hook_list selinux_hooks[] __ro_after_init = {
    LSM_HOOK_INIT(binder_set_context_mgr,
                  selinux_binder_set_context_mgr),
    LSM_HOOK_INIT(binder_transaction,
                  selinux_binder_transaction),

    LSM_HOOK_INIT(capget, selinux_capget),
    LSM_HOOK_INIT(capset, selinux_capset),
    LSM_HOOK_INIT(capable, selinux_capable),

    LSM_HOOK_INIT(bprm_creds_for_exec,
                  selinux_bprm_creds_for_exec),
    LSM_HOOK_INIT(bprm_committing_creds,
                  selinux_bprm_committing_creds),
    LSM_HOOK_INIT(bprm_committed_creds,
                  selinux_bprm_committed_creds),

    LSM_HOOK_INIT(inode_create, selinux_inode_create),
    LSM_HOOK_INIT(inode_permission,
                  selinux_inode_permission),
    LSM_HOOK_INIT(inode_setattr, selinux_inode_setattr),

    LSM_HOOK_INIT(file_permission, selinux_file_permission),
    LSM_HOOK_INIT(file_open, selinux_file_open),
    LSM_HOOK_INIT(file_ioctl, selinux_file_ioctl),
    LSM_HOOK_INIT(mmap_file, selinux_mmap_file),

    /* ... 其他按CONFIG条件编译的网络、IPC、密钥和BPF hook ... */
};

这个数组同时覆盖 Binder、exec、capability、inode、file、socket、网络和安全属性操作。读一个具体 hook 时,先在数组中确认它是否被当前构建条件编译,再追到实现函数;不能因为 SELinux 有一个 inode_permission 就推断 Binder 或 execve 也经过同一函数。

3.3 注册过程 ​

selinux_init() 先初始化 SELinux state、AVC、锁和各种 slab cache,再调用 security_add_hooks()。注册函数为每个 hook 填写 lsmid,建立 static call,并把模块名称加入活动列表。

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

相关函数:selinux_init

c
static __init int selinux_init(void)
{
    pr_info("SELinux:  Initializing.\n");

    memset(&selinux_state, 0, sizeof(selinux_state));
    enforcing_set(selinux_enforcing_boot);
    selinux_avc_init();
    mutex_init(&selinux_state.status_lock);
    mutex_init(&selinux_state.policy_mutex);
    cred_init_security();

    avc_init();
    avtab_cache_init();
    ebitmap_cache_init();
    hashtab_cache_init();

    security_add_hooks(selinux_hooks,
                       ARRAY_SIZE(selinux_hooks),
                       &selinux_lsmid);

    if (avc_add_callback(selinux_netcache_avc_callback,
                         AVC_CALLBACK_RESET))
        panic("SELinux: Unable to register AVC netcache callback\n");
    if (avc_add_callback(selinux_lsm_notifier_avc_callback,
                         AVC_CALLBACK_RESET))
        panic("SELinux: Unable to register AVC LSM notifier callback\n");

    return 0;
}

初始化失败边界很明确:内存 cache、AVC callback 或 hook 注册失败会让内核初始化告警甚至 panic;SELinux 不会以“没有 hook 但继续启动”的方式提供一个看似安全的半初始化状态。

源码文件:kernel/common/include/linux/lsm_hooks.h

相关函数:security_add_hooks

c
extern void security_add_hooks(
        struct security_hook_list *hooks,
        int count, const struct lsm_id *lsmid);

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

相关函数:security_add_hooks

c
void __init security_add_hooks(
        struct security_hook_list *hooks,
        int count, const struct lsm_id *lsmid)
{
    int i;

    if (lsm_active_cnt == 0
            || lsm_idlist[lsm_active_cnt - 1] != lsmid) {
        if (lsm_active_cnt >= MAX_LSM_COUNT)
            panic("%s Too many LSMs registered.\n", __func__);
        lsm_idlist[lsm_active_cnt++] = lsmid;
    }

    for (i = 0; i < count; i++) {
        hooks[i].lsmid = lsmid;
        lsm_static_call_init(&hooks[i]);
    }

    if (slab_is_available()) {
        if (lsm_append(lsmid->name, &lsm_names) < 0)
            panic("%s - Cannot get early memory.\n", __func__);
    }
}

security_add_hooks() 允许同一模块在多个阶段或多个数组中追加 hook:只有当本次 lsmid 与活动列表最后一项不同时,才增加 lsm_active_cnt。这正是 lsm_idlist 不会因模块多次注册数组而重复计数的原因。

3.4 其他模块 ​

BPF LSM 使用同一注册 API,但回调数组由宏从 lsm_hook_defs.h 批量生成,并额外注册 inode_free_security。它没有 SELinux 的独占标志,因此能与 SELinux 并列进入静态调用槽。

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

相关函数:bpf_lsm_init

c
static struct security_hook_list bpf_lsm_hooks[] __ro_after_init = {
#define LSM_HOOK(RET, DEFAULT, NAME, ...) \
    LSM_HOOK_INIT(NAME, bpf_lsm_##NAME),
#include <linux/lsm_hook_defs.h>
#undef LSM_HOOK
    LSM_HOOK_INIT(inode_free_security, bpf_inode_storage_free),
};

static const struct lsm_id bpf_lsmid = {
    .name = "bpf",
    .id = LSM_ID_BPF,
};

static int __init bpf_lsm_init(void)
{
    security_add_hooks(bpf_lsm_hooks,
                       ARRAY_SIZE(bpf_lsm_hooks),
                       &bpf_lsmid);
    pr_info("LSM support for eBPF active\n");
    return 0;
}

DEFINE_LSM(bpf) = {
    .name = "bpf",
    .init = bpf_lsm_init,
};

如果 SELinux 和 BPF LSM 都实现某个返回整数的 hook,最终结果取决于 CONFIG_LSM/lsm= 排序和各模块返回值。BPF 程序 attach 成功不表示它覆盖 SELinux,也不表示 SELinux 策略被修改。

4. Hook调用 ​

4.1 包装函数 ​

内核子系统不直接访问某个模块数组,而是调用统一的 security_*() 包装函数。例如 Binder 的 context manager、事务和引用转移都通过 security 层进入 LSM。

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

相关函数:security_binder_set_context_mgr、security_binder_transaction

c
int security_binder_set_context_mgr(const struct cred *mgr)
{
    return call_int_hook(binder_set_context_mgr, mgr);
}

int security_binder_transaction(const struct cred *from,
                               const struct cred *to)
{
    return call_int_hook(binder_transaction, from, to);
}

int security_capable(const struct cred *cred,
                     struct user_namespace *ns,
                     int cap, unsigned int opts)
{
    return call_int_hook(capable, cred, ns, cap, opts);
}

包装函数的职责是保持内核子系统和具体 LSM 解耦。Binder 只知道 security_binder_transaction() 的返回值,SELinux 只负责自己的 selinux_binder_transaction(),BPF LSM 也可以在同一个 hook 上提供回调。

4.2 整数Hook ​

call_int_hook() 使用 LSM_RET_DEFAULT(HOOK) 初始化返回值,然后展开固定数量的 static call 槽。某个回调返回非默认值时跳到 OUT,后续模块不会再被调用。

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

相关宏:__CALL_STATIC_INT、call_int_hook

c
#define __CALL_STATIC_INT(NUM, R, HOOK, LABEL, ...)       \
do {                                                      \
    if (static_branch_unlikely(                          \
            &SECURITY_HOOK_ACTIVE_KEY(HOOK, NUM))) {     \
        R = static_call(LSM_STATIC_CALL(HOOK, NUM))      \
                (__VA_ARGS__);                           \
        if (R != LSM_RET_DEFAULT(HOOK))                  \
            goto LABEL;                                   \
    }                                                     \
} while (0);

#define call_int_hook(HOOK, ...)                         \
({                                                       \
    __label__ OUT;                                       \
    int RC = LSM_RET_DEFAULT(HOOK);                      \
                                                           \
    LSM_LOOP_UNROLL(__CALL_STATIC_INT,                  \
                    RC, HOOK, OUT, __VA_ARGS__);        \
OUT:                                                     \
    RC;                                                  \
})

这里的“第一个非默认值短路”是关键不变量。对于默认返回 0 的允许型 hook,模块返回负 errno 会立即拒绝;对于默认返回 1 或其他默认值的 hook,语义由 lsm_hook_defs.h 中的定义决定。不能脱离 hook 定义把所有非零值都解释成拒绝。

4.3 VoidHook ​

没有返回值的 hook 使用 call_void_hook(),它会调用所有 active static call,不存在整数 hook 的短路出口。日志、状态同步和对象释放类回调通常属于这一类。

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

相关宏:__CALL_STATIC_VOID、call_void_hook

c
#define __CALL_STATIC_VOID(NUM, HOOK, ...)                \
do {                                                      \
    if (static_branch_unlikely(                          \
            &SECURITY_HOOK_ACTIVE_KEY(HOOK, NUM)))      \
        static_call(LSM_STATIC_CALL(HOOK, NUM))          \
                (__VA_ARGS__);                           \
} while (0);

#define call_void_hook(HOOK, ...)                         \
do {                                                      \
    LSM_LOOP_UNROLL(__CALL_STATIC_VOID,                  \
                    HOOK, __VA_ARGS__);                  \
} while (0)

因此“多个 LSM 会把返回值叠加起来”只适用于某些专门定义的聚合逻辑,不适用于通用整数 hook;通用 call_int_hook() 是默认值加首个决定值模型,call_void_hook() 才是全部执行模型。

4.4 文件访问 ​

把前一篇的文件访问路径接回 LSM,可以看到 VFS 只调用框架包装函数。security_inode_permission() 对 private inode 直接返回 0,否则调用所有已注册的 inode_permission 回调。

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

相关函数:security_inode_permission

c
int security_inode_permission(struct inode *inode, int mask)
{
    if (unlikely(IS_PRIVATE(inode)))
        return 0;
    return call_int_hook(inode_permission, inode, mask);
}

SELinux 只是这个 hook 的一个消费者:selinux_inode_permission() 读取 current SID、inode SID/class 和 requested mask,做自己的 task cache/AVC 查询。若另一个 LSM 在 SELinux 之前返回拒绝,SELinux 可能根本不会被调用;若 SELinux 先返回拒绝,后续模块也不会被调用。

图中“默认值”不是“允许策略”,而是表示该模块没有在此 hook 上做出决定。只有所有模块都返回各自默认值,框架才把默认值交回 VFS。

5. 对象状态 ​

5.1 Blob布局 ​

LSM 不把每个模块的状态字段直接添加到 struct inode、struct cred 等核心结构,而是汇总各模块的 lsm_blob_sizes,为对象分配一个复合 blob。每个模块得到自己的对齐偏移。

源码文件:kernel/common/include/linux/lsm_hooks.h

相关定义:struct lsm_blob_sizes

c
struct lsm_blob_sizes {
    int lbs_cred;
    int lbs_file;
    int lbs_ib;
    int lbs_inode;
    int lbs_sock;
    int lbs_superblock;
    int lbs_ipc;
    int lbs_key;
    int lbs_msg_msg;
    int lbs_perf_event;
    int lbs_task;
    int lbs_xattr_count;
    int lbs_tun_dev;
    int lbs_bdev;
    int lbs_bpf_map;
    int lbs_bpf_prog;
    int lbs_bpf_token;
};

SELinux 和 BPF LSM 可以同时声明 inode blob 需求。框架必须在所有 prepare_lsm() 完成后再创建对应 cache,否则后注册模块没有安全空间访问自己的对象状态。

5.2 大小汇总 ​

lsm_set_blob_size() 以指针大小对齐当前总长度,把本次模块需求改写成它在复合 blob 中的偏移;inode blob 还额外预留 rcu_head。

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

相关函数:lsm_set_blob_size、lsm_set_blob_sizes

c
static void __init lsm_set_blob_size(int *need, int *lbs)
{
    int offset;

    if (*need <= 0)
        return;

    offset = ALIGN(*lbs, sizeof(void *));
    *lbs = offset + *need;
    *need = offset;
}

static void __init lsm_set_blob_sizes(
        struct lsm_blob_sizes *needed)
{
    if (!needed)
        return;

    lsm_set_blob_size(&needed->lbs_cred,
                      &blob_sizes.lbs_cred);
    lsm_set_blob_size(&needed->lbs_file,
                      &blob_sizes.lbs_file);
    if (needed->lbs_inode && blob_sizes.lbs_inode == 0)
        blob_sizes.lbs_inode = sizeof(struct rcu_head);
    lsm_set_blob_size(&needed->lbs_inode,
                      &blob_sizes.lbs_inode);
    lsm_set_blob_size(&needed->lbs_task,
                      &blob_sizes.lbs_task);
    lsm_set_blob_size(&needed->lbs_bpf_map,
                      &blob_sizes.lbs_bpf_map);
    lsm_set_blob_size(&needed->lbs_bpf_prog,
                      &blob_sizes.lbs_bpf_prog);
}

调用方通过 needed->lbs_inode 等字段得到的是本模块偏移,不是原始请求大小。这个字段在初始化后不能再当作“需要多少字节”使用;混淆大小与偏移会导致模块读写其他 LSM 的状态。

5.3 Cache创建 ​

ordered_lsm_init() 在所有模块完成 prepare_lsm() 后才创建 file/inode cache,并依次执行各模块 init()。这是状态生效时机:hook 数组可以静态存在,但对象 blob 的内存布局要先完成。

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

相关位置:ordered_lsm_init

c
for (lsm = ordered_lsms; *lsm; lsm++)
    prepare_lsm(*lsm);

report_lsm_order();

if (blob_sizes.lbs_file)
    lsm_file_cache = kmem_cache_create(
            "lsm_file_cache", blob_sizes.lbs_file,
            0, SLAB_PANIC, NULL);
if (blob_sizes.lbs_inode)
    lsm_inode_cache = kmem_cache_create(
            "lsm_inode_cache", blob_sizes.lbs_inode,
            0, SLAB_PANIC, NULL);

lsm_early_cred((struct cred *)current->cred);
lsm_early_task(current);
for (lsm = ordered_lsms; *lsm; lsm++)
    initialize_lsm(*lsm);

如果一个模块被 lsm= 排除,它的 blob 需求不会汇总;如果模块被启用但 cache 创建失败,SLAB_PANIC 让错误成为内核级致命事件。这些边界保证运行期 hook 看到的对象状态布局是稳定的。

6. SELinux接入 ​

6.1 独占与身份 ​

SELinux 的模块描述位于 hook 数组尾部附近:名称是 selinux,ID 是 LSM_ID_SELINUX,flags 表明它是 legacy major 和 exclusive 模块,初始化入口是 selinux_init,blob 需求来自 selinux_blob_sizes。

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

相关定义:DEFINE_LSM(selinux)

c
DEFINE_LSM(selinux) = {
    .name = "selinux",
    .flags = LSM_FLAG_LEGACY_MAJOR | LSM_FLAG_EXCLUSIVE,
    .enabled = &selinux_enabled_boot,
    .blobs = &selinux_blob_sizes,
    .init = selinux_init,
};

这段声明不会加载 policy,也不会设置 Android domain;它只把 SELinux 接入内核 LSM 初始化框架。Android 用户空间的 SetupSelinux() 后续加载 policy,属于另一条启动阶段主线,详见 SELinux启动 和 SELinux架构。

6.2 Hook与策略 ​

内核 hook 注册完成后,SELinux 才能接收 VFS、Binder、exec、capability 等请求;策略加载前,SELinux 仍可能执行初始化和标签准备,但不能把“hook 存在”误认为“Android policy 已完整加载”。

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

相关函数:selinux_inode_permission

c
static int selinux_inode_permission(
        struct inode *inode, int requested)
{
    u32 sid = current_sid();
    struct inode_security_struct *isec;
    u32 perms;

    requested &= MAY_READ | MAY_WRITE
            | MAY_EXEC | MAY_APPEND;
    if (!requested)
        return 0;

    isec = inode_security_rcu(inode,
                              requested & MAY_NOT_BLOCK);
    if (IS_ERR(isec))
        return PTR_ERR(isec);

    perms = file_mask_to_av(inode->i_mode, requested);
    return avc_has_perm_noaudit(
            sid, isec->sid, isec->sclass,
            perms, 0, NULL);
}

上面只保留 hook 的核心输入,完整实现还包含 task AVC cache、audit required 和 policy sequence 处理。关键是它消费的状态来自两个地方:当前任务 SID 和 inode security blob;这些状态由更早的 LSM object initialization 与 SELinux label 路径提供。

6.3 Binder路径 ​

SELinux 对 Binder 的 hook 与文件 hook 使用同一 LSM 框架,却有不同消费者和参数。Binder driver 通过 security wrapper 交给 selinux_binder_transaction(),SELinux 再按发送者和接收者 credentials 的 SID 做策略查询。

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

相关函数:security_binder_transaction

c
int security_binder_transaction(
        const struct cred *from,
        const struct cred *to)
{
    return call_int_hook(
            binder_transaction, from, to);
}

这里没有 inode、file 或 path 参数,说明 LSM hook 的语义由调用点定义。把 binder_transaction 错当作 file_permission 的别名,会漏掉 Binder sender/receiver、引用转移和 context manager 等不同安全对象。

7. 查询接口 ​

7.1 活动模块 ​

内核提供 lsm_list_modules 系统调用,返回活动模块的数值 ID。它先把所需字节数写回用户空间,缓冲区太小返回 -E2BIG,flags 非零返回 -EINVAL,用户指针不可写返回 -EFAULT。

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

相关函数:sys_lsm_list_modules

c
SYSCALL_DEFINE3(lsm_list_modules,
        u64 __user *, ids,
        u32 __user *, size,
        u32, flags)
{
    u32 total_size =
            lsm_active_cnt * sizeof(*ids);
    u32 usize;
    int i;

    if (flags)
        return -EINVAL;
    if (get_user(usize, size))
        return -EFAULT;
    if (put_user(total_size, size) != 0)
        return -EFAULT;
    if (usize < total_size)
        return -E2BIG;

    for (i = 0; i < lsm_active_cnt; i++)
        if (put_user(lsm_idlist[i]->id, ids++))
            return -EFAULT;
    return lsm_active_cnt;
}

接口返回的是 ID,不是字符串名称;用户空间需要用 uapi/linux/lsm.h 的常量映射名称,或结合 securityfs 的 /sys/kernel/security/lsm 文本。这个设计避免了把名称字符串布局写进稳定 syscall ABI。

7.2 Self属性 ​

同一个文件还提供 lsm_set_self_attr、lsm_get_self_attr,把当前 task 的 LSM 属性交给统一 security 层。它们与 Android 常见的 /proc/self/attr/current 并非完全同一接口,后者是传统 SELinux 属性路径;阅读新 LSM API 时要确认调用的是哪套 ABI。

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

相关函数:sys_lsm_set_self_attr、sys_lsm_get_self_attr

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

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

系统调用只是 ABI 入口,具体模块是否支持某属性由已启用 LSM 的 hook 决定。一个设备启用 SELinux 并不代表它支持所有 LSM 属性;flags、缓冲区大小和模块能力仍是条件路径。

8. 反向证据 ​

8.1 模块列表测试 ​

LSM selftest 不只检查“系统调用返回成功”,还覆盖空指针、缓冲区过小、非法 flags 和实际模块列表与 securityfs 文本的一致性。

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

相关测试:size_null_lsm_list_modules、size_too_small_lsm_list_modules、correct_lsm_list_modules

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

    ASSERT_NE(NULL, syscall_lsms);
    errno = 0;
    ASSERT_EQ(-1, lsm_list_modules(
            syscall_lsms, &size, 0));
    ASSERT_EQ(E2BIG, errno);
    ASSERT_NE(1, size);

    free(syscall_lsms);
}

TEST(correct_lsm_list_modules)
{
    __u32 size = sysconf(_SC_PAGESIZE);
    __u64 *syscall_lsms = calloc(size, 1);
    char *sysfs_lsms = calloc(size, 1);

    ASSERT_EQ(0, read_sysfs_lsms(
            sysfs_lsms, size));
    int count = lsm_list_modules(
            syscall_lsms, &size, 0);
    ASSERT_LE(1, count);

    // ... 按每个 LSM ID 映射名称并与 securityfs 文本比较。
}

第一个测试的输入是只提供 1 字节容量,断言是 errno 为 E2BIG 且 size 被更新为需求值,证明调用方可以先探测容量。第二个测试把 syscall 返回的 ID 列表与 /sys/kernel/security/lsm 中的名称逐项比对,证明活动列表和用户可见报告来自同一注册状态。测试没有证明 hook 的业务决策顺序,也不覆盖 Android policy allow 规则。

8.2 BPF LSM测试 ​

BPF selftest 用一个 LSM 程序监控 exec 和 mprotect,并验证同一程序不能重复 attach。它证明 LSM 框架可以承载 BPF 回调,且回调失败能够影响系统调用结果。

源码文件:kernel/common/tools/testing/selftests/bpf/prog_tests/test_lsm.c

相关函数:test_lsm

c
static int test_lsm(struct lsm *skel)
{
    struct bpf_link *link;
    int err;

    err = lsm__attach(skel);
    if (!ASSERT_OK(err, "attach"))
        return err;

    /* Check that already linked program can't be attached again. */
    link = bpf_program__attach(
            skel->progs.test_int_hook);
    if (!ASSERT_ERR_PTR(link, "attach_link"))
        return -1;

    err = exec_cmd(&skel->bss->monitored_pid);
    if (!ASSERT_OK(err, "exec_cmd"))
        return err;

    ASSERT_EQ(skel->bss->bprm_count, 1,
              "bprm_count");

    err = stack_mprotect();
    if (!ASSERT_EQ(err, -1, "stack_mprotect") ||
        !ASSERT_EQ(errno, EPERM, "stack_mprotect"))
        return err;
    ASSERT_EQ(skel->bss->mprotect_count, 1,
              "mprotect_count");

    lsm__detach(skel);
    return 0;
}

测试输入包括一次 exec、一次可执行栈 mprotect 和重复 attach。关键断言分别是 exec hook 计数为 1、mprotect 返回 EPERM、重复 attach 返回错误;它证明 BPF hook 的加载、调用和拒绝结果闭环。它没有证明 SELinux 与 BPF 在 Android 产品配置中一定同时启用,实际设备仍要检查 CONFIG_BPF_LSM、lsm= 和 /sys/kernel/security/lsm。

9. 调试方法 ​

9.1 顺序检查 ​

bash
# 查看当前内核报告的 LSM 顺序;名称来自实际已启用模块。
adb shell cat /sys/kernel/security/lsm

# 查看内核启动日志中的 CONFIG_LSM、lsm=和初始化顺序。
adb shell dmesg | grep -E 'initializing lsm=|CONFIG_LSM|boot arg lsm='

# 检查 LSM syscall 是否可用于列出数值 ID。
adb shell getconf PAGESIZE

如果 /sys/kernel/security/lsm 不存在,先检查 securityfs 是否挂载以及内核是否启用 CONFIG_SECURITY; 不要直接把“文件不存在”解释为 SELinux 未启用。

9.2 Hook定位 ​

bash
# 从内核公共入口追到具体模块实现。
rg -n 'security_inode_permission|call_int_hook|selinux_inode_permission' \
  kernel/common/security/security.c \
  kernel/common/security/selinux/hooks.c

# 查看某个模块是否注册了目标 hook,以及是否受构建条件控制。
rg -n 'LSM_HOOK_INIT\(inode_permission|LSM_HOOK_INIT\(binder_transaction|CONFIG_' \
  kernel/common/security/selinux/hooks.c \
  kernel/common/security/bpf/hooks.c

静态搜索只能证明源码中存在回调;要证明运行期调用了它,还需要 tracepoint、动态调试、内核日志或相应 selftest。尤其是未出现在 lsm= 顺序中的模块,其静态数组存在也不代表回调 active。

9.3 对象状态 ​

bash
# 查看进程的 SELinux context 和 Linux credentials,确认主体状态。
adb shell 'id; id -Z; grep -E "^(Uid|Gid|CapEff|CapBnd):" /proc/self/status'

# 查看文件对象的标签;它对应 SELinux inode/blob 的 target SID来源。
adb shell 'ls -lZ /system/bin/init /dev/socket/zygote'

# 查看 SELinux AVC cache 统计(内核配置启用时)。
adb shell 'cat /sys/fs/selinux/avc/cache_stats 2>/dev/null || true'

这些命令分别观察 Linux credentials、对象标签和 AVC 统计,不能单独证明某次请求经过了哪个 LSM 槽。要定位一次拒绝,仍需把调用点、hook 顺序、返回值和审计日志串起来。

10. 闭环复述 ​

  1. 从 DEFINE_LSM(selinux) 开始,复述 security_init() 如何读取 CONFIG_LSM/lsm=,为什么 prepare_lsm() 先于 initialize_lsm(),以及独占模块如何影响选择。
  2. 从 selinux_hooks[] 的 LSM_HOOK_INIT(inode_permission, ...) 开始,追到 security_add_hooks()、static call 槽和 security_inode_permission(),说明一个未启用模块为什么不会被调用。
  3. 给定两个整数型 LSM hook,分别构造“第一个模块返回拒绝”和“所有模块返回默认值”的路径,解释 call_int_hook() 何时短路;再对比 call_void_hook() 为什么会执行全部回调。
  4. 从 lsm_blob_sizes 说明 SELinux、BPF LSM 如何共享 inode/file/task 的复合状态空间,并指出在对象 cache 创建后再注册模块会破坏什么不变量。
  5. 使用 lsm_list_modules selftest 的 E2BIG 和列表一致性断言,说明测试证明了活动模块查询 ABI 的哪些边界,哪些策略业务仍需另行验证。

掌握这条链后,LSM 就不再是 SELinux 前面的一层黑盒:内核安全调用点进入统一 wrapper,框架依据模块顺序和 hook 默认值调度回调,模块各自消费自己的策略与对象状态,结果再回到调用点。SELinux 的强制访问控制只是其中一个消费者;理解这一层,才能准确解释多 LSM 共存、root/capability 限制、Binder 安全检查和 Android 设备上的实际模块顺序。