7、SELinux策略编译与加载:checkpolicy工具使用,策略二进制文件生成,内核加载流程,策略热更新机制
好,咱们今天聊点硬核的。SELinux 策略写好了,它只是一堆 .te、.fc、.if 文件,对吧?内核不认识这些。你得把它编译成二进制,然后加载进内核。这个过程,我踩过的坑可不少。
说白了,策略编译和加载,就是把你写的「安全规则说明书」翻译成内核能执行的「机器指令」。今天我就把整个流程掰开揉碎了讲给你听。
7.1 checkpolicy:策略编译器
checkpolicy 是 SELinux 用户空间的工具。它的任务就是把策略源文件编译成二进制策略文件(.pp 或 .bin)。
我个人习惯在 AOSP 源码环境下直接用 m 编译,但如果你想单独调试,checkpolicy 命令行也够用。
7.1.1 基本用法
# 编译一个简单的策略
checkpolicy -M -c 30 -o policy.30 policy.conf
# 参数说明:
# -M 启用 MLS(多级安全)支持
# -c 指定策略版本(Android 常用 30 或 32)
# -o 输出二进制文件
# 最后一个参数是输入的 .conf 文件
嗯,这里要注意:-c 指定的版本号必须和内核支持的版本匹配。我在项目中遇到过,有人编译时用了 -c 32,但内核只支持到 30,结果加载直接失败。你想想看,这种问题排查起来多头疼。
7.1.2 从 .te 到 .conf 的流程
实际上,我们一般不直接写 policy.conf。AOSP 的编译系统会帮你做这些事:
- 收集所有 .te、.fc、.if 文件
- 调用
m4宏处理器展开条件编译 - 调用
checkpolicy生成二进制文件
我曾经手写过一次完整的 policy.conf,大概 5000 行。说实话,那感觉就像在写天书。后来我学乖了,能用宏定义就用宏定义,能拆分文件就拆分文件。
7.2 策略二进制文件生成
编译出来的二进制文件,结构其实很清晰。我画了一张图,你看一眼就明白了。
你看,二进制文件就是一块一块拼起来的。内核加载时,会逐块解析,校验完整性。如果校验和不通过,内核直接拒绝加载。
seinfo 工具查看已加载策略的统计信息。比如 seinfo -t 列出所有类型,seinfo -r 列出所有角色。这在调试时特别有用。
7.3 内核加载流程
策略怎么进内核的?我简单梳理一下流程:
- init 进程启动:Android 启动时,init 进程会读取
/sepolicy或/system/etc/selinux/下的策略文件。 - 调用安全系统调用:init 通过
security_load_policy()系统调用,把二进制策略传给内核。 - 内核安全模块处理:LSM(Linux Security Module)框架中的 SELinux 钩子函数接收数据。
- 策略验证:内核检查版本号、校验和、结构完整性。
- 切换策略:如果验证通过,内核用新策略替换旧策略。注意,这里是原子操作——要么全换,要么不换。
我曾经在调试一个启动 crash 时,发现是策略文件损坏了。init 加载到一半,内核校验失败,直接 panic。从那以后,我每次编译完策略都会先跑一遍 checkpolicy -d 做完整性检查。
7.3.1 内核中的关键代码路径
如果你感兴趣,内核源码里这些函数是核心:
// security/selinux/selinuxfs.c
static ssize_t sel_write_load(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
// 1. 从用户空间拷贝策略数据
// 2. 调用 security_load_policy()
// 3. 如果成功,更新全局策略指针
// 4. 刷新 AVC 缓存
}
嗯,这里有个细节:策略加载是阻塞的。在加载完成之前,所有 SELinux 检查都会暂停。所以生产环境上,策略文件不能太大,否则系统会短暂「卡住」。
7.4 策略热更新机制
热更新,说白了就是系统运行时换策略,不用重启。Android 从 8.0 开始支持这个特性。
7.4.1 热更新的原理
内核维护了一个策略版本号。每次加载新策略,版本号递增。AVC(Access Vector Cache)里每条缓存记录都带版本号。策略更新后,旧缓存自动失效。
我举个例子:
# 查看当前策略版本
cat /sys/fs/selinux/policyvers
# 加载新策略(需要 root)
load_policy /data/local/tmp/new_policy.bin
热更新不是万能的。有些策略变更必须重启:
| 变更类型 | 是否支持热更新 | 说明 |
|---|---|---|
| 添加新类型 | ✅ 支持 | 新类型直接生效 |
| 修改现有规则 | ✅ 支持 | 旧规则被覆盖 |
| 删除类型 | ❌ 不支持 | 内核不允许删除正在使用的类型 |
| 修改初始 SID | ❌ 不支持 | 必须在 init 阶段设定 |
7.4.2 Android 中的热更新实践
Android 的 sepolicy.cpp 里实现了热更新逻辑。大致流程是:
- 编译新策略为二进制文件
- 通过
security_load_policy()加载 - 调用
selinux_android_restorecon()重新标记文件上下文 - 通知所有守护进程重新加载上下文
我个人建议,在开发阶段可以频繁热更新来调试。但到了量产阶段,最好把策略固化到系统镜像里。你想想看,万一 OTA 更新时策略加载失败,用户手机可能直接卡在开机界面。
7.5 避坑指南
最后,我总结几个实战中容易踩的坑:
- 版本不匹配:编译时
-c参数必须和内核匹配。Android 12 以上建议用-c 32。 - 文件上下文遗漏:只更新了 .te 文件,忘了更新 .fc 文件。结果文件标签没变,规则不生效。
- 热更新后不 restorecon:新策略加载了,但已有文件的标签还是旧的。需要手动跑
restorecon -R /。 - 策略文件过大:超过 1MB 的策略文件,加载时间可能超过 1 秒。在低端设备上,这个时间会更长。
好了,关于策略编译和加载,我就讲这么多。记住一句话:策略写得再好,加载不进去也是白搭。多练练编译和加载流程,遇到问题才能快速定位。
公众号:蓝海资料掘金营,微信deep3321