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 加载过程中的关键检查点

整个加载过程,内核会在几个关键点做权限检查。我按顺序列一下:

  1. 文件执行检查:Zygote执行system_server二进制文件时,检查file execute权限。
  2. 域转换检查:从zygote域切换到system_server域时,检查process transition权限。
  3. 文件读取检查:System Server读取配置文件(如/system/etc/selinux/下的策略文件)时,检查file read权限。
  4. Binder通信检查:System Server向ServiceManager注册服务时,检查binder call权限。
  5. 服务调用检查:其他进程调用系统服务时,检查binder transfer权限。

注意:第4点和第5点经常被忽略。很多人以为服务注册成功了就万事大吉,其实后续每次Binder调用,内核都会重新检查权限。如果策略没配好,服务虽然注册了,但别人调不了,等于白搭。

3.5 实战经验:如何调试SELinux上下文加载问题

最后分享一点实战经验。如果你遇到System Server启动时权限相关的问题,可以按这个步骤排查:

  1. 看loglogcat -b events | grep avc,所有SELinux拒绝事件都会打出来。
  2. 看dmesgdmesg | grep avc,内核层的拒绝信息在这里。
  3. 生成策略:用audit2allow工具把拒绝日志转换成策略规则。
  4. 验证策略:把生成的规则加到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通信的守门员。