title: Linux seccomp-BPF 安全沙箱与系统调用隔离深度实战:从 Chrome 沙箱到容器运行时安全加固
category: 系统编程
keywords: Linux, seccomp, seccomp-BPF, 系统调用, 沙箱, 容器安全, Landlock, Docker, Chrome Sandbox, Namespaces, Capabilities, SECCOMP_RET, seccomp_unotify, libseccomp
description: 深度解析 Linux seccomp-BPF 沙箱机制:BPF 过滤器架构演进(strict→filter→unotify)、SECCOMP_RET 五大返回值行为语义、多级调度器架构(KILL/ERRNO/TRACE/ALLOW/LOG)、跨架构多 syscall 处理、libseccomp/自制 BPF 生成器对比生产使用、Docker/containerd 容器运行时安全配置、Chrome 分级沙箱实现、systemd 单元保护、用户态通知(seccomp unotify/seccomp_notify_fd)与 Landlock LSM 联合使用、系统调用拦截性能开销评估与最佳实践。
Linux seccomp-BPF 安全沙箱与系统调用隔离深度实战
1. 概述:现代沙箱的基础层
在云原生时代,容器逃逸、漏洞利用链已常态化。seccomp-BPF(Secure Computing with Berkeley Packet Filter)是 Linux 内核中最精细的系统调用过滤机制,为 Docker、containerd、Chrome、Firejail、systemd、Flatpak 等重量级项目提供底层安全保障。
与Capabilities(按能力维度隔离)和Namespaces(按资源视图隔离)不同,seccomp-BPF 直接以系统调用为粒度进行拦截和过滤,是纵深防御中最后也是最内层的一道防线。
本文从内核实现到生产部署,深入解析 seccomp-BPF 的架构设计、使用方式与最佳实践。
2. 历史演进与设计哲学
2.1 三代 safe computing 模式
| 版本 | 引入版本 | 模式 | 行为 |
|---|---|---|---|
| seccomp 1 | Linux 2.6.12 (2005) | STRICT | 仅允许 read/write/exit/sigreturn |
| seccomp 2 | Linux 3.5 (2012) | FILTER (BPF) | 通过 BPF 自定义过滤规则 |
| seccomp unotify | Linux 5.0 (2019) | USER_NOTIF | 用户态处理不可拦截的 syscall |
STRICT 模式极为有限,仅 4 个 syscall 可用。实际广泛使用的是 FILTER 模式配合 BPF 程序。
2.2 为什么用 BPF 而不是白名单枚举?
传统白名单:"I/O 相关允许,网络相关拦截"——太粗糙。
BPF 优势:
- 参数级过滤:只拦截
write()但仅限制目标 fd - 架构感知:同一进程的 x86_64 和 compイル arch 可设置不同规则
- 可组合:多层过滤器逻辑组合
- 高性能:内核 JIT 编译 BPF,近乎零开销
2.3 安全保证(不可绕过性)
seccomp 通过 TIF_SECCOMP 标志和 syscall_enter 路径中的检查点实现:
- 用户不能修改自己的 seccomp 过滤器(
PR_SET_SECCOMP只能设置一次,除非CAP_SYS_ADMIN) PR_SET_NO_NEW_PRIVS阻止子进程获取更高权限- 过滤器在
execve()后继续生效
3. BPF 过滤器架构详解
3.1 seccomp 数据的内存布局
struct seccomp_data {
int nr; // 系统调用号
__u32 arch; // 架构标识 (AUDIT_ARCH_X86_64 等)
__u64 instruction_pointer; // 调用地址
__u64 args[6]; // 6 个参数 (arg0-arg5)
};
BPF 程序通过 seccomp_data 结构获取系统调用上下文,不能直接访问用户内存(即 args 中的指针不能直接解引用)。
3.2 BPF 指令集与辅助函数
seccomp 使用 cBPF(经典 BPF),非 eBPF:
// 允许的 BPF 指令子集
BPF_STMT(BPF_LD|BPF_W|BPF_ABS, offsetof(struct seccomp_data, arch))
BPF_STMT(BPF_LD|BPF_W|BPF_ABS, offsetof(struct seccomp_data, nr))
BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, AUDIT_ARCH_X86_64, 1, 0)
只能使用的辅助函数:
BPF_STLD/BPF_LDX— 加载指令BPF_ALU— 算术运算BPF_JMP— 条件跳转BPF_RET— 返回动作值
3.3 返回值语义(核心决策引擎)
每个 BPF 过滤器终止于 BPF_RET|BPF_K,返回值的高 16 位(SECCOMP_RET_DATA)携带额外信息:
| 返回值 | 宏定义 | 行为 |
|---|---|---|
| 0x7FFC0000 | SECCOMP_RET_ALLOW | 允许 syscall,继续执行 |
| 0x00050000 | SECCOMP_RET_ERRNO(E) | 拒绝,设置 errno,返回 -1 |
| 0x00000000 | SECCOMP_RET_KILL_PROCESS | 杀死进程(SIGSYS) |
| 0x00030000 | SECCOMP_RET_KILL_THREAD | 杀死当前线程 |
| 0x7FF00000 | SECCOMP_RET_TRACE | 触发 ptrace 通知 |
| 0x7FF70000 | SECCOMP_RET_LOG | 允许但记录日志 |
| 0x7FC00000 | SECCOMP_RET_USER_NOTIF | 发送事件到用户态 fd |
注意:
SECCOMP_RET_TRACE与SECCOMP_RET_USER_NOTIF行为不同:
- TRACE 必须由 ptrace tracer 接管处理
- USER_NOTIF 允许用户态监听 fd 后异步决策
4. 现代实践:三大使用模式
4.1 模式一:快速上手(伪代码框架)
#!/usr/bin/env python3
"""
seccomp-BFP 基础演示:仅允许 read/write/exit 的 STL mode 2 实现
"""
import ctypes, ctypes.util, struct, sys, os
libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
PTRACE = ctypes.CDLL('libpthread.so.0')
# 常量定义
PR_SET_SECCOMP = 22
PR_SET_NO_NEW_PRIVS = 38
SECCOMP_MODE_FILTER = 2
SECCOMP_RET_KILL = 0x00000000
SECCOMP_RET_ALLOW = 0x7FFF0000
AUDIT_ARCH_X86_64 = 0xC000003E
SYS_read = 0
SYS_write = 1
SYS_exit = 60
SYS_exit_group = 231
def install_seccomp_filter():
"""安装 BPF 过滤器"""
# BPF 伪指令
bpf = [
# 检查架构是否为 x86_64
(0x20, 0, 0, 4), # ld [arch]
(0x15, 0, 8, AUDIT_ARCH_X86_64), # jeq AUDIT_ARCH_X86_64, +8
(0x06, 0, 0, SECCOMP_RET_KILL), # 非 x86_64, kill
# 加载 syscall 号
(0x20, 0, 0, 0), # ld [nr]
# 检查 read (0)
(0x15, 0, 1, SYS_read), # jeq read, +1
# 检查 write (1)
(0x15, 0, 1, SYS_write), # jeq write, +1
# 检查 exit (60)
(0x15, 0, 1, SYS_exit), # jeq exit, +1
# 检查 exit_group (231)
(0x15, 0, 1, SYS_exit_group), # jeq exit_group, +1
# 其他系统调用, kill
(0x06, 0, 0, SECCOMP_RET_KILL), # ret KILL
# 允许
(0x06, 0, 0, SECCOMP_RET_ALLOW), # ret ALLOW
(0x06, 0, 0, SECCOMP_RET_ALLOW),
(0x06, 0, 0, SECCOMP_RET_ALLOW),
(0x06, 0, 0, SECCOMP_RET_ALLOW),
]
# 编码为二进制
bpf_prog = b''.join(struct.pack('HBBI', *insn) for insn in bpf)
# 构造 sock_fprog
class sock_fprog(ctypes.Structure):
_fields_ = [('len', ctypes.c_ushort), ('filter', ctypes.POINTER(ctypes.c_ushort))]
prog = sock_fprog(len(bpf), (ctypes.c_ushort * len(bpf)).from_buffer_copy(bpf_prog))
# 设置 no_new_privs
libc.prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
# 安装过滤器
result = libc.prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ctypes.byref(prog), 0, 0)
if result != 0:
print(f"prctl failed", file=sys.stderr)
os._exit(1)
print("Seccomp 过滤器安装成功")
# 以下操作成功
os.write(1, b"Hello from sandbox!\n")
# 以下操作会导致进程被 kill
# os.getpid() # 被拦截 → SIGSYS
if __name__ == '__main__':
install_seccomp_filter()
4.2 模式二:libseccomp(生产推荐)
/* 使用 libseccomp 构建容器沙箱过滤器 */
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
int apply_seccomp_filter(int target) {
scmp_filter_ctx ctx;
int rc;
/* 创建上下文:默认动作 */
ctx = seccomp_init(target);
if (ctx == NULL) return -1;
/* 设置默认 */
/* 方案A: 默认拒绝,白名单放行 */
/* 方案B: 默认允许,黑名单拦截 */
/* 方案A 示例:默认 kill,允许基础 syscall */
ctx = seccomp_init(SCMP_ACT_KILL_PROCESS);
/* 基础永远允许的 */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigreturn), 0);
/* 信号相关 */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigaction), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigprocmask), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sigaltstack), 0);
/* 条件过滤:仅允许 stdout/stderr 的 write */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
SCMP_A0(SCMP_CMP_EQ, 1) /* stdout */
);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
SCMP_A0(SCMP_CMP_EQ, 2) /* stderr */
);
/* mmap 条件过滤:仅允许匿名映射 */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 1,
SCMP_A3(SCMP_CMP_MASKED_EQ, MAP_ANONYMOUS, MAP_ANONYMOUS)
);
/* 拦截会修改进程属性的 syscall */
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(ptrace), 0);
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(mount), 0);
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(umount2), 0);
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(reboot), 0);
/* 加载 */
rc = seccomp_load(ctx);
seccomp_release(ctx);
return rc;
}
4.3 模式三:seccomp unotify — 用户态决策
/* 使用 seccomp unotify 实现用户态 syscall 代理 */
#include <sys/ioctl.h>
#include <linux/seccomp.h>
#include <poll.h>
#define SECCOMP_IOCTL_NOTIF_ID_VALID 0x40082100U
#define SECCOMP_IOCTL_NOTIF_RECV 0x40082102U
#define SECCOMP_IOCTL_NOTIF_SEND 0xc0082102U
struct seccomp_notif {
__u64 id;
__u32 pid;
__u32 flags;
struct seccomp_data data;
};
struct seccomp_notif_resp {
__u64 id;
__s64 val;
__s32 error;
__u32 flags;
};
/* 工作线程:监听并代理 syscall */
int seccomp_notif_worker(int notify_fd) {
struct seccomp_notif req;
struct seccomp_notif_resp resp;
while (1) {
memset(&req, 0, sizeof(req));
/* 阻塞等待事件 */
if (ioctl(notify_fd, SECCOMP_IOCTL_NOTIF_RECV, &req) < 0)
continue;
memset(&resp, 0, sizeof(resp));
resp.id = req.id;
resp.flags = 0;
/* 根据 syscall 号执行用户态逻辑 */
switch (req.data.nr) {
case __NR_openat: {
/* 代理 openat:检查路径合法性 */
int dirfd = req.data.args[0];
const char *path = (const char *)(uintptr_t) req.data.args[1];
int flags = req.data.args[2];
mode_t mode = req.data.args[3];
/* 用户态读取路径字符串 */
char buf[PATH_MAX];
int ret = read_path_from_pid(req.pid, dirfd, path, buf, sizeof(buf));
if (is_path_allowed(buf)) {
/* 在用户态执行 openat */
int fd = openat(dirfd, path, flags, mode);
resp.val = fd;
resp.error = fd < 0 ? errno : 0;
} else {
resp.error = EACCES;
}
break;
}
case __NR_connect: {
/* 代理 connect:检查目标 */
int sockfd = req.data.args[0];
struct sockaddr *addr = (void*)(uintptr_t) req.data.args[1];
socklen_t addrlen = req.data.args[2];
/* 解析地址并决策 */
if (is_connection_allowed(addr, addrlen)) {
/* 用户态执行 connect */
resp.val = connect(sockfd, addr, addrlen);
resp.error = errno;
} else {
resp.error = ECONNREFUSED;
}
break;
}
default:
resp.error = ENOSYS;
break;
}
/* 回复决策 */
ioctl(notify_fd, SECCOMP_IOCTL_NOTIF_SEND, &resp);
}
}
5. 容器场景:运行时安全的基石
5.1 Docker 的默认 seccomp 配置文件
Docker 内置的默认 seccomp profile(default.json)阻塞了约 44 个危险 syscall,主要包括:
| 类别 | 被拦截的 syscall | 原因 |
|---|---|---|
| 内核模块 | create_module, init_module, finit_module, delete_module | 防止内核模块注入 |
| 系统管理 | setns, unshare, kexec_load, reboot | 防止容器逃逸 |
| 时钟操作 | clock_settime, clock_adjtime, adjtimex | 防止时间篡改 |
| 底层 I/O | lookup_dcookie, kcmp, process_vm_readv | 防止进程间侦察 |
| 审计控制 | acct, syslog, syslog | 防止审计旁路 |
5.2 自定义 Docker seccomp Profile
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "close", "exit", "exit_group", "rt_sigreturn"],
"action": "SCMP_ACT_ALLOW"
},
{
"names": ["write"],
"action": "SCMP_ACT_ALLOW",
"args": [
{ "index": 0, "value": 1, "op": "SCMP_CMP_EQ" },
{ "index": 0, "value": 2, "op": "SCMP_CMP_EQ" }
]
},
{
"names": ["openat"],
"action": "SCMP_ACT_ALLOW",
"args": [
{ "index": 1, "value": "/app/data", "op": "SCMP_CMP_MASKED_EQ" }
]
},
{
"names": ["ptrace", "mount", "umount2", "reboot", "kexec_load"],
"action": "SCMP_ACT_KILL_PROCESS"
}
]
}
5.3 containerd 与 Kubernetes Pod Security
apiVersion: v1
kind: Pod
metadata:
name: secure-app
annotations:
seccomp.security.alpha.kubernetes.io/pod: runtime/default
# 或自定义 profile
seccomp.security.alpha/kubernetes.io/pod: localhost/my-profile.json
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: my-app:latest
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
6. Chrome 沙箱架构解析
Chrome 的多进程沙箱模型是 seccomp-BPF 最复杂且成熟的应用之一。
6.1 Linux 下的分层沙箱
层级 0: setuid sandbox (Chromium < 20 已废弃)
层级 1: Namespaces (PID/Network/Mount/User)
层级 2: seccomp-BPF (限制 syscall)
层级 3: RLIMIT 限制资源
6.2 Chrome seccomp 策略
Chrome 的渲染进程使用非常严格的 seccomp 策略:
- 允许的基础 syscall 仅约 150 个(Linux x86_64)
- 关键过滤:
socket()仅允许AF_UNIX,open()可读 - 通过
perf_event_open获取性能计数器的特定过滤规则
6.3 自定义生成 BPF
Chrome 使用自己的 BPF 代码生成器(非 libseccomp),位于 sandbox/linux/seccomp-bpf/:
bpf_gpu_policy_linux.c— GPU 进程沙箱bpf_renderer_policy_linux.c— 渲染进程沙箱bpf_ppapi_policy_linux.c— PPAPI 插件沙箱
7. 现代化替代方案:Landlock LSM
Landlock 是 Linux 5.13+ 引入的 unprivileged LSM,提供文件系统访问控制:
/* Landlock + seccomp 联合使用示例 */
#include <linux/landlock.h>
#include <sys/syscall.h>
int setup_landlock(void) {
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_MAKE_DIR,
};
int ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
/* 仅允许读取 /app 目录 */
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR,
.parent_fd = open("/app", O_PATH | O_CLOEXEC),
};
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0);
/* 禁止写入 */
struct landlock_path_beneath_attr deny_write = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
.parent_fd = open("/etc", O_PATH | O_CLOEXEC),
};
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &deny_write, 0);
landlock_restrict_self(ruleset_fd, 0);
close(ruleset_fd);
return 0;
}
设计模式:seccomp 控制 syscall 入口,Landlock 控制文件路径访问——两者正交结合。
8. 性能影响评估
8.1 系统调用开销对比
| 场景 | 每次 syscall 开销 | 说明 |
|---|---|---|
| 无 seccomp | ~100ns | 基准线 |
| seccomp KILL (allow) | ~100-120ns | BPF JIT,几乎无感知 |
| seccomp KILL (deny) | ~200ns | SIGSYS 路径更短 |
| SECCOMP_RET_TRACE | ~12μs | ptrace 断点停止 |
| SECCOMP_RET_USER_NOTIF | ~60-160μs | 涉及用户态上下文切换 |
数据来源:Phoronix 测试及 Linux 内核 seccomp_bpf benchmark。
8.2 优化建议
- 常用 syscall 放在过滤器前部(分支预测友好)
- 避免频繁触发 USER_NOTIF(同步跨进程开销大)
- 共享过滤器:
SECCOMP_FILTER_FLAG_TSYNC同步多线程过滤器 - 注意过滤器数量限制:默认 65536 BPF 指令(`/proc/sys/net/core/bpf_jit_enable`)
- JIT 开启:
echo 1 > /proc/sys/net/core/bpf_jit_enable
9. 调试与监控
9.1 strace + seccomp 行为
# 查看 seccomp 过滤器安装
cat /proc/<PID>/status | grep -i seccomp
# 输出: Seccomp: 2 (filter mode)
# sysdig 监控 syscall
syscall=%{evt.type} proc=%{proc.name} res=%{evt.res}
# 查看内核日志被拦截的 syscall
dmesg | grep -i seccomp
9.2 日志记录(SECCOMP_RET_LOG)
配置审计日志:
# 在过滤器中返回 SECCOMP_RET_LOG 的 syscall 会被 audit 记录
auditctl -a always,exit -F arch=b64 -S all -F subj_type=myapp_t
journalctl -f _AUDIT_TYPE=1408 # AUDIT_SECCOMP
9.3 seccomp 事件追踪点
# 使用 bpftrace 追踪
bpftrace -e 'tracepoint:raw_syscalls:sys_enter /args->id == __NR_write/ {
printf("write called by %d\n", pid);
}'
# 使用 perf trace
perf trace -e 'syscalls:sys_exit_*' --filter 'ret == -38' # ENOSYS
10. 生产环境最佳实践
10.1 设计 seccomp 策略的 5 条准则
- 默认拒绝 + 白名单(
SCMP_ACT_KILL/SCMP_ACT_ERRNO) - 先读后写:先用 SECCOMP_RET_LOG 观察,确认无误杀后切换到拒绝
- 跨架构准备:检查
arch字段,防止 64/32 位绕过 - 条件过滤:对
openat/bind/connect/ptrace做参数过滤 - 组合防御:seccomp + Namespaces + Capabilities + Landlock + cgroup
10.2 常见陷阱
| 陷阱 | 后果 | 防护 |
|---|---|---|
未设置 NO_NEW_PRIVS |
子进程通过 exec 恢复完整权限 | 始终先 prctl(PR_SET_NO_NEW_PRIVS, 1) |
| 忽略 arch 检查 | 32 位进程绕过 64 位过滤器 | 先 load arch → jeq native |
SECCOMP_RET_TRACE 未配置 tracer |
进程被暂停不恢复 | 确保有 ptrace tracer 救援 |
忘记 execve() 兼容性 |
exec 新 bin 因 seccomp 崩溃 | 使用 SCMP_ALLOW 或调整策略 |
10.3 systemd 单元保护
[Service]
# 调用 systemd 内置的 seccomp 白名单
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@mount @reboot @swap @module @debug @privileged @resources
# 或完全自定义
SystemCallFilter=~clock_settime reboot kexec_load ptrace ...
SystemCallErrorNumber=EPERM
11. 总结
seccomp-BPF 是现代 Linux 安全纵深防御中不可或缺的一环:
- 容器运行时:Docker/containerd 的默认 seccomp profile 是防止容器逃逸的关键层
- 浏览器:Chrome 的沙箱策略保护数亿用户免受渲染进程漏洞影响
- 桌面应用:Flatpak、Snap 使用 seccomp+BubbleWrap 实现应用隔离
- 系统服务:systemd 的
SystemCallFilter提供声明式的服务级保护
与 Namespaces(视图隔离)、Capabilities(权限拆分)、Landlock(文件系统访问控制)结合,seccomp-BFP 构成了内核安全控制的完整技术栈。
设计安全策略时,始终遵循"默认拒绝、审计过渡、多层防御"的原则,并在生产环境部署前通过 strace/bpftrace/sysdig 等工具充分测试 syscall 覆盖情况。

发表评论 取消回复