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.cpp的main()函数中,具体在AndroidRuntime::start()之前。
我给大家梳理一下流程:
- 设置SELinux上下文:Zygote会将自己设置为
u:r:zygote:s0这个安全上下文 - 设置Capabilities:通过
prctl()系统调用设置进程的Capabilities - 设置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
检查dmesg或logcat中是否有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上下文的动态计算等。到时候再聊!