深夜的 Exit Code 159:当“按需生成”的 Seccomp 白名单遭遇 rseq 系统调用与 Falco 绞杀

凌晨3点被报警砸醒,核心交易集群爆发大规模 CrashLoopBackOff,大盘 QPS 呈断崖式下跌。快速拉取 Pod 状态,退出码清一色是 159。结论先行:安全团队在生产环境强推的所谓“零信任” Seccomp 白名单,漏掉了 rseq(Restartable Sequences)和 clone3 系统调用,导致高并发下底层 glibc/Go runtime 被内核直接发送 SIGSYS 斩首;更荒唐的是,排查期间我试图 kubectl exec 进容器抓 strace,直接触发了 Falco 的“防入侵联动”,把排查节点给 Cordon(不可调度)了。

这是一起典型的、脱离一线业务真实运行机制的安全事故。所谓的“安全左移”,绝不能以牺牲系统可用性为代价。

诡异的 159 退出码与案发现场

业务线同学反馈:代码没动,配置没动,只有流量上来时 Pod 会随机暴毙。 查看 K8s 事件,没有任何 OOMKilled 的迹象,只有冰冷的退出码:

$ kubectl get pod -n prod-core
NAME                              READY   STATUS             RESTARTS   AGE
trade-engine-7f89c4d5b-x2k9q      0/1     CrashLoopBackOff   12         15m
trade-engine-7f89c4d5b-z8m2a      0/1     CrashLoopBackOff   15         15m

$ kubectl describe pod trade-engine-7f89c4d5b-x2k9q | grep -A 5 "State:"
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       Error
      Exit Code:    159

Exit Code 159,稍微有点底层经验的人看到这个数字立刻就能反应过来:128 + 31 = 159。 在 Linux 中,Signal 31 是 SIGSYS(Bad system call)。这意味着进程尝试调用了一个内核不认识、或者被安全机制强行阻断的系统调用。

直接切到宿主机,翻看内核日志:

$ dmesg -T | grep audit | tail -n 3
[Wed May 15 03:12:45 2024] audit: type=1326 audit(1715713965.123:4567): auid=4294967295 uid=1000 gid=1000 ses=4294967295 subj=unconfined pid=14325 comm="trade-engine" exe="/app/trade-engine" sig=31 arch=c000003e syscall=334 compat=0 ip=0x7f8a9b8c7d6e code=0x0
[Wed May 15 03:12:47 2024] audit: type=1326 audit(1715713967.890:4568): auid=4294967295 uid=1000 gid=1000 ses=4294967295 subj=unconfined pid=14388 comm="trade-engine" exe="/app/trade-engine" sig=31 arch=c000003e syscall=435 compat=0 ip=0x7f8a9b8c7e10 code=0x0

重点看两个数字:syscall=334syscall=435,架构是 arch=c000003e(x86_64)。 用 ausyscall 翻译一下:

$ ausyscall x86_64 334
rseq
$ ausyscall x86_64 435
clone3

愚蠢的安全策略:当沙盒变成绞肉机

为什么会突然拦截 rseqclone3? 查阅变更记录,安全团队在凌晨2点通过 Kyverno MutatingWebhook 给所有 namespace 强制注入了一个严格的 Seccomp Profile。

这帮天才为了做到所谓的“最小权限”,使用了一款基于 eBPF 的动态追踪工具,在测试环境跑了 5 分钟业务,把这 5 分钟内捕获到的系统调用抓出来,直接生成了白名单。

为什么这种做法极其致命?

  1. 并发场景下的特有调用rseq(Restartable Sequences)是 Linux 4.18 引入的特性,现代 glibc (>=2.35) 和 Go (>=1.19) 极度依赖它来实现无锁的 per-CPU 数据结构。在测试环境几 QPS 的负载下,线程根本不需要激烈竞争,运行时可能不会高频触发 rseq 相关路径;而到了生产环境的万级 QPS,底层 Runtime 一旦触发 rseq,直接撞在 Seccomp 的墙上。

  2. 致命的 Default Action:生成工具极其愚蠢地将默认拦截动作设置为了 SCMP_ACT_KILL_THREAD

{
  "defaultAction": "SCMP_ACT_KILL_THREAD",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": ["epoll_pwait", "futex", "read", "write", "..."],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

如果是防御性编程思维,遇到不在白名单的 Syscall,正确的阻断动作应该是 SCMP_ACT_ERRNO(配合返回 ENOSYS)。 如果配置为 ERRNO,当 glibc 调用 clone3rseq 被拒时,它会优雅地收到 ENOSYS(系统调用未实现),然后 Fallback(降级) 到老版本的 clone 或传统的加锁机制,业务顶多性能掉一点,绝对不会崩溃。 但配成 KILL_THREAD,内核连说话的机会都不给,直接一刀把线程砍了,进程当场暴毙(SIGSYS)。

Falco 绞杀:排查过程中的二次伤害

为了现场验证,我试图 kubectl exec 进其中一个还在 CrashLoop 边缘挣扎的 Pod,想挂个 strace 看下具体是哪段代码触发的:

$ kubectl exec -it trade-engine-7f89c4d5b-x2k9q -n prod-core -- /bin/bash

刚敲下回车,命令卡死。紧接着,监控大盘上该 Pod 所在的整个 Node 直接变成了 SchedulingDisabled,Node 上其余 40 多个正常 Pod 开始被强行驱逐(Evicted)!

看了一眼系统安全群,机器人正在疯狂报警: [Falco Alert] Critical: Terminal shell in container detected. Pod: trade-engine... Rule: Terminal shell in container. Action: Webhook triggered -> Cordon & Drain Node.

我特么当时血压就上来了。 安全团队部署的 Falco 规则引擎,配置了极度激进的 SOAR(安全编排自动化响应)。他们监测到 exec /bin/bash 操作,不分青红皂白(不区分发起方是 CI/CD、未知 IP 还是具有集群 admin 权限的 SRE 堡垒机),直接调用自建的 Webhook 把宿主机给 Cordon 并 Drain 了。

这种缺乏上下文联动、且具有毁灭性控制平面权限的“自动化防御”,在生产环境就是一颗随时引爆的定时炸弹。如果黑客发现了这个规则,只需要伪造请求批量触发报警,就能利用你们自己的安全工具,把你们的生产集群主动瘫痪掉。

技术结论与避坑建议

直接停用 Kyverno webhook 恢复生产后,我给安全团队扔了复盘报告。容器运行时安全不是拿着开源扫描器生成个 JSON 就能上生产的,必须遵循以下底线:

  1. Seccomp 的平滑落地法则 永远不要在生产环境直接上 SCMP_ACT_KILL。第一阶段必须是 SCMP_ACT_LOG,跑满一个完整的业务高峰期,通过分析 dmesg 审计日志收集全量 Syscall。

  2. 正确理解 ENOSYS 的降级语义 拦截未知的现代系统调用(如 clone3, bpf, rseq),强烈建议将 Action 设置为 SCMP_ACT_ERRNO 并返回 ENOSYS。这符合 POSIX 标准,能让大多数现代编程语言的 Runtime 平滑降级到旧版系统调用,避免 SIGSYS 导致的血案。

  3. AppArmor / Falco 联动的爆炸半径控制 安全告警(Detection)和阻断(Enforcement)必须解耦。Falco 检测到异常 shell,可以告警,可以打 Tag,甚至可以隔离特定的 Pod 网络(NetworkPolicy),但绝不允许直接越权调用 K8s API 执行 Node 级别的破坏性操作(Cordon/Drain)。防御系统的权限必须遵循最小化原则。

同类问题速查清单 (Troubleshooting Checklist)

  1. 如何确认是 Seccomp 导致的 SIGSYS?
  2. 检查 Pod 退出码是否为 159
  3. 登录所在 Node,执行 dmesg -T | grep audit | grep sig=31

  4. 如何翻译 Audit 日志中的 Syscall ID?

  5. 查看日志中的 syscall=XXXarch=XXX
  6. 使用 auditd 工具包翻译:ausyscall x86_64 (如 ausyscall x86_64 435 -> clone3)。

  7. 如何排查 Falco Webhook 导致的集群异常抖动?

  8. 检查 Falco 日志:journalctl -u falco | grep "Notice" 或查看 Falco-sidecar 日志。
  9. 检查 K8s 审计日志 (kube-apiserver audit log),过滤发出 Cordon/Evict 请求的 ServiceAccount,确认是否为安全告警组件的联动行为。

  10. 高频被遗漏的基础系统调用有哪些?

  11. rseq (334), clone3 (435), prctl (157 – 经常被 Java 线程管理需要), statx (332 – 很多新版 DB driver 依赖)。编写白名单时需格外留意。