对于刚做 Android 系统开发的同学,总会遇到那么几个让人抓狂的时刻。比如你写了一个再简单不过的 Native 守护进程,代码逻辑无懈可击,文件权限也给到了 777,甚至 root 都拿到了,但程序就是跑不起来。
这时候,如果你看到日志里跳出一行 avc: denied,别慌,也别急着把 SELinux 关了了事(setenforce 0)。那是系统在试图保护你,只是它有点“过度紧张”,把你当成了陌生人。
今天想聊聊 SELinux。这东西在 Android 5.0 之后成了强制标准,很多开发者对它的态度是“能关则关”。其实,配置 SELinux 并没有那么玄学,它更像是在给系统里的每个进程和文件发“身份证”和“通行证”。
咱们不整那些复杂的理论,就顺着一次真实的开发经历,看看怎么把这层安全网织好。
故事的开始:一个无法启动的服务
假设场景是这样的:我在 AOSP 里加了一个自定义服务叫 my_service,放在 /system/bin/ 下,通过 init.rc 启动。
代码编译烧录,重启手机。满心欢喜地期待服务跑起来,结果 logcat 里冷冷清清。打开 dmesg 或者 logcat -b kernel 一看,满屏都是这样的日志:
** 核心日志分析**
这行日志就是 SELinux 的“判决书”。别被它吓到了,我们像法医一样解剖一下:
第一步:给文件发“身份证”
日志里的 tcontext 暴露了问题:我们的 my_service 可执行文件,虽然躺在 /system/bin/ 下,但系统默认给它贴了个通用的 system_file 标签。
但在我们的策略里,我们希望它是一个专门的服务。所以得先给它换个“马甲”。
这就得提到 file_contexts 文件。这文件就像个户籍登记簿,告诉系统哪个路径对应哪个标签。
在设备的 sepolicy 目录下(通常是 device/<厂商>/<机型>/sepolicy/file_contexts),我们得加上这么一行:
/system/bin/my_service u:object_r:my_service_exec:s0
拆解来看:
/system/bin/my_service:这是文件的路径。u:object_r:这是固定的前缀,表示这是个文件对象。my_service_exec:这是重点。这是我们要定义的“类型”。在 SELinux 的语境里,给可执行文件加_exec后缀是个约定俗成的习惯,意思是“这是启动服务的源头”。s0:这是安全级别,Android 里一般都用s0,不用管它。
加上这一行,编译进镜像后,系统就知道:“哦,这个文件不是普通文件,它是 my_service 的启动器。”
第二步:定义“域”与规则
文件有了身份,那进程呢?
当 init 进程启动这个服务时,SELinux 会执行一个很酷的操作,叫域转换。它会看:“嘿,init 正在执行一个类型为 my_service_exec 的文件,那新生成的进程就应该进入 my_service 这个域。”
但这还不够,我们得在 .te 文件里把这些规则写清楚。这是整个 SELinux 策略的心脏。
在 my_service.te 文件里,我们通常会这么写:
# 1. 定义类型
type my_service, domain;
type my_service_exec, exec_type, file_type;
# 2. 这一行宏非常关键,它自动处理了 init 启动服务时的域转换逻辑
init_daemon_domain(my_service)
这几行代码就像是给 my_service 建立了一个专属的“房间”。以后这个程序跑起来,就被关在这个房间里。
第三步:开门与放行
现在,服务能启动了,但新的问题又来了。比如 my_service 想读写一个硬件节点 /dev/my_hw,或者想访问数据目录。
这时候,那个尽职的保安(SELinux)又出来了。它会检查:my_service 域的进程,有没有权限操作 my_hw_device 类型的文件?
如果没有策略,日志里就会疯狂刷 avc: denied。
这时候,很多新手会怎么做?直接 setenforce 0,把保安撤了。但这在发布版本里是绝对禁止的。正确的做法是,根据报错,把“通行证”补上。
在 .te 文件里加上允许规则:
# 允许 my_service 域,对 my_hw_device 类型的字符设备,进行读写打开
allow my_service my_hw_device:chr_file { read write open ioctl };
你看,逻辑是不是很清晰?
谁(my_service) -> 对什么(my_hw_device) -> 做什么(read write...)。
全景视角:SELinux 的“身份体系”
刚才我们只用了 file_contexts 和 .te 文件,但这只是冰山一角。为了让你对整个配置体系有个全景的认识,我们需要区分两类文件:一类是 “贴标签的” (上下文文件),一类是 “定规则的” (策略文件)。
在 Android 开发中,最常用的贴标签的主要有以下 4 个,它们各司其职,共同维护着系统的安全:
1. file_contexts(管文件)
这就是我们刚才用的。它负责给文件系统中的文件、目录、设备节点贴标签。
- 例子:
/system/bin/my_service u:object_r:my_service_exec:s0 - 作用: 告诉系统,这个路径下的东西是什么类型。
2. seapp_contexts(管App)
这个文件对应用开发者非常重要。它负责给 App 进程和 App 数据目录贴标签。
- 例子:
# 普通第三方 App
user=_app domain=untrusted_app type=app_data_file
# 系统特权 App
user=_app seinfo=platform domain=platform_app type=app_data_file
- 作用: 当你安装一个 App 时,系统会读取这个文件。如果是普通 App,就把它关进
untrusted_app的笼子里;如果是系统 App,就给它platform_app或priv_app的身份。
3. property_contexts(管属性)
如果你需要在代码里 setprop 或 getprop(操作系统属性),就需要配这个。
- 例子:
persist.vendor.myapp.enable u:object_r:my_app_prop:s0 - 作用: 定义哪些进程可以读/写哪些系统属性(如
ro.bootloader或persist.sys.xxx)。
4. service_contexts(管Binder)
如果你的服务是通过 Binder 暴露给其他 App 调用的,就需要配这个。
- 例子:
my_daemon u:object_r:my_daemon_service:s0 - 作用: 给 ServiceManager 里的服务注册名打标签,控制谁能调用这个服务。
补充说明: 除了这 4 个“上下文”文件,还有像 hwservice_contexts(管 HIDL 硬件服务)、genfs_contexts(管 /sys /proc 等内核生成文件)等,它们都属于“贴标签”的范畴。而所有的权限逻辑,最终都要汇聚到 .te 文件中通过 allow 规则来落实。
它是如何生效的?
你可能会问,改完这些文件,为什么必须重新编译?
因为 Android 的编译系统(Build System)在编译时,会把这些散落在各个目录下的 .te 文件和 file_contexts 收集起来,通过 checkpolicy 工具编译成一个二进制的 sepolicy 文件,最后打包进 boot.img 或者 system.img 里。
内核启动时,会加载这个二进制策略。所以,你改了源码,如果不重新编译刷机,内核里的保安手里拿的还是旧名单,当然不认你的新规则。
附录:
1. 偷懒神器:audit2allow
手动写规则虽然精准,但有时候太慢了。Android 提供了一个名为 audit2allow 的神器,它能自动分析日志并生成规则建议。
使用姿势:
- 抓取日志:
adb logcat | grep "avc:" > avc.log
- 生成建议:
audit2allow -i avc.log
️ 警告:工具生成的规则有时过于宽泛(比如直接给了 dac_override),千万不要无脑复制粘贴。一定要结合我们上面讲的逻辑,人工审核一遍,确保遵循“最小权限原则”。
2. 权限天梯:普通 App 与系统 App 的鸿沟
很多开发者会问:“为什么我的 App 获取不到某些系统权限?”这往往不是代码写错了,而是 seapp_contexts 定义的“出身”决定了你的上限。
下表直观展示了普通 App 与系统 App 在 SELinux 策略上的核心差异:
| 维度 | 普通 App (untrusted_app) | 系统 App (platform_app / priv_app) |
|---|---|---|
| 判定依据 | 第三方签名 / 无特殊签名 | platform 签名 (LOCAL_CERTIFICATE := platform) |
| 进程域 | untrusted_app | platform_app 或 priv_app |
| 数据目录 | untrusted_app_data_file | privapp_data_file |
| 硬件访问 | 几乎为零(必须通过系统服务中转) | 可直接访问部分硬件节点(需策略允许) |
| 典型场景 | 第三方应用、用户安装的应用 | 电话、短信、设置、系统桌面 |
核心结论:普通 App 被关在沙盒里,想碰硬件必须走 Binder 找系统服务;而系统 App 则是“特权阶级”,拥有更广阔的视野和操作空间。
3. 实战排错:当 setprop 失败时
假设你在代码里写了一句 SystemProperties.set("persist.vendor.myapp.state", "1"),结果应用崩溃了,Logcat 报出如下错误:
** 核心日志分析**
排错三部曲:
- 看目标标签:
tcontext=u:object_r:default_prop:s0。说明系统默认把你这个属性归类为了default_prop。 - 看当前权限:
default_prop通常只允许系统进程修改,普通 App(untrusted_app)是被禁止set的。 - 修改策略:
- 第一步:在
property_contexts中定义专属标签:
- 第一步:在
persist.vendor.myapp.state u:object_r:my_app_prop:s0
- **第二步**:在 `.te` 文件中赋予权限:
allow untrusted_app my_app_prop:property_service { set };
- **第三步**:重新编译刷机。
理解了这一点,你就明白为什么改了源码必须重新编译——因为内核只认那个编译好的二进制文件,它不读源码。
写在最后
配置 SELinux 其实就是一场“贴标签”的游戏。
刚开始接触时,你会觉得那些 type, domain, context 很绕。但当你习惯了这种思维方式——先定义身份,再定义权限——你会发现它其实是系统稳定性的最后一道防线。
它强迫我们把代码隔离好,不让一个不起眼的服务拥有操作整个系统的权力。下次再遇到 avc: denied,不妨心平气和地看看日志,给它补一张“通行证”就好。毕竟,安全这东西,麻烦点,总比被黑了强。