11. SELinux基础(三):策略文件结构、编译与加载、策略版本
好,咱们接着聊SELinux。前两讲我们把SELinux的来龙去脉和基本概念捋了一遍。今天这一讲,我打算带大家钻进SELinux的“肚子”里,看看它的策略文件到底长什么样,又是怎么被编译和加载到系统里的。
说实话,我刚接触SELinux那会儿,最头疼的就是这堆策略文件。一堆.te、.fc、.if后缀的文件,看着就头大。但后来我发现,只要搞懂了它的骨架,这东西其实挺有章法的。
11.1 策略文件的“三驾马车”
SELinux的策略源文件,说白了就三种类型。我习惯叫它们“三驾马车”:
- .te 文件(Type Enforcement)—— 类型强制文件。这是核心,定义了主体(域)对客体(类型)能做什么操作。
- .fc 文件(File Context)—— 文件上下文文件。它告诉系统,哪个文件应该打上哪个安全标签。
- .if 文件(Interface)—— 接口文件。它封装了一些常用的策略规则,方便其他模块调用。
你想想看,这三个文件各司其职,缺一不可。.te 文件定规则,.fc 文件定标签,.if 文件定接口。嗯,这里要注意,.if 文件在Android里用得相对少一些,但在标准Linux发行版里非常常见。
核心理解: .te 文件是“做什么”,.fc 文件是“对谁做”,.if 文件是“怎么做更方便”。
11.2 .te 文件长什么样?
咱们直接看个例子。这是一个简化版的 .te 文件片段,模拟一个app域的策略:
# 定义一个域(domain)
type my_app_domain, domain;
# 定义一个类型(type)
type my_app_data_file, file_type, data_file_type;
# 允许域访问自己的数据文件
allow my_app_domain my_app_data_file:file { read write open getattr };
# 允许域创建socket
allow my_app_domain self:unix_dgram_socket create_socket_perms;
# 允许域使用binder通信
allow my_app_domain servicemanager:binder { call transfer };
这段代码,说白了就是三句话:
- 我定义了一个叫
my_app_domain的域。 - 我定义了一个叫
my_app_data_file的文件类型。 - 我允许这个域对这个文件类型进行读、写、打开等操作。
我个人习惯,写 .te 文件时,属性(attribute) 一定要用好。你看上面的 domain、file_type、data_file_type,这些都是属性。属性就像标签,把一堆类型归到一类,方便批量授权。
小技巧: 在Android里,大部分app域都继承了 untrusted_app 或 platform_app 这些预定义的属性。你写策略时,尽量复用这些属性,别自己从头造轮子。
11.3 .fc 文件——给文件打标签
.fc 文件的作用,就是告诉系统:某个路径下的文件,应该属于哪个类型。格式很简单:
# 正则表达式匹配路径,然后指定类型
/data/data/myapp(/.*)? u:object_r:my_app_data_file:s0
# 精确匹配
/system/bin/my_daemon u:object_r:my_daemon_exec:s0
第一行,匹配 /data/data/myapp/ 下的所有文件,把它们都标记为 my_app_data_file 类型。
第二行,精确匹配 /system/bin/my_daemon 这个可执行文件,标记为 my_daemon_exec 类型。
我在项目中遇到过一个问题:有个同事写 .fc 文件时,路径写错了,导致某个app的数据文件始终打不上正确的标签。结果那个app一运行就报权限错误,查了半天才发现是 .fc 文件里少了个斜杠。嗯,这种坑,踩过一次就记住了。
注意: .fc 文件中的路径匹配是正则表达式,不是简单的通配符。比如 .* 表示任意字符,(/.*)? 表示可选的后缀路径。写错了,标签就打不上。
11.4 策略编译——从源码到二进制
策略源文件写好了,怎么变成系统能识别的二进制策略呢?这就要靠 checkpolicy 和 secilc 这两个工具了。
在Android里,编译过程大致是这样的:
- 把所有 .te、.fc、.if 文件收集起来。
- 用
checkpolicy或secilc编译成二进制的.cil文件(Common Intermediate Language)。 - 再把 .cil 文件打包进
sepolicy镜像里。
编译命令大概长这样:
# 使用 secilc 编译
secilc -o out_policy.cil policy.conf
# 或者使用 checkpolicy(老版本)
checkpolicy -M -c 30 -o sepolicy policy.conf
这里 -c 30 指定了策略版本。为什么要指定版本?因为不同版本的Linux内核支持的SELinux特性不一样。你编译的策略版本太高,旧内核可能加载不了。
11.5 策略加载——让内核听你的
编译好的二进制策略,怎么加载到内核里?有两种方式:
- 静态加载: 在系统启动时,init进程读取
/sepolicy文件,通过security_load_policy()系统调用加载到内核。Android用的就是这种方式。 - 动态加载: 系统运行时,用
load_policy命令加载新的策略文件。这种方式在标准Linux里更常见。
在Android里,你可以在 init.rc 中看到类似这样的语句:
on early-init
# 加载SELinux策略
write /sys/fs/selinux/load 1
# 或者直接调用
restorecon -R /
我记得有一次,我在调试一个自定义ROM时,修改了策略文件,但忘了重新编译boot.img。结果刷机后系统直接起不来,卡在SELinux的初始化阶段。后来我学乖了,每次改策略都先编译成 .cil 文件,用 adb push 推到设备上测试,确认没问题再打包进镜像。
避坑指南: 我曾经因为策略版本不匹配,导致内核加载策略失败。Android 10 以后,策略版本通常是 30 或 31。如果你在编译时用了 -c 33,但内核只支持到 30,那策略就加载不进去。系统会直接进入 permissive 模式,甚至 panic。
11.6 策略版本——别小看这个数字
策略版本(Policy Version)是SELinux内核模块支持的一个特性编号。每个版本对应一组新的功能。比如:
| 版本号 | 新增特性 | 我的备注 |
|---|---|---|
| 15 | 支持多级安全(MLS) | Android 早期版本用的 |
| 20 | 支持条件策略(conditional policy) | 可以动态开关规则 |
| 26 | 支持策略能力(policy capabilities) | 比如 network_peer_controls |
| 30 | 支持扩展权限(extended permissions) | Android 10 默认版本 |
| 33 | 支持更细粒度的文件系统操作 | 新内核才支持 |
你想想看,如果你的内核只支持到版本30,但你编译了一个版本33的策略,内核会直接拒绝加载。所以,编译前一定要确认内核的SELinux版本。
查看当前系统的策略版本,可以用这个命令:
# 查看当前策略版本
cat /sys/fs/selinux/policyvers
# 或者查看内核支持的最大版本
cat /sys/fs/selinux/policycap
11.7 一张图看懂策略文件结构
说了这么多,我画了一张图,帮你把整个流程串起来。从源文件到内核加载,每一步都清晰可见。
11.8 总结一下
这一讲,我们聊了SELinux策略文件的三个核心类型:.te、.fc、.if。也聊了怎么把它们编译成二进制策略,再加载到内核里。最后还提到了策略版本这个容易被忽略的细节。
说白了,SELinux的策略文件就是一套“规则说明书”。你写得好,系统就安全;你写得不好,系统要么跑不起来,要么漏洞百出。我个人建议,刚开始接触时,多看看Android源码里的 system/sepolicy 目录,那里有现成的例子可以模仿。
嗯,今天就到这儿。记住,策略文件的结构不难,难的是写出既安全又高效的规则。多动手,多踩坑,慢慢就熟了。