Seccomp沙箱
本文面向已经读过 进程specialization、进程UID/GID设置 和 进程Capability设置 的读者。问题很具体:一个 Zygote 子进程何时安装系统调用过滤器,过滤器由哪些源码生成,child zygote 为什么还需要另一层 UID/GID 限制。本文不把每个架构生成出的数百条 syscall 展开成静态清单,而是教你沿着源码和构建输入定位它们。读完后,你应能从 forkAndSpecialize() 走到内核 PR_SET_SECCOMP,解释三种策略的选择条件,并判断一次 Seccomp: 2 观测究竟证明了什么。
1. 安全边界
Seccomp-BPF 把一次系统调用看成 (架构, syscall 编号, 参数),由内核执行 BPF 程序并返回允许、错误、陷阱或终止动作。Android 的 Zygote 路径使用 SECCOMP_MODE_FILTER;过滤器安装后由内核在每次 syscall 进入时执行,不是 Java 层的权限检查,也不替代 SELinux、UID/GID 或 capability。
在本文版本中有两类过滤器:
- 普通过滤器:由
set_app_seccomp_filter()、set_app_zygote_seccomp_filter()或set_system_seccomp_filter()安装,分别对应普通应用、App Zygote 和系统 UID 进程。 - 范围过滤器:child zygote 通过
install_setuidgid_seccomp_filter(min, max)追加,只检查setresuid/setresgid的三个 ID 参数是否落在指定范围。
过滤器的 owner 是当前进程的内核 seccomp 状态。Zygote 父进程不会替子进程保存一个可回滚的策略;fork 后 child 自己安装,失败就结束 child。消费者是内核 syscall 路径,因此 Java 代码不会在每次调用前重新查询策略。
图中的两条安全路径不是“二选一”:一个应用 syscall 必须同时满足 seccomp 动作、SELinux 域规则和 Linux 身份权限。seccomp 的生效时机在 child 创建线程、进入 ActivityThread.main() 或 native service 主循环之前。
2. 调用入口
源码文件:
frameworks/base/core/java/com/android/internal/os/Zygote.javaframeworks/base/core/java/com/android/internal/os/ZygoteConnection.java
ZygoteConnection 在需要 Java fork 路径时把解析后的参数交给 Zygote.forkAndSpecialize();Zygote 再进入 JNI。参数中的 uid、gid、gids 和 startChildZygote 决定 native 分支,但普通客户端并不能传入一个“任意 BPF 程序”。
// frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java
pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid,
parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits,
parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName,
fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote,
parsedArgs.mInstructionSet, parsedArgs.mAppDataDir,
parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList,
parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs,
parsedArgs.mBindMountAppStorageDirs,
parsedArgs.mBindMountSyspropOverrides);进入 Zygote 后,参数仍以同一组值传入 JNI;下面的实现片段展示 fork 前后的 Java 接缝。
// frameworks/base/core/java/com/android/internal/os/Zygote.java
static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags,
int[][] rlimits, int mountExternal, String seInfo, String niceName,
int[] fdsToClose, int[] fdsToIgnore, boolean startChildZygote,
String instructionSet, String appDataDir, boolean isTopApp,
String[] pkgDataInfoList, String[] allowlistedDataInfoList,
boolean bindMountAppDataDirs, boolean bindMountAppStorageDirs,
boolean bindMountSyspropOverrides) {
ZygoteHooks.preFork();
int pid = nativeForkAndSpecialize(uid, gid, gids, runtimeFlags, rlimits,
mountExternal, seInfo, niceName, fdsToClose, fdsToIgnore,
startChildZygote, instructionSet, appDataDir, isTopApp,
useFifoUi, pkgDataInfoList, allowlistedDataInfoList,
bindMountAppDataDirs, bindMountAppStorageDirs,
bindMountSyspropOverrides);
if (pid == 0) {
NetworkUtilsInternal.setAllowNetworkingForProcess(containsInetGid(gids));
}
ZygoteHooks.postForkCommon();
return pid;
}这里 pid == 0 只表示当前执行流已经在 child;真正的 seccomp 安装发生在 JNI 的 SpecializeCommon(),所以 postForkCommon() 和网络状态同步都在过滤器安装之后。父端只收到 pid 或异常,不拥有 child 的 seccomp 状态。
3. 特化时机
源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
static void SetUpSeccompFilter(uid_t uid, bool is_child_zygote) {
if (!gIsSecurityEnforced) {
ALOGI("seccomp disabled by setenforce 0");
return;
}
// Apply system or app filter based on uid.
if (uid >= AID_APP_START) {
if (is_child_zygote) {
set_app_zygote_seccomp_filter();
} else {
set_app_seccomp_filter();
}
} else {
set_system_seccomp_filter();
}
}gIsSecurityEnforced 在 Zygote 尚未 fork 前由 security_getenforce() 缓存;应用 child 不能再调用该接口查询。这里的 UID 判断只负责选择策略类别:uid >= AID_APP_START 再区分 child zygote,其余 UID 进入 system filter。函数返回值没有被检查,bionic 的 install_filter() 对 prctl() 失败使用 PLOG(FATAL),因此失败不会回到 Java 继续执行。
// frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
if (setresgid(gid, gid, gid) == -1) {
fail_fn(CREATE_ERROR("setresgid(%d) failed: %s", gid, strerror(errno)));
}
// The child still has CAP_SYS_ADMIN here; changing uid would clear it.
SetUpSeccompFilter(uid, is_child_zygote);
SetSchedulerPolicy(fail_fn, is_top_app);
if (setresuid(uid, uid, uid) == -1) {
fail_fn(CREATE_ERROR("setresuid(%d) failed: %s", uid, strerror(errno)));
}setresgid() 后、setresuid() 前是关键窗口。源码注释给出的原因是安装过滤器需要 child 仍具备 CAP_SYS_ADMIN;改成先设置 PR_SET_NO_NEW_PRIVS 会破坏后面的 SELinux domain transition。安装动作完成后,调度策略仍要在失去调度权限前设置,最后才切换 UID。任何 fail_fn 都走 ZygoteFailure 的致命 child 路径,不能留下一个只完成了一半特化的进程。
第一阶段仍在 Zygote child 的特权窗口内准备身份;第二阶段把 BPF 交给内核并形成不可由 Java 撤销的进程状态;第三阶段才丢失 UID 权限并完成剩余 runtime/SELinux 初始化。图中的 fatal error 不返回 SpecializeCommon() 正常路径。
4. 策略生成
源码文件:
bionic/libc/seccomp/seccomp_policy.cppbionic/libc/seccomp/include/seccomp_policy.hbionic/libc/Android.bp
Java/JNI 只调用三个 C 函数;真正的 BPF 数组是构建期由 genseccomp 生成的架构文件。seccomp_policy.cpp 先校验 AUDIT_ARCH_*,再把 primary/secondary 架构的 syscall 表拼接到一个 sock_fprog,最后调用 prctl。
// bionic/libc/seccomp/seccomp_policy.cpp
static bool install_filter(filter const& f) {
struct sock_fprog prog = {
static_cast<unsigned short>(f.size()),
const_cast<struct sock_filter*>(&f[0]),
};
// The caller must have CAP_SYS_ADMIN or PR_SET_NO_NEW_PRIVS.
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) < 0) {
PLOG(FATAL) << "Could not set seccomp filter of size " << f.size();
return false;
}
return true;
}
bool _set_seccomp_filter(FilterType type) {
filter f;
// Select the generated primary/secondary policy for APP,
// APP_ZYGOTE, or SYSTEM, then validate architecture and append it.
...
ExamineSyscall(f);
for (size_t i = 0; i < p_size; ++i) f.push_back(p[i]);
Disallow(f);
...
return install_filter(f);
}省略号对应的是架构选择和 secondary policy 拼接,不代表运行时读取文本文件。头文件只暴露四个稳定入口:
// bionic/libc/seccomp/include/seccomp_policy.h
bool set_app_seccomp_filter();
bool set_app_zygote_seccomp_filter();
bool set_system_seccomp_filter();
bool install_setuidgid_seccomp_filter(uint32_t uid_gid_min,
uint32_t uid_gid_max);构建输入在 bionic/libc/Android.bp 中分组:普通 app 使用 SYSCALLS.TXT、common/app allowlist、common/app blocklist;system 使用 common/system allowlist 和 common blocklist;app zygote 则由 SECCOMP_BLOCKLIST_APP.TXT 生成一个移除 setresgid* 条目的派生 blocklist。三种策略的差异来自这些输入和架构 syscall 编号,不应从 archive 文章中的“严格程度表”反推某个版本的固定 syscall 集合。
这条构建链解释了两个容易混淆的事实:第一,SECCOMP_ALLOWLIST_SYSTEM.TXT 在当前源码中只有注释,没有额外条目,system 策略仍由 SYSCALLS.TXT 与 common blocklist 生成;第二,APP_ZYGOTE 并不是“app 策略加一张运行时表”,而是独立生成的架构 BPF 数组。
5. App Zygote
源码文件:frameworks/base/core/java/com/android/internal/os/ChildZygoteInit.java
App Zygote 需要继续 fork isolated child,但不能把任意 UID/GID 交给它的子进程。因此它在进入自己的 socket loop 前先校验范围、设置 NO_NEW_PRIVS,再安装第二个过滤器。
// frameworks/base/core/java/com/android/internal/os/ChildZygoteInit.java
Os.prctl(OsConstants.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
int uidGidMin = parseIntFromArg(args, Zygote.CHILD_ZYGOTE_UID_RANGE_START);
int uidGidMax = parseIntFromArg(args, Zygote.CHILD_ZYGOTE_UID_RANGE_END);
if (uidGidMin == -1 || uidGidMax == -1) {
throw new RuntimeException("Couldn't parse UID range start/end");
}
if (uidGidMin > uidGidMax) {
throw new RuntimeException("Passed in UID range is invalid, min > max.");
}
if (uidGidMin < Process.FIRST_APP_ZYGOTE_ISOLATED_UID) {
throw new RuntimeException("Passed in UID range does not map to isolated processes.");
}
Zygote.nativeInstallSeccompUidGidFilter(uidGidMin, uidGidMax);
server.registerServerSocketAtAbstractName(socketName);PR_SET_NO_NEW_PRIVS 让后续安装范围过滤器满足内核的无特权条件;普通 specialization 的主过滤器仍在 root/CAP_SYS_ADMIN 窗口中安装。范围过滤器并不允许 setresuid/setresgid,它只在已有 allowlist 允许这些 syscall 的前提下,把参数约束到 [min, max]。
// bionic/libc/seccomp/seccomp_policy.cpp
static void ValidateSetUidGid(filter& f, uint32_t min, uint32_t max,
bool primary) {
ExamineSyscall(f);
f.push_back(BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K,
setresuid_nr, 0, 12));
for (int arg = 0; arg < 3; arg++) {
ValidateSyscallArgInRange(f, arg, min, max);
}
ExamineSyscall(f);
f.push_back(BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K,
setresgid_nr, 0, 12));
for (int arg = 0; arg < 3; arg++) {
ValidateSyscallArgInRange(f, arg, min, max);
}
// Non-matching syscalls remain available to the regular policy.
Allow(f);
}这里的 owner 是 child zygote 的 seccomp filter stack;消费者仍是内核。ValidateSyscallArgInRange() 对每个参数先判断 >= min,再判断 < max + 1,越界就执行 Disallow()。因为范围过滤器默认 Allow,它不会绕过先前的 app-zygote allowlist;两层过滤器叠加时,任一层拒绝都使调用失败或触发陷阱。
6. 失败边界
失败处理要区分“跳过”“拒绝”和“进程终止”:
| 条件 | 代码路径 | 结果 |
|---|---|---|
| SELinux permissive | gIsSecurityEnforced == false | 记录日志并跳过主过滤器;不能把该设备观测外推为 enforcing 行为 |
prctl(PR_SET_SECCOMP) 失败 | install_filter() | PLOG(FATAL),child 终止,不进入 Java/native 主循环 |
| child zygote 范围缺失或反向 | ChildZygoteInit | 抛出 RuntimeException,尚未注册 server socket |
范围低于 FIRST_APP_ZYGOTE_ISOLATED_UID | ChildZygoteInit | 拒绝启动,避免把 system UID 纳入范围 |
| syscall 不在生成的 allowlist | 内核 BPF 末尾 Disallow | 由策略动作拒绝;不会回退到 Java 权限检查 |
setresuid/gid 参数越界 | 范围过滤器 ValidateSyscallArgInRange | 该 syscall 触发 Disallow,其他 syscall 仍由主过滤器决定 |
主过滤器的安装函数返回 bool,但底层失败路径是 fatal;范围过滤器在 JNI 包装中显式检查返回值:
// frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
bool installed = install_setuidgid_seccomp_filter(uidGidMin, uidGidMax);
if (!installed) {
RuntimeAbort(env, __LINE__, "Could not install setuid/setgid seccomp filter.");
}因此“过滤器未安装”不是一个可供上层重试的普通返回状态。恢复动作只能由外部进程重新启动失败的 zygote/app;不能在同一个 child 中卸载或放宽已安装的 seccomp filter。
7. 反向验证
源码测试:frameworks/base/libs/native_activity_thread/tests/src/android/nativeservice/test/NativeServiceProcessTest.java 先绑定一个 native service,读取其 /proc/<pid>/attr/current 和 /proc/<pid>/status。测试断言 isolated service 的 SELinux type 为 isolated_app、CapEff 为零、Seccomp 为 2。这组输入覆盖的是一个真实 isolated native service 的最终进程状态;它证明过滤器已经进入 Linux seccomp filter mode,也证明 capability 被清空,但没有证明每一条 syscall 都可用或所有 app UID 都走同一策略。
源码文件:frameworks/base/libs/native_activity_thread/tests/src/android/nativeservice/test/NativeServiceProcessTest.java
// frameworks/base/libs/native_activity_thread/tests/src/android/nativeservice/test/NativeServiceProcessTest.java
String statusOutput = SystemUtil.runShellCommand("cat /proc/" + pid + "/status");
ProcStatusInfo info = parseProcStatus(statusOutput);
assertTrue("CapEff should be zero",
"0000000000000000".equals(info.capEff) || "0".equals(info.capEff));
assertTrue("Seccomp filter should be applied", "2".equals(info.seccomp));设备上的只读诊断可以把“安装了吗”和“哪个进程”分开验证:
# 输入:目标设备上的进程名;输出:PID、UID、Seccomp mode 和有效 capability。
pid=$(adb shell pidof com.example.app | tr -d '\r')
adb shell "grep -E '^(Name|Pid|Uid|CapEff|Seccomp):' /proc/$pid/status"
# 输入:当前源码树;输出:策略选择、安装点和构建输入的命中位置。
rg -n "SetUpSeccompFilter|PR_SET_SECCOMP|install_setuidgid_seccomp_filter" \
frameworks/base/core/jni/com_android_internal_os_Zygote.cpp \
bionic/libc/seccomp/seccomp_policy.cpp
rg -n "SECCOMP_ALLOWLIST|SECCOMP_BLOCKLIST|genseccomp|app_zygote" \
bionic/libc/Android.bp bionic/libc/SECCOMP_*.TXT若 /proc/<pid>/status 显示 Seccomp: 0,先检查进程是否真的由 Zygote specialization 创建、设备是否 permissive,以及读取的 PID 是否在命令执行期间已经退出。若显示 2,还要结合 UID、SELinux context 和 CapEff 判断是哪一类进程;单独一个数字不能推出“应用拥有完整 app allowlist”。
理解这篇文章的一个有效检验是:从 ChildZygoteInit.runZygoteServer() 复述到 ValidateSetUidGid(),指出哪一步限制范围、哪一步安装到内核,以及为什么一个越界 setresuid() 不能被 Java 层捕获后继续使用。另一个检验是修改某个 SECCOMP_BLOCKLIST_* 输入后,能否说出需要重新生成哪一组架构 policy,以及运行中的进程为什么不会自动获得新策略。
