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_table | LSM 框架 | 每个 hook 的静态调用槽数组 | call_int_hook()/call_void_hook() |
lsm_blob_sizes | LSM 框架汇总 | cred/inode/file/task 等对象的复合 blob 大小 | 对象分配和模块访问 |
lsm_idlist | LSM 框架 | 已初始化模块的数值 ID | lsm_list_modules 系统调用 |
| SELinux state/AVC | SELinux | enforcing、策略、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
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
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
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
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
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
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
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
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
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
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
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
#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
#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
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
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
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
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)
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
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
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
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
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
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
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 顺序检查
# 查看当前内核报告的 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定位
# 从内核公共入口追到具体模块实现。
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 对象状态
# 查看进程的 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. 闭环复述
- 从
DEFINE_LSM(selinux)开始,复述security_init()如何读取CONFIG_LSM/lsm=,为什么prepare_lsm()先于initialize_lsm(),以及独占模块如何影响选择。 - 从
selinux_hooks[]的LSM_HOOK_INIT(inode_permission, ...)开始,追到security_add_hooks()、static call 槽和security_inode_permission(),说明一个未启用模块为什么不会被调用。 - 给定两个整数型 LSM hook,分别构造“第一个模块返回拒绝”和“所有模块返回默认值”的路径,解释
call_int_hook()何时短路;再对比call_void_hook()为什么会执行全部回调。 - 从
lsm_blob_sizes说明 SELinux、BPF LSM 如何共享 inode/file/task 的复合状态空间,并指出在对象 cache 创建后再注册模块会破坏什么不变量。 - 使用
lsm_list_modulesselftest 的E2BIG和列表一致性断言,说明测试证明了活动模块查询 ABI 的哪些边界,哪些策略业务仍需另行验证。
掌握这条链后,LSM 就不再是 SELinux 前面的一层黑盒:内核安全调用点进入统一 wrapper,框架依据模块顺序和 hook 默认值调度回调,模块各自消费自己的策略与对象状态,结果再回到调用点。SELinux 的强制访问控制只是其中一个消费者;理解这一层,才能准确解释多 LSM 共存、root/capability 限制、Binder 安全检查和 Android 设备上的实际模块顺序。
