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策略之后。具体流程是这样的:

  1. 内核启动init进程,此时上下文为u:r:kernel:s0
  2. init进程挂载必要的文件系统(tmpfs、devpts等)
  3. init进程加载SELinux策略文件(/sepolicy
  4. init进程执行selinux_init_all()完成初始化
  5. 通过exec()系统调用重新执行自身,触发域转换
  6. 此时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_transitionallow规则没写对。我一般会先检查kernel.te里有没有定义type_transition kernel init_process init:process init;,再检查init.te里有没有对应的allow规则。

小技巧:如果不想每次都翻日志,可以临时把SELinux设为permissive模式:

setenforce 0

但注意,这只是调试手段。生产环境一定要切回enforcing模式。

8.7 知识体系总览

为了让你更直观地理解整个流程,我画了一张图:

early-init阶段SELinux域转换流程 kernel域 u:r:kernel:s0 加载策略 加载SELinux策略 /sepolicy selinux_init_all() init域 u:r:init:s0 关键SELinux规则 type_transition kernel init_process init:process init; allow kernel init:process transition; allow init init:process dyntransition; 常见问题与排查 1. 域转换失败 → 检查type_transition规则是否存在 2. 权限被拒 → 检查allow规则是否完整 3. 子进程无法回收 → 检查sigchld权限 4. 调试命令:ps -Z | grep init 和 logcat -b events | grep "avc: denied"

这张图把整个流程串起来了。从kernel域开始,经过策略加载,最终转换到init域。每个环节都有对应的SELinux规则在背后支撑。

好了,关于early-init阶段的域转换和init策略编写,就聊这么多。记住一点:域转换不是自动的,它需要明确的策略规则来驱动。写策略时,多想想「谁允许谁做什么」,思路就清晰了。


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