Linux Seccomp Notify 与用户态回调扩展安全沙箱:从容器隔离到高级系统调用虚拟化
在现代 Linux 安全架构中,seccomp(Secure Computing mode)是容器运行时(Docker、Podman、Kubernetes)、沙箱(Firecracker、gVisor)和浏览器沙箱(Chromium、Firefox)的核心防线。传统的 SECCOMP_MODE_STRICT 和 SECCOMP_MODE_FILTER 采用白名单策略——不被允许的系统调用直接导致进程被 SIGSYS 终止。这种"非黑即白"的模型在大多数场景下足够安全,但也暴露出一个根本性缺陷:无法支持那些"有时需要、有时危险"的系统调用。
Linux 5.0 引入的 SECCOMP_RET_USER_NOTIF(Seccomp Notify)开创了全新的范式——将"信任决策"从内核转移到用户态 supervisor 进程。本文将深入剖析 Notify 机制的内核实现、API 编程模型、在容器安全中的实战应用(特别是根容器内运行需要 mount/chmod 的场景),以及如何基于此构建一个完整的系统调用虚拟化层。
一、Seccomp 的演进:从白名单到可审计的拦截
1.1 传统 Seccomp Filter 的局限
经典的 BPF seccomp 过滤器在系统调用入口执行,对每个 syscall 只能返回以下 verdict 之一:
SECCOMP_RET_ALLOW // 放行
SECCOMP_RET_KILL // 杀死进程
SECCOMP_RET_TRAP // 发送 SIGSYS
SECCOMP_RET_ERRNO // 返回错误码
SECCOMP_RET_TRACE // 通知 ptrace tracer
SECCOMP_RET_LOG // 记录后放行
当一个容器需要运行 strace、gdb、fakeroot 或需要文件系统操作的工具时,这种二元模型直接撞上矛盾:如果允许相关 syscall(如 ptrace、mount、chmod),则容器逃逸风险陡增;如果禁止,则正常功能无法使用。
1.2 为什么 Ptrace 不够用
SECCOMP_RET_TRACE 系统调用通知 ptrace tracer 进程,但基于 ptrace 的方案有几个难以克服的缺陷:
- 语义鸿沟:tracer 需要理解每个 syscall 的 ABI(参数结构、指针解引用等),实现一个通用的 syscall 转发器极其困难
- 性能开销:每次 syscall 触发两次上下文切换(被 trace 进程 ↔ tracer),对于 syscall 密集负载开销极大
- 语义保持:ptrace 修改的是被 trace 进程的寄存器,无法自然地在另一个进程中"模拟" syscall 并回写结果
- 安全边界模糊:ptrace tracer 拥有对被 trace 进程的完全控制权,打破了最小特权原则
Seccomp Notify 的设计目标是:让一个监督进程可以检查、模拟或拒绝系统调用,同时保持被监管进程的安全边界。
二、Seccomp Notify 内核架构与工作原理
2.1 核心数据结构
Seccomp Notify 的核心是 struct seccomp_filter 配合 SECCOMP_RET_USER_NOTIF 返回值,以及通过 seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog) 创建的文件描述符:
// 用户态 API(通过 libseccomp 或 raw syscall 调用)
struct seccomp_notif_sizes sizes = {0};
syscall(__NR_seccomp, SECCOMP_GET_NOTIF_SIZES, 0, &sizes);
// 分配缓冲区
struct seccomp_notif *req = malloc(sizes.seccomp_notif);
struct seccomp_notif_resp *resp = malloc(sizes.seccomp_notif_resp);
struct seccomp_notif_addrs *addrs = NULL; // 6.0+ 可用
当被监管进程执行了一个被 Notify 标记的系统调用时,内核执行以下流程:
- BPF 过滤器返回
SECCOMP_RET_USER_NOTIF - 内核冻结被监管进程(处于
TASK_INTERRUPTIBLE状态) - 构建
seccomp_notif结构并写入 listener fd - Supervisor 进程通过
ioctl(SECCOMP_IOCTL_NOTIF_RECV)读取通知 - Supervisor 决定:允许、拒绝、模拟或注入自定义错误码
- 通过
ioctl(SECCOMP_IOCTL_NOTIF_SEND)返回结果 - 内核恢复被监管进程的执行
2.2 通知结构详解
struct seccomp_notif 包含了系统调用的完整上下文:
struct seccomp_notif {
__u64 id; // 唯一通知 ID(用于后续响应匹配)
__u32 pid; // 触发 syscall 的进程 PID
__u32 flags; // 标志位(SECCOMP_NOTIF_FLAG)
struct seccomp_data data; // syscall 信息
};
struct seccomp_data {
int nr; // 系统调用号
__u32 arch; // 架构标识(AUDIT_ARCH_X86_64 等)
__u64 instruction_pointer; // 触发时的 RIP
__u64 args[6]; // 六个系统调用参数
};
Linux 6.0 引入的 struct seccomp_notif_addrs 进一步暴露了每个参数对应的地址空间信息,使得 supervisor 可以更精确地判断参数的可信度。
2.3 响应选项与语义
Supervisor 需填充 struct seccomp_notif_resp 来回应:
struct seccomp_notif_resp {
__u64 id; // 必须与请求的 id 匹配
__s64 val; // 返回值(模拟结果或 -errno)
__s32 error; // 错误码(0 表示允许,负 errno 表示拒绝)
__u32 flags; // SECCOMP_USER_NOTIF_FLAG 标志组合
};
关键标志位:
| 标志 | 语义 |
|---|---|
SECCOMP_USER_NOTIF_FLAG_CONTINUE |
忽略 val/error,重新执行原始 syscall(绕过 notify) |
| 无标志 + error=0, val=X | 模拟 syscall 返回 X |
| 无标志 + error=-EPERM | 阻止 syscall,进程收到 EPERM |
2.4 Seccomp Notify 的关键安全约束
为防止 supervisor 进程滥用权限,内核实施了多项重要约束:
- Supervisor 必须在同一 seccomp 命名空间中:不能跨命名空间代理
- Respons 伪造屏障:supervisor 返回的
val必须在合理范围内(如 open() 返回的 fd 必须指向被监管进程已经打开的真实 fd) - Madvise 毒化:被监管进程调用
madvise(MADV_DONTNEED)可以用于将 supervisor 传递的数据从页面缓存清除,避免数据残留 - ID 一致性:每个 notify 的六字节 ID 保证唯一性,防止响应被注入到错误的监听器
三、基于 Seccomp Notify 的系统调用虚拟化
Seccomp Notify 最强大的应用是系统调用虚拟化——在用户态完整模拟一个被拦截的系统调用,使得被监管进程感知不到自己被限制。
3.1 Supervisor 进程设计
一个完整的 Notify-based supervisor 需要实现以下核心循环:
#include <linux/seccomp.h>
#include <linux/audit.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define WAIT_FOR_CHILD 0x1
#define INOTIFY_WATCH 0x2
// 从被监管进程的用户空间读取内存
static long get_message(struct seccomp_notif *req, void *dst,
size_t dst_size, off_t src_addr,
int listener) {
struct seccomp_notif_addrs addrs = {};
struct iovec iov = {
.iov_base = dst,
.iov_len = dst_size,
};
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
};
// SECCOMP_IOCTL_NOTIF_ID_VALID 检查 ID 是否仍然有效
// SECCOMP_IOCTL_NOTIF_ID_VALID_WRONG_DIRCT (6.0+)
return ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, req);
}
int main(int argc, char *argv[]) {
struct seccomp_notif_sizes sizes;
struct seccomp_notif *req;
struct seccomp_notif_resp *resp;
int listener;
// 获取通知结构体大小
if (seccomp(SECCOMP_GET_NOTIF_SIZES, 0, &sizes) < 0) {
perror("seccomp(GET_NOTIF_SIZES)");
return 1;
}
req = malloc(sizes.seccomp_notif);
resp = malloc(sizes.seccomp_notif_resp);
// 主监听循环
while (1) {
memset(req, 0, sizes.seccomp_notif);
memset(resp, 0, sizes.seccomp_notif_resp);
// 阻塞等待通知
if (ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, req) < 0) {
if (errno == EINTR) continue;
perror("ioctl(RECV)");
break;
}
resp->id = req->id;
resp->flags = 0;
// 根据 syscall 号分发处理
switch (req->data.nr) {
case __NR_openat:
handle_openat(req, resp, listener);
break;
case __NR_mount:
handle_mount(req, resp, listener);
break;
case __NR_chmod:
case __NR_fchmod:
handle_chmod(req, resp, listener);
break;
default:
// 不允许的 syscall
resp->error = -EPERM;
resp->val = -1;
break;
}
// 发送响应
if (ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, resp) < 0) {
perror("ioctl(SEND)");
}
}
free(req);
free(resp);
return 0;
}
3.2 虚拟化 openat 系统调用
openat 是文件操作的基础 syscall,虚拟化它需要处理路径解析、fd 管理和权限检查:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// 虚拟化 openat: 在 supervisor 进程中以自己的身份执行
void handle_openat(struct seccomp_notif *req,
struct seccomp_notif_resp *resp,
int listener) {
int dirfd = (int)req->data.args[0];
const char *pathname = (const char *)(uintptr_t)req->data.args[1];
int flags = (int)req->data.args[2];
mode_t mode = (mode_t)req->data.args[3];
// 安全: 读取被监管进程用户空间中的路径字符串
char path[PATH_MAX] = {0};
// 方法一:通过 /proc/self/mem 跨进程读取
// 方法二:使用 seccomp 的 process_vm_readv
// 方法三:使用 pidfd_getfd + readlink
// 最简单的方案:通过 /proc/<pid>/root/pathname 在 supervisor
// 命名空间中解析
char proc_path[PATH_MAX];
snprintf(proc_path, sizeof(proc_path),
"/proc/%d/root%s", req->pid, pathname);
// 在 supervisor 进程执行真实的 open
int fd = open(proc_path, flags, mode);
if (fd < 0) {
resp->error = -errno;
resp->val = -1;
} else {
// 将 fd 转移到被监管进程
// 方法:使用 pidfd_getfd()(Linux 5.6+)
resp->error = 0;
resp->val = fd; // supervisor 拥有的 fd
resp->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
// 接下来需要将 fd 交付给被监管进程
deliver_fd_to_child(req->pid, fd, listener);
close(fd);
}
}
3.3 FD 交付机制
Seccomp Notify 的一个核心挑战是:supervisor 返回的 fd 必须是被监管进程能操作的。Linux 提供了两种机制:
方案 A:pidfd_getfd(Linux 5.6+)
#include <sys/syscall.h>
int pidfd_getfd(int pidfd, int fd, unsigned flags) {
return syscall(__NR_pidfd_getfd, pidfd, fd, flags);
}
// 在 supervisor 中的交付逻辑
void deliver_fd_to_child(pid_t child_pid, int fd, int listener) {
// 首先获取被监管进程的 pidfd
int pidfd = pidfd_open(child_pid, 0);
// 按 ID 验证的 ioctl 之后发送 SECCOMP_IOCTL_NOTIF_ADDFD
// Linux 5.14+ 提供了原生的 fd 注入
struct seccomp_notif_addrs addrs = {};
// 使用 SECCOMP_IOCTL_NOTIF_ADDFD 原子操作
int flags = SECCOMP_ADDFD_FLAG_SEND |
SECCOMP_ADDFD_FLAG_SETFD;
ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD, &addrs);
close(pidfd);
}
方案 B:SCM_RIGHTS 通过 Unix Domain Socket 传递
由于原始 fd 在不同进程间不直接兼容的传统方式是通过 Unix socket 发送:
// 通过 Unix Domain Socket 发送 fd
int send_fd(int sock, int fd_to_send) {
struct msghdr msg = {0};
struct iovec iov;
char buf[1];
buf[0] = 'f';
iov.iov_base = buf;
iov.iov_len = 1;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
union {
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
} control_un;
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg);
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
*(int *)CMSG_DATA(cmptr) = fd_to_send;
return sendmsg(sock, &msg, 0);
}
然而,更优雅的方式是使用 SECCOMP_IOCTL_NOTIF_ADDFD(Linux 5.14+),它在一次 ioctl 调用中完成 fd 注入到目标 notify 响应:
// Linux 5.14+ 的原生 fd 注入
struct seccomp_notif_addrs notif_addrs = {};
struct seccomp_notif_resp resp = {
.id = req->id,
.val = target_fd,
.error = 0,
.flags = 0,
};
// SECCOMP_IOCTL_NOTIF_ADDFD 将 fd 转移到被监管进程
struct seccomp_notif_addrs addrs = {};
// 将 supervisor 注入的 fd 进入监听流程
// 返回的新 fd 在被监管进程中生效
// 注意:SECCOMP_ADDFD_FLAG_SEND 标志会自动将附带新 fd 的响应
// 发送回内核,无需额外调用 NOTIF_SEND
int new_fd = ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD, &addrs);
3.4 指针参数的安全解引用
Seccomp Notify 事件传递的是被监管进程用户空间的 64 位参数值,其中指针类参数(如 openat 中的 pathname)指向被监管进程自己的地址空间。supervisor 不能直接解引用这些指针——必须通过安全的跨进程内存访问机制:
方案一:/proc/pid/mem
int read_process_mem(pid_t pid, void *dst, size_t len,
uintptr_t remote_addr) {
char path[64];
snprintf(path, sizeof(path), "/proc/%d/mem", pid);
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
if (lseek(fd, (off_t)remote_addr, SEEK_SET) < 0) {
close(fd);
return -1;
}
ssize_t ret = read(fd, dst, len);
close(fd);
return (ret == (ssize_t)len) ? 0 : -1;
}
// 注意:需要 PTRACE_ATTACH 权限才能读取 /proc/pid/mem
方案二:process_vm_readv (更高效)
#include <sys/uio.h>
ssize_t process_vm_readv(pid_t pid,
const struct iovec *local_iov,
unsigned long liovcnt,
const struct iovec *remote_iov,
unsigned long riovcnt,
unsigned long flags);
ssize_t safe_read_child(pid_t pid, void *dst, size_t len,
uintptr_t src) {
struct iovec local = { .iov_base = dst, .iov_len = len };
struct iovec remote = { .iov_base = (void *)src,
.iov_len = len };
return process_vm_readv(pid, &local, 1, &remote, 1, 0);
}
注意:上述两种方法都需要 CAP_SYS_PTRACE 能力或等价权限,这意味着 supervisor 本身需要一定特权。这也是为什么 notify 架构中通常由一个特权 supervisor 服务多个被监管进程。
四、容器安全场景实战
4.1 根容器(Rootless Container)中的 FUSE 挂载
根容器不使用宿主 root 权限运行容器引擎,但某些场景(rootless Docker + --privileged 缺少某些操作)需要 mount() syscall。传统 rootless 方案使用 fakeroot + subuid mapping,但无法处理真实的 mount 操作。
基于 Seccomp Notify 的方案:
// 特权 supervisor 运行在容器外部,代表容器发起 mount
void handle_mount(struct seccomp_notif *req,
struct seccomp_notif_resp *resp,
int listener) {
const char *source = (const char *)(uintptr_t)req->data.args[0];
const char *target = (const char *)(uintptr_t)req->data.args[1];
const char *fstype = (const char *)(uintptr_t)req->data.args[2];
unsigned long mountflags = (unsigned long)req->data.args[3];
const void *data = (const void *)(uintptr_t)req->data.args[4];
// 在容器命名空间中执行 mount(通过 setns)
int target_fd = pidfd_open(req->pid, 0);
setns(target_fd, CLONE_NEWNS);
// 安全检查:不允许挂载危险文件系统
static const char *allowed_fs[] = {
"proc", "sysfs", "tmpfs", "cgroup", "cgroup2",
"fuse", "overlay", "ext4", "xfs", NULL
};
bool allowed = false;
for (int i = 0; allowed_fs[i]; i++) {
if (strcmp(fstype, allowed_fs[i]) == 0) {
allowed = true;
break;
}
}
if (!allowed) {
resp->error = -EPERM;
resp->val = -1;
return;
}
// 限制挂载点必须在容器内
char resolved_target[PATH_MAX];
if (!is_inside_container(target, req->pid)) {
resp->error = -EINVAL;
resp->val = -1;
return;
}
// 执行 mount(supervisor 具有所需权限)
int ret = mount(source, target, fstype, mountflags, data);
if (ret < 0) {
resp->error = -errno;
resp->val = -1;
} else {
resp->error = 0;
resp->val = 0;
}
}
4.2 安全的 Chroot/Pivot Root 代理
容器初始化时通常需要 chroot 或 pivot_root,但 CAP_SYS_CHROOT 本身允许逃逸。Seccomp Notify 可以 validates 目标路径:
void handle_pivot_root(struct seccomp_notif *req,
struct seccomp_notif_resp *resp,
int listener) {
const char *new_root = (const char *)(uintptr_t)req->data.args[0];
const char *put_old = (const char *)(uintptr_t)req->data.args[1];
char new_root_path[PATH_MAX];
safe_read_child(req->pid, new_root_path, PATH_MAX,
(uintptr_t)new_root);
// 安全检查:必须指向容器 preconfigured 的 rootfs
static const char *prefix = "/var/lib/containers/storage/overlay";
if (strncmp(new_root_path, prefix, strlen(prefix)) != 0) {
resp->error = -EPERM;
return;
}
// 放行:内核确认权限后执行
resp->error = 0;
resp->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
// 内核将实际执行 pivot_root
}
4.3 Docker × Seccomp Notify 的集成模式
Docker 当前使用的 runtime/default seccomp profile 一旦返回 SECCOMP_RET_ERRNO,syscall 被阻止。如果希望在某些容器中使用 Notify:
{
"defaultAction": "SCMP_ACT_ERRNO",
"listenerPath": "/run/seccomp-supervisor.sock",
"listenerMetadata": "container_id=abc123",
"syscalls": [
{
"names": ["mount", "umount2",
"pivot_root", "chroot"],
"action": "SCMP_ACT_NOTIFY"
},
{
"names": ["openat", "openat2"],
"action": "SCMP_ACT_NOTIFY"
},
{
"names": ["clone", "unshare",
"setns"],
"action": "SCMP_ACT_NOTIFY"
}
]
}
在 Kubernetes 中通过 SecurityContext 注入:
apiVersion: v1
kind: Pod
metadata:
name: sandbox-with-supervisor
annotations:
container.apparmor.security.beta.kubernetes.io/main: "runtime/default"
# Seccomp Notify profile path annotation
spec:
containers:
- name: main
image: internal-app:latest
securityContext:
seccompProfile:
type: Localhost
localhostProfile: notify-supervisor.json
- name: supervisor
image: seccomp-supervisor:latest
securityContext:
capabilities:
add: ["SYS_ADMIN", "SYS_PTRACE", "SYS_CHROOT"]
volumeMounts:
- name: listener
mountPath: /run/seccomp-supervisor.sock
volumes:
- name: listener
emptyDir: {}
五、高级性能优化与并发架构
5.1 _NOTIFY ioctl 的性能特征
Seccomp Notify 监听器的自然 API 是阻塞式的 ioctl,每个 RECV 会阻塞直到有新的 notify 到来。在高并发场景下,必须采用多线程或 epoll 驱动的事件循环。
关键限制:在 Linux 6.0 之前,SECCOMP_IOCTL_NOTIF_RECV 不支持 O_NONBLOCK。从 Linux 6.0 开始,可以将监听 fd 设置为非阻塞模式:
// 设置监听 fd 为非阻塞
int flags = fcntl(listener, F_GETFL, 0);
fcntl(listener, F_SETFL, flags | O_NONBLOCK);
// 现在 RECV 在无通知时返回 -EAGAIN
5.2 多线程 Supervisor 架构
#include <pthread.h>
#define MAX_WORKERS 16
#define NOTIF_CACHE_SIZE 4096
// 工作线程池结构
typedef struct {
int listener; // 共享的监听 fd
pthread_t workers[MAX_WORKERS];
_Atomic int active_count;
} supervisor_pool;
// 线程参数
typedef struct {
supervisor_pool *pool;
int worker_id;
} worker_arg;
void *supervisor_worker(void *arg) {
worker_arg *warg = (worker_arg *)arg;
supervisor_pool *pool = warg->pool;
struct seccomp_notif_sizes sizes;
seccomp(SECCOMP_GET_NOTIF_SIZES, 0, &sizes);
struct seccomp_notif *req = malloc(sizes.seccomp_notif);
struct seccomp_notif_resp *resp = malloc(sizes.seccomp_notif_resp);
while (pool->running) {
// 关键:多个线程共享同一个 listener fd
// 内核保证每个 notify 只被一个线程获得(竞争消费模式)
int ret = ioctl(pool->listener,
SECCOMP_IOCTL_NOTIF_RECV, req);
if (ret < 0) {
if (errno == EINTR) continue;
break;
}
// 处理通知并发送响应
handle_syscall(req, resp, pool->listener);
ioctl(pool->listener, SECCOMP_IOCTL_NOTIF_SEND, resp);
}
free(req);
free(resp);
return NULL;
}
重要:多个线程同时调用 NOTIF_RECV 是安全的,内核确保每个 notify 仅被一个线程领取,形成天然的工作窃取(work stealing)模型。
5.3 BPF Map 加速:绕过 Notify 的纯内核处理
对于高频且决策逻辑简单的 syscall(如 getpid、time、clock_gettime),通知用户态的开销过高。可以结合 BPF Map 实现"内核内快速路径":
// BPF 程序中的逻辑
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u32);
__type(value, u64);
__uint(max_entries, 1024);
} fast_cache SEC(".maps");
// XDP/socket filter 类型的 BPF
SEC("tp/raw_syscalls/sys_enter")
int trace_syscall(struct trace_event_raw_sys_enter *ctx) {
u32 nr = ctx->id;
// 对高频 syscalls,直接从 BPF Map 读取结果
// 避免了用户态 notify 的上下文切换开销
if (nr == __NR_getpid ||
nr == __NR_gettimeofday) {
// 缓存命中则直接返回
}
// 复杂的 syscall 上升至用户态 notify
return 0;
}
六、Seccomp Notify 的安全边界与潜在风险
6.1 TOCTOU 攻击窗口
由于 Notify 是异步的(supervisor 检查时 syscall 已中断),存在检查时间点与执行时间点的差异窗口。内核通过以下机制缓解:
- id 唯一性:每个 notify 有唯一的 64 位 ID
- id 存活期验证:supervisor 可以调用
SECCOMP_IOCTL_NOTIF_ID_VALID检查 ID 仍然有效(被监管进程仍然在该 syscall 上等待) - madvise DONTNEED 屏障:被监管进程在调用 notify 点的 mmap 区域的任何修改都会使 supervisor 返回的响应无效
6.2 Supervisor DoS
如果 supervisor 处理缓慢或崩溃,被监管进程将持续被阻塞。Linux 6.3+ 引入了 SECCOMP_IOCTL_NOTIF_SET_FLAGS 配合超时机制来处理这种情况。
6.3 权限泄漏路径
必须确保 supervisor 本身的权限边界:
- 仅暴露 SECCOMP_RET_USER_NOTIF 标记的 target syscall
- supervisor 对 syscalls 的模拟结果必须经过内核合理性检查(如 fd 验证)
- 不应当在 supervisor 中存储被监管进程的敏感信息
七、生产级 Supvervisor 完整实现
以下是一个可以部署于容器的 full-featured supervisor(基于 libseccomp 封装的高层 API):
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
static int install_notify_filter(int *listener_fd) {
scmp_filter_ctx ctx;
// 不允许被监管进程禁用 seccomp
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
perror("prctl(NO_NEW_PRIVS)");
return -1;
}
// 放行大部分 syscall,仅虚拟化关键 syscall
ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) return -1;
// 将需要虚拟化的 syscall 标记为 NOTIFY
int targets[] = {
SCMP_SYS(mount),
SCMP_SYS(umount2),
SCMP_SYS(pivot_root),
SCMP_SYS(chroot),
SCMP_SYS(clone),
SCMP_SYS(unshare),
SCMP_SYS(setns),
SCMP_SYS(openat),
SCMP_SYS(openat2),
};
for (int i = 0; i < sizeof(targets)/sizeof(targets[0]); i++) {
if (seccomp_rule_add(ctx, SCMP_ACT_NOTIFY,
targets[i], 0) < 0) {
fprintf(stderr, "Failed to add %d: %s\n",
targets[i], strerror(errno));
}
}
// 创建监听 fd
*listener_fd = seccomp_listener_fd(ctx);
// 加载过滤器(fork 后 child 执行此过滤器)
seccomp_export_bpf(ctx, STDOUT_FILENO); // 调试
if (seccomp_load(ctx) < 0) {
perror("seccomp_load");
seccomp_release(ctx);
return -1;
}
seccomp_release(ctx);
return 0;
}
int main(int argc, char *argv[]) {
int listener = -1;
if (install_notify_filter(&listener) < 0) {
return 1;
}
printf("Seccomp Notify supervisor listening on fd %d\n", listener);
// 主循环:接受通知并处理
struct seccomp_notif_sizes sizes;
seccomp(SECCOMP_GET_NOTIF_SIZES, 0, &sizes);
struct seccomp_notif *req = malloc(sizes.seccomp_notif);
struct seccomp_notif_resp *resp = malloc(sizes.seccomp_notif_resp);
while (1) {
memset(req, 0, sizes.seccomp_notif);
memset(resp, 0, sizes.seccomp_notif_resp);
if (ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, req) < 0) {
if (errno == EINTR) continue;
break;
}
resp->id = req->id;
// 根据 syscall 号分发
int nr = req->data.nr;
int should_supervise = 1;
switch (nr) {
case SCMP_SYS(openat):
handle_openat(req, resp, listener);
break;
case SCMP_SYS(mount):
handle_mount_supervisor(req, resp, listener);
break;
default:
// 默认: 拒绝未预期 syscall
resp->error = -EPERM;
resp->val = -1;
fprintf(stderr, "Unexpected notify nr=%d\n", nr);
}
// 发送响应
if (ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, resp) < 0) {
perror("NOTIF_SEND");
}
}
free(req);
free(resp);
close(listener);
return 0;
}
八、总结与展望
Seccomp Notify 代表了 Linux 安全模型从"内核强制"到"用户态可编程"的关键范式转变:
核心价值: - 将不可解除的"允许/拒绝"升级为策略化的"决策代理" - 使容器引擎能在 rootless 模式下安全地虚拟化特权 syscall - 为无特权 FUSE、overlay mount、用户命名空间嵌套提供通用的安全基础
设计权衡: - 每个通知 2 次上下文切换 + supervisor 处理延迟 ≈ 5-50μs(对比直接 syscall 的 50-200ns) - 仅适合决策频率受限的操作(mount、clone、open 等),不适合 read/write - Supervisor 本身成为系统中的特权组件,需要内部加固
演进方向: - Linux 6.x 持续优化 id 验证和 fd 注入的性能 - 与 Landlock 的联合使用(文件访问控制由 Landland 完成,syscall 控制由 Notify 完成) - eBPF 扩展:内核内缓存决策路径避免用户态往返(community RFC 进行中)
在实际生产中,gVisor 的 Gofer 进程、Firecracker 的 seccomp 过滤层,以及 containerd 的 CRI 沙箱实现中,都能看到 Seccomp Notify 核心思想的落地。理解这一机制,不仅是解锁高级容器安全能力的关键,更是深入 Linux 安全子系统设计的必经之路。
参考文档:Linux 6.6 kernel source(kernel/seccomp.c、include/uapi/linux/seccomp.org)、libseccomp 官方文档、Docker security 文档

发表评论 取消回复