为什么使用 BPF 私有栈的可抢占内核可能被 classic uprobe 程序触发崩溃?
简短回答: classic uprobe 调用可以留在同一个 CPU 上,同时仍被抢占。在为符合条件的 BPF 程序按“每程序、每 CPU”分配一份私有栈的 x86 内核中,另一个 task 随后可能在同一 CPU 上运行同一程序,并复用第一个调用的栈。第一个 task 恢复执行后,verifier 已证明为有效指针的 spill register 可能装入无关的运行时数据;后续直接 load 或 map helper 因而会在 JIT 代码中 fault,并导致 kernel panic。
这应被视为执行不变量被破坏,而不是 verifier 接受了无效指针操作的证据。Verifier 检查的是一次抽象执行,其中 spill 会完整恢复。实际失败发生在之后:两次具体执行共享了基于“同一 CPU 上互斥”假设设计的存储。
问题的范围也不是“所有 uprobe 都不安全”。私有栈 eligibility 与 x86 JIT 支持在 Linux 6.13 开发周期进入内核。是否进入风险路径还取决于精确 kernel build、架构、JIT 实现、程序 stack 使用量、uprobe attachment path 与 preemption 行为。发行版回移植会让只看版本号的判断失效。
栈损坏如何发生
BPF 私有栈用于减轻 kernel stack 压力。x86 JIT 为每个 subprogram、每个 CPU 分配私有存储;eligibility 逻辑要求运行时具备 recursion protection,因为每个 CPU 只有一份副本。只有同一程序的两次调用不会重叠使用这份副本时,这个设计才安全。
当前调查中的 classic uprobe 路径破坏了这个假设:
- Task A 在 CPU 0 进入 classic uprobe 程序,开始使用该程序的 CPU-0 私有栈。
- Uprobe runner 调用
migrate_disable(),因此 Task A 不会迁移到其他 CPU,但这本身不会禁止抢占。 - Task A 在寄存器或 verifier 跟踪的指针已经 spill 到私有栈时被抢占。
- Task B 在 CPU 0 运行、命中同一个 uprobe,并调用同一个 BPF 程序。
- Task B 复用并覆盖同一份 per-program、per-CPU 私有栈 slot。
- Task A 恢复后 reload 已损坏的 spill。此时 native code 使用的值不再符合 verifier 证明过的 pointer type。
因此,表面上的崩溃位置可能会误导排障。Fault 可能出现在 BPF map helper、普通 map-value load,或 bad restore 之后的其他 dereference。给某一个应用指针增加 null check 也许能修复一个独立的程序 bug,却不能证明私有栈损坏已经消失。
一个公开 reproducer 具体展示了这一区别:应用侧缺失的 null check 无论如何都应修复;与此同时,JIT 执行还恢复了与 verifier state 不一致的值。另一份公开报告在并发 TLS 与进程活动下复现 panic,KASAN 把无效访问定位到由 uprobe 进入的 BPF 程序。这些现象支持私有栈假设,但巡检时所提议的 kernel fix 尚未被上游接受。
修改生产内核前先确认机制
应收集足够证据,把这个 bug 与普通 offset 错误、过期 userspace ABI 或应用错误区分开:
uname -a
grep -E 'CONFIG_(BPF_JIT|PREEMPT|PREEMPT_DYNAMIC)=' /boot/config-"$(uname -r)"
bpftool prog show
bpftool prog dump xlated id PROG_ID opcodes linum
bpftool prog dump jited id PROG_ID opcodes linum最后一条命令要求调用者能够读取 JIT 指令。保存匹配的 BPF object、BTF、程序 ID/名称、attach type、translated instructions、JIT dump 与精确 kernel build。原始 panic 输出应留在 incident system;除非已经检查其中没有 secret 和私有拓扑,否则不要放进公开 issue 或内容仓库。
然后沿相互独立的维度缩小 workload:
- 只禁用可疑 classic uprobes,其他 BPF 程序保持加载。如果 panic 消失,就能缩小 attachment set,而不是归咎于全部 BPF workload。
- 缩减到一个程序和一个 symbol,再提高同一 CPU 上的并发调用。可疑机制需要同一程序的重叠执行,普通单次测试可能永远不会触发。
- 对比包含私有栈 commits 的精确 kernel build 与不包含它们的 build,或与带候选 kernel fix 的 build。应检查 commit,而不是只比较 release string。
- 在不受影响的内核上单独验证程序自己的 pointer 和 offset handling。程序 bug 与 kernel 执行 bug 可以同时存在。
- 对照 translated instructions 与 native faulting instruction。关键信号是 runtime register value 与 verifier-approved type 不一致,而不仅仅是 fault 发生在某个 helper 中。
PREEMPT 配置只是风险线索,不是完整诊断。反过来,轻载系统无法复现也不能证明安全;interleaving window 可能只是很少出现。
按照要恢复的执行不变量选择缓解措施
最安全的立即缓解措施,是在行为尚未验证的内核上 detach 或禁用受影响的 classic uprobe 程序。如果 instrumentation 是可选项,损失这部分信号也好过危及 host availability。
选择内核时,应使用未激活该私有栈路径的 build,或包含已接受修复并通过并发 reproducer 的 build。简单降到某个 release 并不充分,因为发行版可能回移植私有栈变更。同样,不要未经独立的兼容性、性能与安全评审,就把全局关闭 BPF JIT 当成临时生产 workaround。
两个候选修复说明了正确的不变量,但目前仍是提案:
- kernel-side patch 在 classic uprobe array 的每个真实程序外获取现有 per-program recursion context。如果同一程序已经在该 CPU 上活跃,就增加 missed-invocation counter 并跳过嵌套调用,从而恢复 per-CPU 私有栈的单一使用者假设。
- draft application patch 用 weak
bpf_preempt_disable()和bpf_preempt_enable()kfunc 包住所有可能从 uprobe 进入的程序主体。Tail call 需要特殊处理,因为 verifier 禁止在仍关闭抢占时 return 或 tail-call。只有所有入口、出口与 tail-call path 都覆盖时,这种方法才会阻止第一次调用被抢占。
两份提案都不能称为上游修复。Kernel-side 方案集中维护执行不变量,不要求每个 BPF 项目了解实现细节。Application guard 可以作为项目完全控制相关程序时的有限过渡,但必须在每个支持内核上测试 verifier compatibility 与 runtime cost。
应用任何缓解后,都要重新运行并发 reproducer;如果 kernel fix 暴露 missed-run accounting,还应检查该计数,并执行正常功能与负载测试。“只运行一次没有 panic”不够:还要验证目标 probe 仍能 attach、预期事件没有静默丢失、tail-call path 能在最老的受支持内核上加载。
参考资料
- Linux commit:选择可使用私有栈的 BPF subprogram
- Linux commit:为 x86 JIT 增加 per-subprogram、per-CPU 私有栈
- Linux commit:实现 sleepable uprobe 使用的 program-array path
- 公开 kernel-panic 报告与 reproducer 引用
- 使用 BPF preemption kfunc 的 draft application guard
- 实验性 kernel patch:用 per-program recursion context 保护 classic uprobes
- bpftool 程序检查与 JIT dump 文档
- BPF 邮件列表提案:folio-backed scratch 与 pool allocator
- BPF 邮件列表提案:最大 16 字节 aggregate return value
- Prempti 架构:由 Falco rules 评估 tool-call hook
- ActPlane 架构:面向 agent process tree 的 kernel-level policy enforcement
- Crash-safe eBPF dataplane loader 设计与实现
今日社区讨论
今天通过普通可见浏览器检查了全部 6 个获准社区、共 15 个 allowlist 频道或公开页面,所有目标均可访问。公开论坛使用其普通可见旧版界面完成检查。入选问题来自过去 24 小时,没有使用七天回退。以下内容已删除姓名、账号、雇主、频道身份、消息链接、精确时间、私有拓扑、原始日志与可回搜措辞;没有保留原始 transcript。
Verifier proof 仍依赖 runtime 保持 machine state
当天最强讨论把 host panic 与可抢占、私有栈内核上的 classic uprobe 执行联系起来。实际教训是:verifier acceptance 证明的是 BPF instruction model 的属性;JIT 与 invocation path 必须保持使这份证明成立的 register 与 stack slot。当 runtime 把另一个值恢复进 verifier 跟踪的 pointer register 时,看似安全的 map access 也会变成 native fault。
调查也说明为什么不能把两个 bug 合并成一个。Probe 可能错误处理合法的应用层 null value,而重叠的 kernel invocation 又独立损坏 saved register。两者都应修复,并在隔离的 kernel path 上分别测试;null-check 测试通过不能作为 kernel invariant 已修复的证据。公开 kernel patch 与 application patch 仍是提案,因此保守的运行答案依旧是选择性禁用 probe,或使用经过验证的 kernel build。
Agent guardrail 必须明确覆盖范围与失败语义
第二组讨论比较了 coding agent 的两类 runtime-enforcement layer。一类在工具执行前评估 structured tool request,通过 rule engine 返回 allow、deny 或 ask verdict。它能提供精确的 intent-level policy 与有用的 audit story,却看不到一个已允许 shell command 内部发生的 syscall。另一类沿 process lineage 在 kernel 中执行 file、exec、network、information-flow 与 causal-order rule。它能覆盖间接 subprocess 行为,但需要 Linux capability、kernel support,以及对 block、kill、notify 与 fail-closed 行为的谨慎定义。
这些层是互补关系,不是互相替代。评审应明确 observation point、action point、跨事件 state 如何持久化、policy engine 不可用时发生什么,以及还剩哪些 bypass。Tool hook 提供 high-level context,OS enforcement 提供 effect-level coverage,isolation 则继续承担最终资源边界。
新 BPF 能力正在同时跨越 allocator、ABI 与 verifier 边界
公开开发列表当天有两个特别活跃的提案。Folio-backed bump allocator 计划替代 batch-shaped path 中反复进行的小对象分配,例如 verifier stack-state node 和 generic map-update buffer。它声称能以线性分配与 bulk teardown 降低开销,但生命周期必须真正符合 region 结构:individual ownership、partial free、reclaim pressure 与 fallback object 都要显式处理。该系列仍是带 runtime switch 与 subsystem-specific adoption 的初始提案,应通过 benchmark 和 fault injection 验证,不能当成已经可用的 kernel API。
另一个系列把 kfunc 与 BPF subprogram return 扩展为通过 R0:R2 返回最大 16 字节 aggregate。这不只是 ABI 变更。JIT 必须正确放置第二部分,precision backtracking 与 liveness 必须建模 R2,verifier 必须阻止 pointer provenance 通过 aggregate field 被洗成 scalar,interpreter fallback 也不能静默采用不同语义。提案中的约束——global boundary 只允许 scalar-only aggregate member、kfunc return 需要架构 opt-in、并要求 JIT 执行——展示了扩大 return ABI 所需的 end-to-end contract。
Crash-safe loader 应协调 kernel state,而不是假设进程永远存活
公开论坛出现了一份新的 loader 设计,关注 userspace process 意外退出后的恢复。可复用的模式是:把 pinned object 与 link 视为 observed kernel state,另行保存 desired configuration,并让启动时 reconciliation 保持 idempotent。每个 object 都要有 owner、version 或 generation,以及 terminal cleanup rule;否则重启的 loader 无法区分可复用状态与残留垃圾。
该设计还把 blocking kernel operation 与 async control plane 分离,并在不要求每个 case 都有 live kernel 的情况下测试决策逻辑。帖子尚未出现实质 follow-up,因此更适合作为 implementation proposal,而不是 community consensus。其余聊天与项目专用页面较安静、只有 automated notification,或在当日窗口内没有新的技术问题。
继续阅读
返回索引
eBPF 问答
这里每天收录一个关于 eBPF、Linux 可观测性、性能分析、运行时扩展或安全问题的回答。每篇回答都以公开的一手资料为依据,并在末尾附上匿名化的当日社区讨论摘要。
上一篇 / 上一页
eBPF 程序应如何在多个网络 hook 之间携带每包元数据?
简短回答:目前上游内核没有一个能让 skb 穿过所有网络 hook 时始终携带数据的通用 BPF 暂存区。生产者是 XDP、消费者是 TC ingress 时,应使用 datameta;数据实际属于 flow 或 socket 时,应使用稳定标识作为 key 的 map;skb-cb 只能视为由当前网络层短期拥有的暂存区,不能当作跨协议栈 ABI。正在评审的 BPF skb extension
下一篇 / 下一页
BPF 可扩展调度生效时,cgroup v2 的 cpu.max 仍会限制 CPU 时间吗?
简短回答: 不一定。cpu.max 是 fair scheduler 为其管理的 task 提供的硬带宽控制。当 schedext scheduler 接管一个 task 后,kernel 可以把该 cgroup 的 period、quota 与 burst 值交给 BPF scheduler,但 BPF scheduler 必须自行实现 accounting、throttling 与
- 最后更新
- 2026年8月17日
- 首次发布
- 2026年8月17日
- 贡献者
- Littlefisher619
这个页面有帮助吗?