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 的编译系统会帮你做这些事:

  1. 收集所有 .te、.fc、.if 文件
  2. 调用 m4 宏处理器展开条件编译
  3. 调用 checkpolicy 生成二进制文件

我曾经手写过一次完整的 policy.conf,大概 5000 行。说实话,那感觉就像在写天书。后来我学乖了,能用宏定义就用宏定义,能拆分文件就拆分文件。

7.2 策略二进制文件生成

编译出来的二进制文件,结构其实很清晰。我画了一张图,你看一眼就明白了。

SELinux 策略二进制文件结构 策略文件头(版本号、魔数、校验和) 访问向量缓存(AVC) 类型强制规则(TE) 角色、用户、约束条件 文件上下文映射(FC) 尾部标记 + 校验和

你看,二进制文件就是一块一块拼起来的。内核加载时,会逐块解析,校验完整性。如果校验和不通过,内核直接拒绝加载。

小技巧: 你可以用 seinfo 工具查看已加载策略的统计信息。比如 seinfo -t 列出所有类型,seinfo -r 列出所有角色。这在调试时特别有用。

7.3 内核加载流程

策略怎么进内核的?我简单梳理一下流程:

  1. init 进程启动:Android 启动时,init 进程会读取 /sepolicy/system/etc/selinux/ 下的策略文件。
  2. 调用安全系统调用:init 通过 security_load_policy() 系统调用,把二进制策略传给内核。
  3. 内核安全模块处理:LSM(Linux Security Module)框架中的 SELinux 钩子函数接收数据。
  4. 策略验证:内核检查版本号、校验和、结构完整性。
  5. 切换策略:如果验证通过,内核用新策略替换旧策略。注意,这里是原子操作——要么全换,要么不换。

我曾经在调试一个启动 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 阶段设定
警告: 热更新时,如果新策略有语法错误,内核会拒绝加载,但旧策略仍然有效。不过,如果新策略导致 AVC 拒绝大量访问,系统可能变得不稳定。我曾经在测试机上热更新了一个过于严格的策略,结果 adbd 直接被拒绝访问,机器变砖了。所以,生产环境慎用热更新

7.4.2 Android 中的热更新实践

Android 的 sepolicy.cpp 里实现了热更新逻辑。大致流程是:

  1. 编译新策略为二进制文件
  2. 通过 security_load_policy() 加载
  3. 调用 selinux_android_restorecon() 重新标记文件上下文
  4. 通知所有守护进程重新加载上下文

我个人建议,在开发阶段可以频繁热更新来调试。但到了量产阶段,最好把策略固化到系统镜像里。你想想看,万一 OTA 更新时策略加载失败,用户手机可能直接卡在开机界面。

7.5 避坑指南

最后,我总结几个实战中容易踩的坑:

  • 版本不匹配:编译时 -c 参数必须和内核匹配。Android 12 以上建议用 -c 32
  • 文件上下文遗漏:只更新了 .te 文件,忘了更新 .fc 文件。结果文件标签没变,规则不生效。
  • 热更新后不 restorecon:新策略加载了,但已有文件的标签还是旧的。需要手动跑 restorecon -R /
  • 策略文件过大:超过 1MB 的策略文件,加载时间可能超过 1 秒。在低端设备上,这个时间会更长。

好了,关于策略编译和加载,我就讲这么多。记住一句话:策略写得再好,加载不进去也是白搭。多练练编译和加载流程,遇到问题才能快速定位。


公众号:蓝海资料掘金营,微信deep3321