8、启动阶段SELinux策略定制:early-init阶段的SELinux域转换,init进程的SELinux策略编写
好,我们直接进入正题。Android系统启动,说白了就是一场「权限大迁徙」。从内核态到用户态,从临时文件系统到真正的根文件系统,每个进程的「身份」都在不断变化。而SELinux,就是这场迁徙中的安检员。
今天重点聊两个事:early-init阶段的域转换,以及init进程自身的SELinux策略。这两个点,我在项目里踩过不少坑,今天一并倒出来。
8.1 early-init阶段:谁在裸奔?
系统刚启动时,init进程跑在kernel域里。嗯,你没看错——就是和内核一个域。这其实挺危险的,因为此时SELinux还没完全初始化,策略文件都没加载呢。
我刚开始做启动优化时,就遇到过一个问题:early-init阶段某个服务想写日志,结果被SELinux无情拒绝。查了半天才发现,它还在kernel域里裸奔,根本没有写日志的权限。
核心要点:early-init阶段,init进程的SELinux上下文是u:r:kernel:s0。直到策略文件加载完成,才会切换到u:r:init:s0。
为什么会这样?因为SELinux策略文件本身存储在/sepolicy或/system/etc/selinux/下,加载策略需要文件系统访问权限。这就成了一个「先有鸡还是先有蛋」的问题。
8.2 域转换:从kernel到init
域转换发生在init进程加载完SELinux策略之后。具体流程是这样的:
- 内核启动init进程,此时上下文为
u:r:kernel:s0 - init进程挂载必要的文件系统(tmpfs、devpts等)
- init进程加载SELinux策略文件(
/sepolicy) - init进程执行
selinux_init_all()完成初始化 - 通过
exec()系统调用重新执行自身,触发域转换 - 此时init进程的上下文变为
u:r:init:s0
这里有个细节:域转换不是自动发生的,它依赖于SELinux策略中的type_transition规则。我习惯在编写策略时,先确认这条规则是否存在。
# 在kernel.te或init.te中定义
type_transition kernel init_process init:process init;
# 还需要allow规则配合
allow kernel init:process transition;
allow init init:process dyntransition;
你想想看,如果没有这条type_transition,init进程会一直留在kernel域里。那后续启动的所有服务,都会继承这个高危域——后果不堪设想。
8.3 init进程的SELinux策略编写
init进程的SELinux策略,是整个Android系统策略的基石。它定义了init进程能做什么、不能做什么,以及如何管理子进程的域转换。
我个人习惯把init策略分成三块来写:
| 策略模块 | 职责 | 关键权限 |
|---|---|---|
| init.te | init进程自身的行为 | 文件读写、socket创建、属性设置 |
| init_rc.te | 解析和执行init.rc脚本 | 文件执行、服务启动、域转换 |
| init_shell.te | init启动的shell进程 | 命令执行、临时文件操作 |
我曾经在某个项目中,因为init.te里漏写了一条allow init self:capability sys_admin,导致init进程无法挂载某些文件系统。系统卡在启动动画上,死活进不去桌面。排查了整整两天才找到原因——嗯,从那以后我再也不敢小看capability权限了。
8.4 实战:编写init域转换策略
假设我们要让init进程启动一个名为my_service的服务,并且这个服务要运行在my_app域里。策略怎么写?
# 1. 定义域
type my_app, domain;
type my_app_exec, exec_type, file_type;
# 2. 定义域转换规则
type_transition init my_app_exec:process my_app;
# 3. 允许init执行该文件
allow init my_app_exec:file { read getattr execute };
# 4. 允许init启动my_app域进程
allow init my_app:process transition;
allow my_app init:process sigchld;
# 5. 允许my_app域访问必要资源
allow my_app self:capability { chown dac_override };
allow my_app my_app_exec:file { read execute };
这里有个容易踩的坑:type_transition规则只定义了「当init执行my_app_exec类型的文件时,应该转换到my_app域」。但如果没有对应的allow规则,转换会被SELinux拒绝。
注意:域转换需要同时满足三个条件:
- 源域(init)有权限执行目标文件
- 源域有权限转换到目标域
- 目标域有权限从源域接收信号(sigchld)
缺一不可。我曾经因为漏了sigchld权限,导致init无法回收子进程,系统内存越吃越多。
8.5 early-init阶段的特殊处理
early-init阶段,文件系统还没完全就绪。这时候init进程需要做一些特殊操作,比如:
- 挂载
/proc、/sys等虚拟文件系统 - 创建设备节点(
/dev/null、/dev/console等) - 设置内核参数(通过
/proc/sys)
这些操作在SELinux策略里需要特别授权。我习惯在init.te里单独写一段early-init的权限:
# early-init阶段特殊权限
allow init proc:filesystem mount;
allow init sysfs:filesystem mount;
allow init devtmpfs:filesystem mount;
# 创建设备节点
allow init device:dir { create add_name write };
allow init device:chr_file { create setattr };
# 设置内核参数
allow init proc_sys:file { write setattr };
说白了,early-init阶段的策略要「宽」一点。因为这时候系统还没完全建立起来,很多安全边界还没形成。等系统进入late-init阶段,再逐步收紧权限。
8.6 调试技巧:怎么看域转换是否成功?
我调试SELinux策略时,最常用的命令就两个:
# 查看当前进程的SELinux上下文
ps -Z | grep init
# 查看域转换是否被拒绝
logcat -b events | grep "avc: denied"
如果域转换失败,你会看到类似这样的日志:
avc: denied { transition } for pid=1 comm="init" scontext=u:r:kernel:s0 tcontext=u:r:init:s0 tclass=process
看到这条日志,基本可以确定是type_transition或allow规则没写对。我一般会先检查kernel.te里有没有定义type_transition kernel init_process init:process init;,再检查init.te里有没有对应的allow规则。
小技巧:如果不想每次都翻日志,可以临时把SELinux设为permissive模式:
setenforce 0
但注意,这只是调试手段。生产环境一定要切回enforcing模式。
8.7 知识体系总览
为了让你更直观地理解整个流程,我画了一张图:
这张图把整个流程串起来了。从kernel域开始,经过策略加载,最终转换到init域。每个环节都有对应的SELinux规则在背后支撑。
好了,关于early-init阶段的域转换和init策略编写,就聊这么多。记住一点:域转换不是自动的,它需要明确的策略规则来驱动。写策略时,多想想「谁允许谁做什么」,思路就清晰了。
公众号:蓝海资料掘金营,微信deep3321