2、Zygote进程的权限初始化:Zygote如何继承和设置初始权限上下文

好,我们接着聊Zygote。上一章我们讲了Zygote是怎么启动的,这一章我们聚焦一个更核心的问题——权限

Zygote是所有Java进程的父进程,说白了,它就像一个「权限分发中心」。它自己是什么权限?它怎么给子进程设置权限?这些搞不清楚,后面分析App启动、沙箱隔离都会一头雾水。

我个人习惯把Zygote的权限初始化分成三个阶段来看:继承阶段、固化阶段、分发阶段。咱们一个一个说。

2.1 Zygote的权限继承:从init进程来的「根」

Zygote是由init进程通过解析init.rc文件启动的。init进程本身是root权限,但Zygote启动时,init.rc里会明确指定它的用户和组。

我们来看一段典型的init.rc配置:

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    class main
    priority -20
    user root
    group root readproc
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

注意看这两行:

  • user root —— Zygote以root用户运行
  • group root readproc —— 属于root组和readproc组

嗯,这里要注意:Zygote虽然是root,但它不是「为所欲为」的root。它只是继承了init进程的root身份,但实际能力受到SELinux策略和Capabilities的严格限制。

关键点:Zygote的root权限是「名义上的root」,它的真正权限由SELinux上下文和Capabilities共同决定。这一点我当年刚接触时也困惑过——为什么root还能被限制?后来才明白,Android的安全模型早就不是传统的Unix权限模型了。

2.2 权限上下文的设置:SELinux与Capabilities

Zygote启动后,会立即进行权限上下文的设置。这个过程发生在app_main.cppmain()函数中,具体在AndroidRuntime::start()之前。

我给大家梳理一下流程:

  1. 设置SELinux上下文:Zygote会将自己设置为u:r:zygote:s0这个安全上下文
  2. 设置Capabilities:通过prctl()系统调用设置进程的Capabilities
  3. 设置groups:添加额外的补充组ID

代码层面,核心实现在selinux_android_setcontext()这个函数里。我简化一下关键逻辑:

// 伪代码,展示核心逻辑
static int selinux_android_setcontext(int uid, int isSystemServer, const char* seinfo) {
    char *ctx = NULL;
    
    // 1. 根据进程类型构造SELinux上下文
    if (isSystemServer) {
        ctx = strdup("u:r:system_server:s0");
    } else {
        // Zygote自身
        ctx = strdup("u:r:zygote:s0");
    }
    
    // 2. 设置安全上下文
    if (setcon(ctx) < 0) {
        // 设置失败,直接abort
        // 我遇到过因为SELinux策略没加载导致这里崩溃的情况
        return -1;
    }
    
    // 3. 设置Capabilities
    // Zygote需要保留的能力:CAP_SETUID, CAP_SETGID, CAP_SETPCAP等
    // 用于后续创建子进程时设置权限
    struct __user_cap_header_struct cap_header;
    struct __user_cap_data_struct cap_data[2];
    
    cap_header.version = _LINUX_CAPABILITY_VERSION_3;
    cap_header.pid = 0;
    
    // 只保留必要的capability
    cap_data[0].effective = (1 << CAP_SETUID) | (1 << CAP_SETGID) | ...;
    cap_data[0].permitted = cap_data[0].effective;
    cap_data[0].inheritable = 0;
    
    capset(&cap_header, cap_data);
    
    return 0;
}

避坑指南:我曾经在移植Android到一款新设备时,遇到Zygote启动后无法fork子进程的问题。查了两天才发现是SELinux策略中zygote域的setcontext权限没开。记住,Zygote的SELinux上下文设置必须在fork任何子进程之前完成,否则子进程会继承错误的上下文。

2.3 Zygote的Capabilities策略

Zygote的Capabilities设置非常讲究。它不能太多,否则子进程可能继承过多权限;也不能太少,否则无法完成子进程的权限设置。

我整理了一张表,列出Zygote保留的关键Capabilities:

Capability 作用 为什么需要
CAP_SETUID 设置用户ID 创建App进程时切换到对应UID
CAP_SETGID 设置组ID 创建App进程时切换到对应GID
CAP_SETPCAP 修改进程的Capabilities 为子进程设置更严格的Capabilities
CAP_NET_RAW 原始套接字 某些系统服务需要(如netd)
CAP_SYS_PTRACE 跟踪其他进程 调试和监控需要

你想想看,如果Zygote保留了CAP_SYS_ADMIN这种大权限,那它fork出来的每个App进程理论上都能继承这个权限,这还得了?所以Zygote的Capabilities策略是:只保留创建子进程所必需的最小权限集

2.4 权限上下文的传递:fork时的继承机制

当Zygote fork出子进程时,子进程会继承父进程的:

  • 用户ID和组ID(初始相同)
  • Capabilities(包括effective、permitted、inheritable集合)
  • SELinux上下文(初始相同)
  • 补充组列表

但注意,这只是初始状态。Zygote在fork之后、真正执行业务代码之前,会立即调用ZygoteConnection.handleChildProc()来调整子进程的权限:

// ZygoteConnection.java 中的核心逻辑
private void handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors,
        FileDescriptor pipeFd, boolean isZygote) {
    
    // 1. 关闭Zygote socket(子进程不需要监听)
    closeZygoteSocket(parsedArgs.abiList);
    
    // 2. 设置进程的UID和GID
    // 这里会调用native层函数设置
    setProcessGroup(parsedArgs.uid, parsedArgs.gid);
    
    // 3. 设置补充组
    // 比如:everyone、inet等
    setSupplementaryGroups(parsedArgs.gids);
    
    // 4. 设置SELinux上下文
    // 根据seinfo信息设置对应的安全上下文
    // 比如App进程会设置为 u:r:untrusted_app:s0
    setSELinuxContext(parsedArgs.seinfo, parsedArgs.uid);
    
    // 5. 设置Capabilities
    // 清除子进程不需要的Capabilities
    // 只保留App进程真正需要的
    setCapabilities(parsedArgs.permittedCapabilities, parsedArgs.effectiveCapabilities);
    
    // 6. 设置调度策略和优先级
    // ...
}

个人经验:我在调试一个App权限异常问题时,发现App的SELinux上下文被错误地设置成了u:r:system_app:s0。查到最后,是seinfo参数在传递过程中被篡改了。所以,Zygote在设置子进程上下文时,一定要对传入的参数做校验,不能盲目信任。

2.5 一个完整的权限初始化时序图

为了让大家更直观地理解,我画一个时序流程:

init进程 (root)
    │
    ├── 解析init.rc,启动Zygote
    │   user=root, group=root
    │
    ▼
Zygote进程启动
    │
    ├── 1. 设置SELinux上下文 → u:r:zygote:s0
    ├── 2. 设置Capabilities → 保留最小权限集
    ├── 3. 设置补充组 → readproc等
    │
    ▼
Zygote进入等待状态
    │
    ├── 收到创建App请求
    │
    ▼
fork() 子进程
    │
    ├── 子进程继承父进程权限
    │   (UID=root, SELinux=zygote, Caps=继承)
    │
    ▼
子进程权限调整
    │
    ├── 1. setuid → 切换到App的UID (例如 u0_a123)
    ├── 2. setgid → 切换到App的GID
    ├── 3. setcon → 切换到 u:r:untrusted_app:s0
    ├── 4. capset → 清除大部分Capabilities
    │
    ▼
App进程启动 (权限已隔离)

说白了,Zygote就像一个「权限转换器」。它自己持有较高的权限,但每次创建子进程时,都会把子进程的权限降到最低。这种设计保证了Android系统的安全性——任何App进程都无法获得超出其应有的权限

2.6 常见问题与排查思路

我在实际工作中,遇到过不少Zygote权限相关的问题。这里分享几个典型场景:

  • 场景一:Zygote启动后无法fork
    检查dmesglogcat中是否有SELinux avc denial日志。通常是zygote域的fork权限没开。

  • 场景二:App进程权限过高
    检查Zygote的Capabilities设置是否正确。我曾经见过因为capset调用失败,导致App继承了Zygote的CAP_NET_RAW权限。

  • 场景三:App无法访问某些资源
    检查SELinux上下文是否正确。用ps -Z查看进程的安全上下文,确认是否设置成了预期的值。

总结一下:Zygote的权限初始化,核心就三件事——继承init的root身份、固化自己的SELinux和Capabilities、在fork后精确降权。理解了这个流程,你就能明白Android是如何通过Zygote实现进程权限隔离的。

下一章,我们会深入分析Zygote fork子进程时的具体权限调整细节,包括UID/GID的切换时机、SELinux上下文的动态计算等。到时候再聊!