1. 为什么我决定自己动手实现一个LSM demo
接触Linux安全模块(LSM)有一段时间了,但一直觉得停留在“看文档+改配置”的层面,对底层机制缺少真正的体感。纸上得来终觉浅,所以这次决定自己动手写一个简单的LSM demo,把内核安全模块的加载、挂接、策略生效整条链路完整跑一遍。这个demo的目标很明确:不做大而全的安全系统,只实现一个能拦截特定文件读操作并打印日志的最小可运行模块,用来验证LSM框架的核心流程。选择这个切入点是因为文件读操作是LSM最典型、最容易观察效果的场景——拦截点明确、回调简单、日志验证直观。
这个demo适合谁看?我的判断是:对Linux内核机制有一定了解、但还没实际写过内核安全模块的开发者,以及对系统安全、容器隔离、沙箱实现感兴趣的同学。整个过程不需要你有多深的内核功底,但需要你具备基本的C语言能力、Linux操作经验,以及做好反复编译内核模块的心理准备。
先说清楚LSM到底是什么。Linux安全模块是一个内核级的框架,它不自己实现安全策略,而是在内核的关键操作路径上提供一系列钩子函数(hook),让安全模块(比如SELinux、AppArmor)能够挂接自己的检查逻辑。你可以把它理解成一座桥:内核本来可以直接放行所有操作,LSM在桥上设置了若干检查点,每个安全模块都可以在这些检查点部署自己的“哨兵”,哨兵有权放行或拦截。这种设计的好处是,安全策略与内核主体逻辑解耦,不同的安全模块可以按需加载,甚至可以同时共存(通过LSM的blob机制和hook优先级机制)。
在我动手之前,有几个核心问题需要先搞清楚:LSM的钩子是怎么注册的?模块加载后内核怎么知道要调用我的回调?我的安全策略应该放在哪里执行?这些问题的答案直接决定了demo的实现路径。下面先梳理整体架构和设计思路。
2. LS M的整体设计与核心思路拆解
2.1 从系统调用到安全模块的完整调用链
要理解LSM的工作方式,首先要沿着系统调用的路径走一遍。以read操作举例:用户态程序调用read()后,内核执行到对应的vfs_read(),在真正操作文件内容之前,内核会经过一个关键的检查点——security_file_permission()。这个函数就是LSM框架暴露的钩子之一,它会遍历所有已注册的LSM模块,逐个调用它们注册的file_permission回调。如果任何一个模块返回非零值,这次read操作就会被拒绝,用户态会收到一个错误码(通常是EPERM或EACCES)。
这个设计有几个值得注意的点:
- 钩子不是“某个模块私有”的,而是所有LSM模块共享同一套钩子列表
- 返回值的语义是约定好的:0表示放行,负的错误码表示拒绝
- 安全检查发生在真正操作资源之前,所以不会产生“操作到一半被拦”的中间状态
我最初以为LSM的钩子调用是“先到先得”,只要有一个模块放行就算通过。实际不是这样。每个钩子点上的检查是“与”的关系:所有模块都放行才放行,任何一个模块拒绝就拒绝。这意味着如果你同时加载了SELinux和自己的demo模块,即使你的模块放行了,SELinux如果拒绝,操作照样被拦。这个语义我在写demo时特别注意,因为调试过程中很容易出现“我的回调没生效”的错觉——很可能是其他安全模块先拦截了。
2.2 为什么选择Linux Security Module框架而不是其他方案
实现内核安全功能的方式不止LSM一种,常见的选择还有:
- 直接修改内核源码,在特定系统调用里硬编码检查逻辑
- 使用内核审计框架(audit)做事后记录
- 使用eBPF(扩展的伯克利包过滤器)在跟踪点挂载程序
- 使用LSM框架挂接标准钩子
每条路径都有各自适用的场景。直接改内核源码最灵活,但维护成本极高,每次内核版本升级都要重新适配,而且很难做到与上游内核共存,基本等于维护一个私有分支。eBPF方案很强大,尤其适合观测和轻量拦截场景,但LSM类型的eBPF程序依赖较新的内核版本(5.7+),而且对一些深层安全语义的表达不如原生LSM模块直接。audit框架只能记录,不能主动拦截,所以这套demo的目标场景(拦截某类文件访问)没法直接用audit实现。
LSM框架的优势在于:不修改内核主线的代码,以模块形式加载,与内核版本的兼容性靠一套稳定的钩子接口来保证,钩子函数的语义清晰,非常适合做安全策略的快速原型验证。对于我的demo,LSM是最贴合目标的路径——既能验证安全模块的加载机制,又有明确的钩子接口可以依赖,后续如果想扩展策略,也只需在回调里加逻辑即可。
2.3 关键技术前提与运行环境确认
动手之前,我列了一份环境清单,建议你也先确认这些条件,否则编译和加载环节会很折腾:
| 项目 | 建议要求 | 备注 |
|---|---|---|
| Linux内核版本 | 4.7及以上 | 低版本也可用,但部分接口有差异 |
| 内核源码 | 与当前运行内核版本匹配 | 编译模块需要内核头文件 |
| 编译器 | gcc及make | 需与内核版本兼容 |
| 内核配置 | 启用LSM框架相关选项 | 见下文说明 |
| 系统权限 | root或可sudo | 加载模块需要特权 |
我用的内核版本是6.x(具体小版本号只要和源码对应就行),编译环境是标准发行版内核加对应头文件包。注意:如果发行版默认安装的内核没有启用某些LSM相关配置,你的模块可能能编译但加载失败,或者加载成功但钩子不触发。我建议在编译前就检查一下当前内核的config文件(通常在/boot目录或/proc/config.gz中),确认CONFIG_SECURITY和CONFIG_SECURITYFS是开启的。
重要提示:如果你运行的内核已经启用了SELinux或AppArmor,demo模块加载后所有钩子都会参与调用链。由于这些模块的默认策略可能拦截大部分操作,调试时要留意“系统行为异常不一定是你的模块导致的”。建议在隔离的虚拟机或专用测试机上进行实验,不要在生产环境直接测试。
2.4 最小可运行模块的模块结构规划
我规划的demo模块包含四个核心部分:
- 模块初始化和退出函数:负责注册和注销LSM钩子
- 钩子回调函数:实现具体的检查策略
- 策略控制逻辑:通过模块参数或procfs条目动态控制是否启用拦截,方便测试
- 日志输出:用printk记录每次拦截或放行事件
目标是在不影响系统正常工作的前提下,对指定文件的读操作产生可观察的拦截效果。这样既能证明LSM框架确实生效,又不会把测试环境搞挂,方便反复实验。
3. 核心细节解析与实操要点
3.1 LSM钩子的注册与注销机制
在旧版本内核中,LSM模块通常通过security_add_hooks()函数注册钩子。而在较新的内核版本里,官方更推荐使用结构化的方式:定义一个security_hook_list数组,然后通过security_add_hooks()(或更现代的lsm_register_hooks(),具体看你使用的分支)挂载。在6.x内核中,一般定义如下结构:
static struct security_hook_list demo_hooks[] = { LSM_HOOK_INIT(file_permission, demo_file_permission), };LSM_HOOK_INIT宏的作用是把钩子函数的地址填充到对应的hook结构体里。file_permission是LSM框架暴露的钩子名,demo_file_permission是我实现的回调函数。还需要定义一个表示LSM模块自身信息的结构体:
static struct lsm_id demo_lsm_id = { .name = "demo_lsm", .id = 42, // 需要选取一个未占用的LSM ID };不同的LSM模块在注册时往往会提供不同的优先级或顺序,但作为demo,直接使用默认顺序即可,不需要刻意调整优先级。
注销逻辑相对简单,在模块退出函数中调用security_delete_hooks()(或对应版本接口),把之前注册的钩子列表移除即可。不过我实际测试发现,如果模块被强制卸载(rmmod -f),内核可能来不及完整执行注销流程,后续再加载同名模块可能出现状态残留。所以调试时尽量正常卸载,不要图省事用强制参数。
3.2 回调函数的核心逻辑与参数含义
我们关注的file_permission钩子,其函数签名是:
int demo_file_permission(struct file *file, int mask);第一个参数是当前操作涉及的文件对象,第二个参数是操作掩码。mask中常见的标志位包括MAY_READ(读)、MAY_WRITE(写)、MAY_EXEC(执行)。我们demo只需关注MAY_READ标志。
回调的编写要点在于:判断“是否是我关心的文件”和“是否为读操作”,然后决定返回0还是返回错误码。这里有一个容易犯的错误:很多人以为回调返回“拒绝”后用户程序会立刻看到错误,但实际上用户态拿到的是read系统调用的返回结果,可能是EPERM(操作不允许)。要验证拦截是否生效,需要在用户态测试程序里检查返回值,并配合dmesg查看内核日志。
为了避免无差别拦截导致系统异常,demo里我加入了一个简单的路径匹配逻辑:通过内核辅助函数获取文件路径,然后与预置的目标路径比较。注意内核态不能直接使用用户态常用的字符串函数和路径解析方式,需要用d_path等内核API,而且要小心处理路径缓冲区的大小(一般定义为PATH_MAX,即4096字节)。
3.3 模块参数与动态策略控制
写模块时我顺手实现了两个模块参数,极大方便了调试:
- enable_intercept:布尔值,控制是否真正拦截,还是仅记录日志,默认为true
- target_path:字符串,指定要监控的文件路径,默认为空表示不限制路径
这样一来,加载模块时可以传入参数:
insmod demo_lsm.ko target_path="/tmp/testfile" enable_intercept=1也可以在运行时通过/sys/module/demo_lsm/parameters/下的文件修改参数值(如果模块允许写入)。这个设计很实用,因为调试初期你并不想真的拦截系统内所有文件的读取,只想观察某个指定路径的访问情况。
3.4 关于LSM ID与模块名称的注意事项
新版本内核对LSM模块的标识有更严格的要求,包括lsm_id结构体中的id必须唯一。如果你加载模块时遇到“LSM ID冲突”之类的错误,多半是id选取和其它模块重复了。可以通过如下方式查看系统当前注册的LSM模块列表:
cat /sys/kernel/security/lsm如果你看到系统里已经有capability、selinux、apparmor等字样,说明这些模块已经注册。新注册的demo模块会添加到这个列表末尾。注意:有些发行版内核将某些LSM设为“不可禁用”,即便你没有主动加载它们,相关钩子也已经参与调用链。这对demo的调试没有实质影响,但你要意识到系统中的安全策略是一个叠加的组合。
3.5 为什么我选择在file_permission而非其他钩子做拦截
LSM框架提供了大量钩子,比如file_open、file_permission、mmap_file、path_mknod等。我最终选择了file_permission,原因是:
- 它覆盖的调用路径广:read、write、mmap等常见文件操作都会经过这个钩子
- 它的语义清晰:mask参数直接告诉你是什么操作
- 它容易观察:配合printk日志,可以清楚看到每次操作发生的时间、进程、路径
如果选择file_open,也能达到类似效果,但file_open只覆盖文件打开阶段,对于一些已经打开文件描述符的后续读取操作,就观察不到了。所以file_permission更适合“拦截文件读操作”这个目标。
4. 实操过程与核心环节实现
4.1 准备内核头文件与构建环境
Demo模块编译必须依赖与当前内核匹配的头文件。这里分两种情况:
- 如果你的发行版内核是自己编译并安装的,源码目录一般就在/usr/src/linux-$(uname -r)
- 如果用的是发行版自带内核,通常安装对应名称的kernel-devel或linux-headers包即可
我用的环境是某Linux发行版,直接安装headers包:
sudo apt install linux-headers-$(uname -r)执行完确认一下目录结构是否完整:
ls /usr/src/linux-headers-$(uname -r)关键是确认存在include目录、Makefile以及Module.symvers(如果有)。Headers包不完整会导致编译时找不到内核头文件或者生成模块时缺少符号信息,编译报错时不要急着怀疑代码,先检查环境。
4.2 完整Makefile与源码编译
创建一个工作目录,比如demo_lsm,里面放demo_lsm.c和Makefile。Makefile的写法有标准模板:
obj-m += demo_lsm.o KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build all: make -C $(KERNEL_DIR) M=$(PWD) modules clean: make -C $(KERNEL_DIR) M=$(PWD) clean注意:这里KERNEL_DIR指向的是内核构建目录,通常就是符号链接到/usr/src/linux-headers-xxx。如果多版本内核并存,最好显式指定KERNEL_DIR,避免用uname -r拿到预期之外的版本。
在源码中,模块入口与退出函数必须用module_init和module_exit宏标注,否则insmod时内核无法正确找到入口点。另外要给模块添加许可证声明,建议写GPL,否则内核会报“module license 'unspecified' taints kernel”警告,有些内核符号在非GPL模块下也无法链接。
MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple LSM demo module");4.3 demo模块源码逐段解析
下面给出一份能编译通过的最小源码(按6.x内核接口编写),我建议你通读一遍并理解每个字段的含义,而不是直接复制粘贴:
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/security.h> #include <linux/path.h> #include <linux/dcache.h> #include <linux/fs.h> #include <linux/namei.h> #include <linux/slab.h> static char *target_path = "/tmp/testfile"; module_param(target_path, charp, 0644); MODULE_PARM_DESC(target_path, "Path to monitor"); static bool enable_intercept = true; module_param(enable_intercept, bool, 0644); MODULE_PARM_DESC(enable_intercept, "Enable actual interception"); static int demo_file_permission(struct file *file, int mask) { char *buf; char *path; int ret = 0; if (!(mask & MAY_READ)) return 0; if (!target_path[0]) return 0; buf = (char *)__get_free_page(GFP_KERNEL); if (!buf) return 0; path = d_path(&file->f_path, buf, PAGE_SIZE); if (IS_ERR(path)) goto out; if (strcmp(path, target_path) == 0) { printk(KERN_INFO "demo_lsm: denied read on %s by pid %d\n", path, current->pid); if (enable_intercept) ret = -EPERM; } out: free_page((unsigned long)buf); return ret; } static struct security_hook_list demo_hooks[] = { LSM_HOOK_INIT(file_permission, demo_file_permission), }; static struct lsm_id demo_lsm_id = { .name = "demo_lsm", .id = 42, }; static int __init demo_lsm_init(void) { security_add_hooks(demo_hooks, ARRAY_SIZE(demo_hooks), &demo_lsm_id); printk(KERN_INFO "demo_lsm: module loaded\n"); return 0; } static void __exit demo_lsm_exit(void) { security_delete_hooks(demo_hooks, ARRAY_SIZE(demo_hooks)); printk(KERN_INFO "demo_lsm: module unloaded\n"); } module_init(demo_lsm_init); module_exit(demo_lsm_exit);这段代码里有几个细节需要说明:
第一,d_path返回的是从根目录开始的完整路径(如“/tmp/testfile”),而你传入的target_path也需要是绝对路径。曾经有人传入相对路径,结果永远匹配不上,半天查不出原因。第二,每次回调都分配一页内存并调用d_path是有性能开销的,在生产级模块中应该做缓存或更精细匹配;demo里这样写是为了可读性和稳定性优先。第三,strcmp比较路径时是大小写敏感的,如果你要匹配的路径涉及不同大小写形式,需要额外处理。
4.4 编译、加载与验证流程
编译和加载流程如下:
make sudo insmod demo_lsm.ko target_path="/tmp/testfile" enable_intercept=1加载后查看内核日志,确认初始化成功:
dmesg | tail -20如果没有看到“demo_lsm: module loaded”,加载多半失败了,用lsmod确认一下模块是否在列表里,并用dmesg查看报错详情。
接下来准备一个测试文件:
echo "hello demo" > /tmp/testfile cat /tmp/testfile正常情况下,cat会报错:Operation not permitted。查看dmesg会看到我们打印的denied日志。为了确认拦截目标是精确的,再创建一个不在监控列表中的文件:
echo "hello world" > /tmp/otherfile cat /tmp/otherfile此时应该正常输出,没有任何拦截日志——说明我们的路径过滤逻辑生效了。
4.5 动态开关策略的验证方法
如果你在测试过程中不想一直拦截,可以动态修改模块参数。只要参数文件的权限允许写入(0644表示所有者可读写,其他用户可读),root可以这样操作:
echo 0 > /sys/module/demo_lsm/parameters/enable_intercept cat /tmp/testfile此时cat应该能正常读到内容,同时dmesg里仍会打印“denied”日志(因为我们选择在disable时只记录不拦截)。再把开关打开,恢复正常拦截。这种方式对调试非常有帮助,尤其是判断“到底是LSM回调没触发,还是触发了但没拦截”。
注意:模块参数中字符串类型的target_path通过sysfs修改时,长度不能超过原始参数定义时的最大值,否则写入会失败。实际测试中我发现,如果一开始只给了很短的默认路径,后续想改成更长的路径会报错。解决方法是编译时把模块参数的默认值或传入值预留足够长度,或者干脆在代码里用固定大小字符数组承载。
4.6 结合用户态测试的完整验证
内核模块只完成了拦截功能,用户态还需要配合做验证。我在测试机上写了个简单的C程序,模拟“读取受保护文件”和“读取普通文件”两种场景:
#include <stdio.h> #include <errno.h> #include <string.h> #include <fcntl.h> #include <unistd.h> void read_file(const char *path) { int fd = open(path, O_RDONLY); if (fd < 0) { printf("open %s failed: %s\n", path, strerror(errno)); return; } char buf[64]; ssize_t n = read(fd, buf, sizeof(buf)-1); if (n < 0) { printf("read %s failed: %s\n", path, strerror(errno)); } else { buf[n] = '\0'; printf("read %s success: %s\n", path, buf); } close(fd); } int main() { read_file("/tmp/testfile"); read_file("/tmp/otherfile"); return 0; }编译后运行,预期输出是:受保护文件read失败,错误为Operation not permitted;普通文件read成功。这里有一个有趣的细节:即使open成功,后续read也可能失败,因为file_permission钩子会在每次read时触发。所以用户态程序拿到“open成功但read失败”的组合是完全可能的,这点别被误导成“模块没拦截”。
5. 开机自动加载与模块持久化
5.1 把demo模块纳入开机加载流程
临时加载只适合开发调试,如果想要这个安全模块在系统启动后自动生效,还需要在模块加载之前将模块文件复制到标准模块目录下,并配置加载策略:
sudo cp demo_lsm.ko /lib/modules/$(uname -r)/extra/ sudo depmoddepmod会重新扫描内核模块依赖关系,生成modules.dep等文件。之后用modprobe加载时,系统才能找到这个模块。接着把加载命令写入开机启动配置,常见的做法是:
- 在/etc/modules-load.d/下新建一个conf文件,写入模块名(不含.ko扩展名)
- 或者使用systemd的modprobe服务加载
5.2 内核启动参数与LSM启用的关系
这里要强调一个细节:模块本身加载了,并不意味着LSM框架一定会调用它的钩子。部分版本内核(特别是启用了一些安全特性后)要求LSM模块在启动早期通过安全子系统注册,而简单模块用insmod在系统运行阶段加载也受支持。如果你想在启动时就加载,可以通过内核引导参数禁用SELinux/AppArmor(仅在测试环境这么做!),或者把模块做成启动早期加载的方式。对demo来说,运行阶段insmod就足够了,不需要折腾引导参数。
5.3 卸载与状态清理
卸载模块使用rmmod或modprobe -r。我的建议是卸载后检查一下dmesg,确认security_delete_hooks执行正常,没有残留日志或内核告警。如果在同一内核会话中反复加载/卸载模块,每次加载都会尝试注册相同的LSM ID和钩子,如果上次注销不完整,本次会失败或产生重复注册。遇到这种情况,先重启测试环境再试往往是最快的解决办法。
6. 常见问题与排查技巧实录
6.1 编译报错:某些宏定义找不到
编译LSM模块最常见的报错是找不到security_add_hooks、LSM_HOOK_INIT等符号或宏。原因通常是:
- 内核头文件路径不对,编译器没有找到include/linux/security.h
- 内核版本较老,所用接口名称不同
- CONFIG_SECURITY未启用,导致security.h中相关声明缺失
我建议检查内核配置文件:
grep CONFIG_SECURITY /boot/config-$(uname -r)如果CONFIG_SECURITY未开启,你需要重新编译内核或在当前内核上启用该选项,否则LSM相关头文件无法提供完整声明。
6.2 模块加载成功但钩子不触发
看着dmesg里模块加载日志正常,但访问目标文件时没有任何反应。这种情况我排查过几次,最后发现的原因高度一致:路径不匹配。要么是target_path传成了相对路径,要么是实际文件路径与传入路径存在符号链接差异,比如/tmp/testfile在系统里可能是某个tmpfs的实际路径,解析后和d_path输出的路径不一致,导致strcmp不相等。
建议在回调里临时打印每次读操作的路径(不判断是否匹配),先把路径看明白再做过滤逻辑。这是最直接的排查手段。
6.3 所有文件都被拦截或系统明显卡顿
如果模块对整个系统的文件访问产生了广泛影响,首先要检查是不是mask判断写错了。例如你希望只拦截MAY_READ,但代码里判断条件写成了“if (mask & MAY_READ) return -EPERM;”,而没有区分是否为目标文件。一旦register钩子生效,所有读操作都会返回错误。解决思路很简单:每一步检查都先落日志,确认是“逻辑错了”还是“路径匹配范围过宽”。
我在调试中经常用到的思路是:先用enable_intercept=0加载模块,让它只打印日志不拦截,观察一段时间系统是否正常;确认匹配逻辑无误后,再把拦截开关打开。这套流程可以极大降低调试风险,也推荐给你。
6.4 卸载模块时报错“Resource temporarily unavailable”
出现这个提示,通常意味着某个进程正在使用模块的相关状态或被引用。LSM模块本身一般不会被进程持有引用,但如果你在回调里分配了资源没有释放,或者有正在执行的回调上下文尚未结束,就可能出现这种情况。常见处理方法是终止相关测试进程后等待片刻再卸载。如果实在无法卸载,重启后再加载新版本模块即可,不用在运行状态下死磕。
6.5 常见错误速查表
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 编译报错找不到security.h | 内核头文件不完整或路径错误 | 安装匹配的headers包,检查KERNEL_DIR |
| insmod报错Unknown symbol | 模块没有包含正确的内核符号,或头文件版本不匹配 | 检查Module.symvers,核对内核版本 |
| 加载成功但无日志 | 钩子未注册,或路径匹配不上,或日志级别被抑制 | 用sysctl或dmesg -n调整日志级别,打点调试 |
| 系统不稳定、大量拒绝 | mask判断过宽,或路径匹配条件过于泛化 | 先用只记录不拦截模式观察 |
| 卸载时异常 | 回调资源未释放或存在未完结上下文 | 终止相关进程,重启环境 |
7. 从demo迈向真实安全模块的路线
7.1 为demo补充合理的策略框架
当前demo的路径匹配用的是字符串比较,这在实际项目中不可扩展。真实安全模块通常需要:
- 基于inode的安全标记(security blob),而不是每次走路径解析
- 基于用户、进程、文件组合的多维策略决策
- 支持策略动态加载/更新,而不是内核模块参数写死
从LSM框架角度看,如果要给文件系统对象打安全标签,需要额外实现set和get安全标记相关的钩子。这会让模块复杂度上一个台阶,但也是迈向真实安全模块必须走过的路。
7.2 结合eBPF与LSM的混合方案
如果你的目标是快速原型验证或轻量级监控,eBPF+LSM的路径可能更合适:使用BPF LSM程序挂载到file_permission钩子,可以用更灵活的方式加载和卸载策略,不需要编译内核模块,迭代速度快很多。但BPF LSM依赖较新的内核和BPF特性,且对复杂状态管理能力有限。它适合做“快速实验”,而传统的LSM模块适合做“深度定制”。两者并非互相替代,而是互补关系。
7.3 安全模块的测试方法论
测试LSM模块,我的一点体会是:不能只测“模块能否拦截目标操作”,还要测“模块是否放行了不该放行的操作”“模块对系统整体性能影响多大”“模块与其他安全模块叠加时的行为是否符合预期”。我建议写一个自动化的测试脚本,覆盖以下场景:
- 目标文件读取被拦截,非目标文件正常读取
- 读写打开时的不同表现
- 软链接指向目标文件时的行为
- 删除、重命名目标文件时的行为
- 并发读取时模块的稳定性
8. 写在最后的经验小结
8.1 调试LSM模块最大的障碍不是代码,而是看不见的“叠加态”
系统里往往存在多个安全模块同时工作,你的模块只是其中一环。很多现象看似是“你的模块有问题”,实际是另一个模块先拒绝了操作。我建议每次调试都先查看系统当前LSM列表,再判断哪个环节拦截了操作。这种“叠加态”的排查思路,比单纯盯着自己的代码更有效。
8.2 先打印日志再优化性能
开发demo阶段不要去纠结每次回调分配内存会不会拖慢系统。先把观察点铺开,日志打全,行为摸清楚,后面再做缓存、路径匹配树、inode映射这些优化手段。性能优化放在功能稳定之后做,能省你大量反复编译的时间。
8.3 这个demo后续还能怎么扩展
如果你顺着这条线继续玩下去,我建议下一步尝试实现基于inode的安全标记,也就是不仅拦截路径,还能让不同文件携带不同的安全属性;再往后可以尝试把策略决策部分放到用户态,通过netlink或虚拟文件系统与内核模块交互,形成完整的策略管理与执行分离。这样你已经不是在“写demo”了,而是在设计一个内聚的安全子系统。
回到最初的问题:为什么我要自己动手实现一个LSM demo?因为只有真正写过一遍注册、回调、策略、调试的完整流程,你才会深刻理解LSM不只是内核里的“一堆钩子”,而是一套严谨的安全扩展机制。这种理解,光看文档是得不到的。