3、System Server的权限启动:系统服务进程的SELinux上下文加载过程
好,我们接着往下聊。上一节我们把init进程的SELinux初始化讲完了,现在轮到System Server登场了。这个进程在Android里有多重要?这么说吧,它挂了,手机就重启了。所以它的权限控制,必须做到滴水不漏。
我个人习惯把System Server的启动分成三个阶段来看:预加载、上下文切换、服务注册。每个阶段都有对应的SELinux策略在起作用。咱们一个一个拆开讲。
3.1 预加载阶段:谁有资格启动System Server?
System Server是谁拉起来的?是Zygote进程。但Zygote不能随便fork一个进程就叫System Server,得有「许可证」。
这个许可证,就是SELinux策略里的一条规则。我截取一段典型的策略文件给你看:
# system_server.te
type system_server, domain;
type system_server_exec, exec_type, file_type;
init_daemon_domain(system_server)
allow zygote system_server:process transition;
allow system_server self:capability { sys_boot sys_nice };
注意看第一行:init_daemon_domain(system_server)。这个宏干了什么事?它告诉内核:当Zygote要执行system_server这个可执行文件时,允许发生域转换——从zygote域跳到system_server域。
核心要点:没有这条规则,Zygote就算把system_server的二进制文件加载进内存,也跑不起来。内核会直接拒绝执行,返回一个SIGKILL。
我在项目中遇到过一个问题:某厂商定制了一个系统服务,想放在System Server里一起启动。结果一开机就crash,查了半天,就是忘了加init_daemon_domain。嗯,这种坑踩过一次就记住了。
3.2 上下文切换:从Zygote域到System Server域
好,现在Zygote拿到了许可,开始fork子进程。但这时候子进程的SELinux上下文还是u:r:zygote:s0。它得切换到u:r:system_server:s0才行。
这个切换发生在哪里?在app_main.cpp里。我简化一下关键代码:
// frameworks/base/cmds/app_process/app_main.cpp
int main(int argc, char* const argv[]) {
// ... 省略初始化 ...
if (strcmp(arg, "--zygote") == 0) {
// Zygote模式
} else if (strcmp(arg, "--system-server") == 0) {
// 关键:设置SELinux上下文
selinux_android_setcontext(
SELINUX_DOMAIN_SYSTEM_SERVER,
NULL,
"system_server"
);
}
}
这个selinux_android_setcontext函数,说白了就是告诉内核:我要切换域了,你检查一下策略文件,看看允不允许。
内核会做什么?它会查avc_has_perm,检查当前进程(zygote)是否有权限执行process transition到目标域(system_server)。如果策略允许,就切换成功;否则,进程直接挂掉。
避坑指南:我曾经遇到一个诡异的问题——System Server启动后,某些文件访问总是被拒绝。后来发现是selinux_android_setcontext调用时,第二个参数传错了。那个参数是「进程名称」,如果传了NULL,内核会用默认的进程名去匹配策略,很容易出问题。所以我的建议是:显式传"system_server"字符串,别偷懒。
3.3 服务注册:每个系统服务的权限隔离
System Server启动后,会注册一大堆系统服务。比如AMS(ActivityManagerService)、WMS(WindowManagerService)、PMS(PackageManagerService)等等。这些服务虽然都跑在System Server进程里,但它们的权限是隔离的。
怎么隔离的?靠的是SELinux的上下文标签。每个服务在注册时,都会带上自己的SELinux上下文。举个例子:
// ServiceManager注册服务
status_t sm = defaultServiceManager();
sm->addService("activity", new ActivityManagerService(),
/* allowIsolated */ false,
/* dumpPriority */ DUMP_FLAG_PRIORITY_CRITICAL,
/* sid */ "u:r:activity_service:s0");
注意最后一个参数sid,它指定了这个服务的SELinux上下文是u:r:activity_service:s0。其他进程要调用这个服务,必须拥有对应的权限。
我整理了一张表,列出几个关键服务的上下文:
| 服务名称 | SELinux上下文 | 主要权限 |
|---|---|---|
| ActivityManagerService | u:r:activity_service:s0 | 管理Activity栈、进程生命周期 |
| WindowManagerService | u:r:window_service:s0 | 窗口管理、输入事件分发 |
| PackageManagerService | u:r:package_service:s0 | 安装/卸载应用、权限管理 |
| PowerManagerService | u:r:power_service:s0 | 电源管理、唤醒锁 |
你想想看,如果AMS和WMS用的是同一个上下文,那一个服务被攻破,另一个也跟着完蛋。所以这种隔离设计,其实是把「最小权限原则」落到了实处。
3.4 加载过程中的关键检查点
整个加载过程,内核会在几个关键点做权限检查。我按顺序列一下:
- 文件执行检查:Zygote执行system_server二进制文件时,检查
file execute权限。 - 域转换检查:从zygote域切换到system_server域时,检查
process transition权限。 - 文件读取检查:System Server读取配置文件(如
/system/etc/selinux/下的策略文件)时,检查file read权限。 - Binder通信检查:System Server向ServiceManager注册服务时,检查
binder call权限。 - 服务调用检查:其他进程调用系统服务时,检查
binder transfer权限。
注意:第4点和第5点经常被忽略。很多人以为服务注册成功了就万事大吉,其实后续每次Binder调用,内核都会重新检查权限。如果策略没配好,服务虽然注册了,但别人调不了,等于白搭。
3.5 实战经验:如何调试SELinux上下文加载问题
最后分享一点实战经验。如果你遇到System Server启动时权限相关的问题,可以按这个步骤排查:
- 看log:
logcat -b events | grep avc,所有SELinux拒绝事件都会打出来。 - 看dmesg:
dmesg | grep avc,内核层的拒绝信息在这里。 - 生成策略:用
audit2allow工具把拒绝日志转换成策略规则。 - 验证策略:把生成的规则加到
system_server.te里,重新编译boot image,刷机验证。
我记得有一次,客户反馈System Server启动后一直报permission denied。我一看log,发现是system_server域没有权限读取/data/system目录下的某个文件。用audit2allow生成了一条allow system_server system_data_file:file read;规则,加进去就好了。问题不大,但如果不熟悉这个流程,可能会卡很久。
好,关于System Server的SELinux上下文加载过程,就讲到这里。下一节我们会聊Service Manager的权限控制——那个更复杂,因为它是所有Binder通信的守门员。