
1. seccomp 到底解决了什么问题第一次在生产环境里被 seccomp 绊倒是因为一个跑在容器里的服务莫名其妙开始返回 EPERM日志里连堆栈都没有。折腾了半天才发现是运行时的默认 seccomp 配置把我们内部一个依赖unshare做隔离的小模块给卡死了。那次之后我才认真把 seccomp 从头到尾捋了一遍——它不是那种配一下就行的东西你如果不知道它在哪一层拦截、拦截之后表现成什么样子排查成本会高得离谱。seccomp 是 Linux 内核提供的一套系统调用过滤机制全称 secure computing mode。它的核心思路非常朴素把一个进程能发起的系统调用限制在一个明确的集合里出了集合的调用要么被拒绝要么被直接干掉进程。系统调用是用户态程序和内核打交道的唯一入口把入口收窄等于把一个被攻破的进程能做的事情砍到最低限度。所以它最常见的使用场景是沙箱、容器运行时、浏览器渲染进程、代码执行服务这类代码不完全可信的地方。这篇内容适合三类人看一是在做容器、沙箱、Serverless 这类隔离基础设施需要给运行时加一层防护的工程师二是遇到容器里系统调用被拒、想搞清楚到底是谁在拦的开发三是单纯想理解 Linux 安全机制怎么从内核原语一步步落到生产配置上的技术爱好者。我会从手写第一段 BPF 过滤器开始到生产环境怎么画像、怎么切白名单、怎么灰度上线再到踩过的坑和排查手册一路写下来。全程是实操向能直接抄的部分我会给完整代码。要强调的是seccomp 不是万能钥匙。它只约束系统调用这一个维度管不了文件权限、管不了网络策略、管不了资源耗尽。它更像一道门槛很低的闸机装起来不费事但装错了会把自己的腿夹住。2. 动手之前必须搞清楚的底层机制2.1 三种模式其实常用的只有一种内核里的 seccomp 一共有三种运作模式通过prctl或者seccomp系统调用切换。严格模式SECCOMP_MODE_STRICT是最早的形态限制极其粗暴进程只能调用read、write、_exit和sigreturn这四个。注意是_exit而不是exit_group多线程程序在里面连正常退出都费劲。这个模式基本只适合做极小型的计算沙箱实际生产里很少直接用。过滤模式SECCOMP_MODE_FILTER才是真正被广泛使用的那一个。它允许你挂一个 BPF 程序上去每次系统调用发生前内核会先跑一遍这个程序根据返回值决定放行、拒绝还是杀掉进程。这个 BPF 是经典 BPFcBPF不是现在 eBPF 那一套指令集简单得多但限制也更多。日志模式严格来说不算独立模式而是过滤模式的一种动作——你可以让某个系统调用照常执行但往内核审计日志里记一笔。这个在灰度阶段极其有用后面会专门讲。切换模式的时候有个绕不开的前置条件加载过滤器要求当前线程设置no_new_privs或者持有CAP_SYS_ADMIN。绝大多数场景下我们选前者因为它不需要特权普通用户也能用。no_new_privs一旦设置就不可撤销它的含义是这个进程及其后代在执行execve之后不能获得比现在更多的权限——也就是说 setuid 二进制对它们失效了。这个语义正好和 seccomp 配套因为如果进程能通过执行 setuid 程序提权那过滤器就形同虚设。# 看一眼当前 shell 有没有加载过滤器 grep -E Seccomp|NoNewPrivs /proc/self/status # Seccomp: 0 - 没启用 # Seccomp: 2 - 过滤模式 # Seccomp_filters: 1 - 挂了几个过滤器注意Seccomp_filters这个字段是 Linux 4.9 之后才有的老内核上看不到。排查老环境时要靠别的手段。2.2 为什么必须检查 arch 字段第一次手写过滤器的时候我很自然地只判断了系统调用号跑起来也没问题。直到有人提醒我在 x86-64 上进程完全可以切到 32 位模式执行代码那时候走的是 i386 的系统调用表编号和 x86-64 完全不同。如果你只按 x86-64 的编号做判断攻击者只要用 32 位 ABI 发起调用就能绕过整个过滤器。这就是为什么几乎所有靠谱的 seccomp 过滤器第一条指令都是读seccomp_data.arch比对不上就直接杀进程。struct seccomp_data的结构长这样struct seccomp_data { int nr; /* 系统调用号 */ __u32 arch; /* 架构标识如 AUDIT_ARCH_X86_64 */ __u64 instruction_pointer; /* 触发调用的指令地址 */ __u64 args[6]; /* 六个参数 */ };除了 archinstruction_pointer偶尔会被用来做更细的校验但因为它和地址空间布局强相关做可移植的过滤器时基本不用。真正会用到的是args当你需要允许socket但不允许创建某些协议族这种参数级控制时就得把 64 位参数拆成高低两个 32 位来比。2.3 经典 BPF 的三个硬限制写 seccomp 过滤器时必须接受三个事实它们会直接影响你的方案设计。第一每个过滤器的指令数上限是 4096 条。白名单方式给几百个系统调用各写一条看起来离上限很远但如果你还要对参数逐个比对指令数会迅速膨胀。我见过一个项目为了精确控制ioctl的请求码光这一条规则就写了两百多条指令。真要突破这个数量级只能拆成多个过滤器叠加——因为Seccomp_filters是可以大于 1 的多个过滤器是全部都要通过的关系。第二累加器是 32 位的。经典 BPF 里只有 A 和 X 两个寄存器都是 32 位。想比较一个 64 位参数只能先读低 32 位、再读高 32 位用跳转组合起来判断。这写起来很啰嗦也是很多人转向 libseccomp 的直接原因。第三动作返回值只有 16 位数据位。SECCOMP_RET_*的高 16 位是动作类型低 16 位是附带数据用SECCOMP_RET_DATA这个掩码取。比如SECCOMP_RET_ERRNO | (EPERM SECCOMP_RET_DATA)就是让这个系统调用直接返回 EPERM。千万别忘了和掩码做与运算否则返回值会变成一个内核不认识的怪东西行为不可预期。动作的优先级是固定的内核会从所有匹配的规则里挑级别最高的那个执行顺序大致是杀进程 杀线程 触发 SIGSYS 返回 errno 用户态接管 ptrace 接管 记日志 放行。3. 手写第一个 seccomp 过滤器3.1 从二十行代码开始光看文档容易飘直接写一个能跑的最小例子最实在。下面这段代码的作用是只拦unshare这一个系统调用让它返回 EPERM其余全部放行。#define _GNU_SOURCE #include stdio.h #include stdlib.h #include unistd.h #include errno.h #include stddef.h #include sys/prctl.h #include sys/syscall.h #include linux/seccomp.h #include linux/filter.h #include linux/audit.h #define MY_ARCH AUDIT_ARCH_X86_64 static int install_filter(void) { struct sock_filter filter[] { /* 1. 先校验架构不匹配直接杀 */ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, MY_ARCH, 1, 0), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS), /* 2. 读系统调用号 */ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), /* 3. 命中 unshare 就返回 EPERM否则跳过 */ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_unshare, 0, 1), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM SECCOMP_RET_DATA)), /* 4. 兜底放行 */ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), }; struct sock_fprog prog { .len (unsigned short)(sizeof(filter) / sizeof(filter[0])), .filter filter, }; if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror(prctl(PR_SET_NO_NEW_PRIVS)); return -1; } if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, prog)) { perror(prctl(PR_SET_SECCOMP)); return -1; } return 0; } int main(void) { if (install_filter() ! 0) return 1; /* 这里应该失败errno 为 EPERM */ if (unshare(CLONE_NEWNS) -1) { perror(unshare); } printf(still alive, filter is working\n); return 0; }编译运行gcc -O2 -o seccomp-demo seccomp-demo.c ./seccomp-demo # unshare: Operation not permitted # still alive, filter is working3.2 逐条拆解这段 BPF很多人第一次看BPF_JUMP(..., 1, 0)这种写法是懵的1和0到底是跳几条。规则是条件为真时跳过jt条指令继续执行为假时跳过jf条指令继续执行。这里的跳过 N 条是相对当前指令的下一条开始数跳 0 就是执行紧接着的下一句。所以第二条指令BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, MY_ARCH, 1, 0)的意思是如果 arch 相等跳过下一条也就是跳过那个 KILL直接去读系统调用号不相等就往下走执行 KILL。这个真跳过、假落下的写法在 seccomp 里出现频率特别高因为它能把异常处理紧贴在判断后面读起来顺一点。第三条KILL_PROCESS我故意用了杀进程而不是返回错误。架构不匹配这种情况正常程序根本不会触发一旦触发就说明有人在尝试换 ABI 绕过这种时候没有任何理由让它继续跑。SECCOMP_RET_KILL_PROCESS和SECCOMP_RET_KILL_THREAD的区别值得记一下前者从 Linux 4.14 开始提供整个线程组一起死后者老名字就是SECCOMP_RET_KILL只杀掉触发调用的那个线程同一个进程里的其他线程还活着。多线程服务里如果不想出现半个进程还活着的诡异状态用 KILL_PROCESS 更省心。3.3 NO_NEW_PRIVS 和 TSYNC两个必须理解清楚的开关PR_SET_NO_NEW_PRIVS这个调用看起来像是额外负担但它是 seccomp 能在无特权场景下使用的关键。内核的逻辑是只有在你保证这个进程不会通过 execve 获得额外权限的前提下才允许你给它加过滤器否则过滤器可能被一个 setuid 程序绕过。设置之后不可撤销而且会被子进程继承。如果你的服务本身需要执行 setuid 的辅助程序那就得走CAP_SYS_ADMIN那条路代价是要么给容器特权要么精细地只给这一个 capability。另一个开关是SECCOMP_FILTER_FLAG_TSYNC。默认情况下prctl(PR_SET_SECCOMP, ...)只给调用它的那一个线程装过滤器其他线程不受影响。如果程序是多线程的而你在某个线程里装了过滤器就会出现同一进程内部分线程受约束、部分不受约束的割裂状态这种漏洞非常隐蔽。TSYNC 的作用是把过滤器同步到线程组里的所有线程如果其中有线程已经装过不兼容的过滤器调用会返回 EINVAL 而不是静默失败。/* 用 seccomp() 系统调用配合 TSYNC 标志 */ int rc syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, prog); if (rc -1 errno EINVAL) { /* 说明有线程带着不兼容的过滤器需要先统一 */ perror(seccomp TSYNC); }实操心得如果你打算在启动早期就装过滤器最好在创建任何线程之前完成。这样既不用处理 TSYNC 的兼容性问题也能保证所有后续线程天然继承过滤规则。我现在的做法是在main函数最开头、任何pthread_create之前就把过滤器装好。4. 生产落地从系统调用画像到白名单4.1 先画像再动刀给已有服务加 seccomp最容易翻车的做法是凭经验列一个白名单。人对系统调用的直觉几乎必然是错的——你以为一个简单的 HTTP 服务只会用read/write实际上它可能因为 DNS、NSS、locale、日志库、随机数种子而在启动阶段调用了上百个系统调用。正确的姿势是先画像。用strace把服务在一段时间内的系统调用全量抓下来按频次和名称统计# 跟住所有子进程统计各类系统调用的次数 strace -f -c -o trace-summary.txt ./your-service # 想要更细的把完整调用名抓下来去重 strace -f -e traceall -o trace-full.txt ./your-service awk -F( {print $1} trace-full.txt | awk {print $NF} | sort -u syscalls.txt这里有几个关键点必须注意否则画像结果是残缺的一定要跟子进程-f。很多服务会 fork 出 worker漏掉子进程等于漏掉一大半调用面。一定要覆盖完整生命周期。启动、热加载、优雅退出、错误分支走的路完全不同。只抓稳态运行的十分钟你会漏掉execve、restart_syscall、rt_sigreturn这些一次性但必需的调用。一定要覆盖异常路径。如果服务有降级逻辑比如主链路失败后去读本地缓存文件这条路径平时根本不会跑上线后一旦触发就被 seccomp 拦死。我一般的做法是抓三类场景正常请求压测一轮、触发一次配置热加载、触发一次优雅重启。三轮合起来的系统调用集合才是白名单的起点。4.2 白名单和黑名单怎么选这是 seccomp 落地绕不开的决策。两种思路的差别不是风格问题而是安全模型的差别。维度白名单默认拒绝黑名单默认放行默认动作SCMP_ACT_ERRNO/SCMP_ACT_KILLSCMP_ACT_ALLOW安全强度高新出现的攻击面默认被封低只能封已知危险调用维护成本高每次依赖升级都可能要加规则低基本不用动上线风险高漏一个调用就报 EPERM低误伤概率小适用场景容器运行时、代码沙箱、暴露面大的服务内部服务、快速加一层兜底如果你做的是多租户的代码执行环境没有任何理由用黑名单——用户代码能调用的系统调用应该是极其有限的一个子集白名单是唯一合理的选择。反过来如果你只是想给一个内部服务加一层防护黑名单拦掉ptrace、kexec_load、bpf、userfaultfd、perf_event_open这类东西性价比更高也不容易出事。现实中更常见的是混合策略以白名单为骨架但对确实难以枚举的调用比如ioctl、fcntl用参数级规则或直接放行同时接受这部分残余风险。4.3 用 libseccomp 把可读性拉回来手写 BPF 适合理解原理不适合维护。真到生产环境基本都会用 libseccomp它把系统调用名、参数比较、多架构处理都封装好了。#include seccomp.h #include errno.h #include stdio.h int main(void) { /* 默认动作设为拒绝返回 EPERM */ scmp_filter_ctx ctx seccomp_init(SCMP_ACT_ERRNO(EPERM)); if (!ctx) return 1; /* 明确声明允许的架构顺带处理 32 位兼容 */ seccomp_arch_add(ctx, SCMP_ARCH_X86_64); seccomp_arch_remove(ctx, SCMP_ARCH_NATIVE); /* 视情况调整 */ seccomp_arch_add(ctx, SCMP_ARCH_X86); /* 基础 IO */ 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(fstat), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(lseek), 0); /* 内存管理 */ seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mprotect), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0); /* 线程与同步 */ seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(futex), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(clone), 0); /* 想观察某个调用但不拦截用 LOG */ seccomp_rule_add(ctx, SCMP_ACT_LOG, SCMP_SYS(socket), 0); if (seccomp_load(ctx) ! 0) { perror(seccomp_load); seccomp_release(ctx); return 1; } seccomp_release(ctx); /* 剩下的业务代码 */ return 0; }编译的时候记得链接gcc -O2 -o svc svc.c -lseccompseccomp_rule_add的第四个参数是参数比较规则的数量传 0 表示不比较参数。后面那句seccomp_arch_add(ctx, SCMP_ARCH_X86)是很多人会漏掉的如果进程可能执行 32 位代码必须显式把 32 位架构也加进去libseccomp 会为每个架构生成对应的规则。不加的话32 位调用会落到默认动作上——如果你默认是 KILL那会直接崩如果你忘了设默认动作行为会更混乱。避坑提醒seccomp_arch_remove(ctx, SCMP_ARCH_NATIVE)和seccomp_arch_add的顺序会影响最终规则集。我的习惯是先把所有需要的架构加进去再删掉不需要的最后调seccomp_export_bpf导出来看一眼确认生成的指令数量和架构覆盖符合预期。4.4 参数级过滤别只看调用号只判断调用号的过滤器有个明显短板socket是合法的但socket(AF_NETLINK, ...)和socket(AF_PACKET, ...)在很多沙箱里就不该被允许。libseccomp 支持参数级比较写法是这样/* 允许 IPv4 TCP其他协议族一律拒绝 */ seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, AF_INET)); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, AF_INET6));SCMP_A0到SCMP_A5对应六个参数SCMP_CMP_EQ、SCMP_CMP_NE、SCMP_CMP_LT、SCMP_CMP_MASKED_EQ是比较方式。这里有个非常重要的细节libseccomp 的规则匹配是任意一条匹配规则生效而不是最具体的规则优先。也就是说上面这两条加上默认的 ERRNOsocket(AF_NETLINK, ...)会落到默认动作被拒绝这正是我们要的。但如果你先写了一条不带参数的SCMP_ACT_ALLOW, SCMP_SYS(socket), 0那参数级规则就永远不会被触发了——因为无参数规则放行了一切。参数比较也不是免费的。每加一条参数规则生成的 BPF 指令都会增加而且因为 64 位参数要拆成两次读取一条参数规则通常要消耗好几条指令。我实测过把一个两百条白名单的过滤器全部加上参数比较之后指令数从一千多涨到了三千多逼近 4096 的上限。所以参数级过滤要挑重点用别贪。4.5 灰度节奏LOG 先行ERRNO 跟进KILL 兜底给线上服务加 seccomp最忌讳一步到位直接上 KILL。我的标准节奏分三步第一步全量 LOG。把默认动作设成SCMP_ACT_LOG或者对不在白名单里的调用记日志但不拦截。跑一到两周把日志里出现的意外调用全部收进白名单。这一步的产出是你知道自己的服务到底用了哪些系统调用而且覆盖了真实流量下的所有分支。第二步切 ERRNO。把默认动作换成SCMP_ACT_ERRNO(EPERM)。这时候被拦的调用会让业务代码看到一个明确的错误码而不是进程直接消失。观察监控里的 EPERM 错误率如果某个调用被频繁命中要么是白名单漏了要么是业务逻辑真的有问题——两种情况都需要人来看一眼。第三步对真正的危险调用上 KILL。白名单之外的调用返回 EPERM而像ptrace、process_vm_readv、kexec_load、bpf、userfaultfd这几个直接SCMP_ACT_KILL_PROCESS。这类调用正常业务代码永远不该碰一旦出现基本可以判定是攻击行为没有给错误码的必要。/* 危险调用直接杀 */ seccomp_rule_add(ctx, SCMP_ACT_KILL_PROCESS, SCMP_SYS(ptrace), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL_PROCESS, SCMP_SYS(process_vm_readv), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL_PROCESS, SCMP_SYS(process_vm_writev), 0); seccomp_rule_add(ctx, SCMP_ACT_KILL_PROCESS, SCMP_SYS(kexec_load), 0);这套节奏跑下来绝大多数服务在两到三周内可以从 LOG 走到稳定状态。急着一天上线也行但要接受凌晨被告警叫醒的概率。5. 踩坑记录与排查速查手册5.1 症状、原因、处置对照表下面这张表是我这两年攒下来的按症状查基本能覆盖八成问题。现象常见原因处置方式进程莫名消失日志无堆栈命中 KILLSIGSYS 无处理函数临时把默认动作改 LOG复现后看审计日志某个系统调用固定返回 EPERM白名单漏了该调用或参数比较写反用strace -f抓失败点确认是哪个调用收到 SIGSYS 信号命中SECCOMP_RET_TRAP注册 SIGSYS handler读si_syscall字段多线程服务行为不一致过滤器只装在了单线程上改用SECCOMP_FILTER_FLAG_TSYNC32 位程序绕过限制未校验 arch 或未添加 32 位架构加 arch 检查libseccomp 里补SCMP_ARCH_X86execve 后过滤器失效执行了 setuid 程序且未设no_new_privs设置PR_SET_NO_NEW_PRIVS或避免 setuid容器启动报 EPERM运行时默认 profile 拦截用--security-opt seccompunconfined对比验证过滤器加载返回 EINVALTSYNC 遇到不兼容的已有过滤器统一在启动最早阶段安装避免分散加载规则数没超但加载失败生成的 BPF 指令超过 4096拆成多个过滤器或减少参数级规则抓 SIGSYS 信息的写法大概是这样配合SECCOMP_RET_TRAP用#include signal.h #include sys/syscall.h static void sigsys_handler(int sig, siginfo_t *info, void *ctx) { (void)sig; (void)ctx; /* si_syscall 是被拦下的系统调用号 */ fprintf(stderr, blocked syscall: %d\n, info-si_syscall); _exit(128 SIGSYS); } /* 注册方式 */ struct sigaction sa { .sa_sigaction sigsys_handler, .sa_flags SA_SIGINFO, }; sigaction(SIGSYS, sa, NULL);5.2 容器运行时里的 seccomp 长什么样大部分人是通过容器第一次接触 seccomp 的所以这一层必须讲清楚。Docker 和 containerd 都自带一份默认 profile默认动作是SCMP_ACT_ERRNO里面有一张很长的白名单把大约三百多个常规系统调用放行另外有一小撮被显式禁止还额外挡掉了若干带参数的危险调用。这份默认 profile 的存在感极强因为它的默认动作是拒绝。你如果往容器里塞一个用了冷门系统调用的程序报错就是 EPERM而容器日志里可能什么线索都没有。定位方式很简单# 排除法先关掉 seccomp 跑一次 docker run --security-opt seccompunconfined your-image # 如果关掉就好了说明是 seccomp 拦的 # 导出自定义 profile 再逐条加回来生产环境上正确做法是导出默认 profile把你的程序需要但被拒的调用加进白名单然后用自定义 profile 启动而不是长期用unconfined。在 Kubernetes 里对应的是securityContext.seccompProfile1.25 之后默认走 RuntimeDefault也就是说默认就是开着的这一点很多人不知道。apiVersion: v1 kind: Pod spec: securityContext: seccompProfile: type: Localhost localhostProfile: profiles/app-profile.json containers: - name: app image: your-image:latest自定义 profile 的 JSON 结构不复杂核心就是defaultAction加一组syscalls数组每个元素包含names、action需要的话再加args。我一般会从默认 profile 拷一份出来改而不是从零写因为默认 profile 里那些参数级规则的细节很容易被漏掉。5.3 几个只有踩过才知道的细节过滤器是不可撤销的。一旦seccomp_load成功当前进程再没有任何办法摘掉它子进程还会继承。这意味着你的调试窗口只在加载之前。所以我现在的习惯是加一个环境变量开关开发环境默认不装需要验证的时候手动打开。execve会保留过滤器但有例外。正常情况下过滤器跟着进程走fork、clone、execve都不会丢。但如果执行的是一个带 setuid 或 setgid 位的二进制而且进程没有设置no_new_privs内核会把过滤器清掉。这个行为是从安全角度设计的但它确实会让人困惑——同一个程序直接跑没问题被 setuid 包装一层就出事了。CLONE_NEWUSER的坑。很多沙箱方案会先创建一个用户命名空间然后在里面装 seccomp 过滤器。这么做的问题是在用户命名空间里加载过滤器需要额外的谨慎处理而且容器运行时的默认 profile 通常会直接把clone带CLONE_NEWUSER标志的调用挡掉。如果你的方案依赖这个得提前和运行时的配置对齐。性能开销别过度担心但要测。过滤器是在每次系统调用路径上执行的代价是固定的 BPF 求值。指令数在一千以内的过滤器实测对吞吐的影响基本淹没在噪声里。真正影响性能的往往不是 seccomp 本身而是被 ERRNO 拦掉之后业务代码的重试风暴。所以性能验证的重点应该放在有没有被误拦导致的异常重试而不是纠结 seccomp 加了几个纳秒。规则顺序不决定优先级。前面提过一次这里再强调libseccomp 里规则是任一匹配即生效但如果你有两条动作不同的规则同时匹配同一个调用比如一条 ALLOW、一条 ERRNO最终生效的是内核定义的动作优先级而不是你写的顺序。避免这种歧义最好的办法就是别写冲突规则白名单里出现过的调用不要再写一条拒绝规则。5.4 一个真实的排查过程最后分享一次完整的排查感觉比任何文档都有用。现象是一个 Go 写的服务在容器里跑几小时后会有极低概率出现请求超时日志里没有任何错误。第一步先在容器里看 seccomp 状态Seccomp: 2确实开着。第二步把默认动作临时改成 LOG导出审计日志。第三步在日志里 grepSECCOMP发现有一类futex调用被拦了——但只拦了一部分因为 Go 运行时绝大多数 futex 调用都是白名单里的类型。第四步看具体参数是被拦的那种 futex 带了一个很少见的操作码只在 GC 的某个特定阶段出现。结论是默认 profile 里对futex的参数白名单不完整覆盖了常见操作码漏了 GC 场景下的那一个。解决办法是往自定义 profile 里补一条针对该操作码的规则。整个过程最难的不是修是意识到偶发超时和系统调用被拦之间有关联——毕竟从现象上看这两者八竿子打不着。这件事给我留下的经验是只要容器里跑的东西对延迟敏感而现象又是偶发性的就该去审计日志里翻一眼 seccomp。被 ERRNO 拦掉的调用通常会被上层包装成一个普通的错误加上重试逻辑之后就变成了偶发变慢最容易被忽略。