开源安全代理项目 bomfather/agent 近期公布了一组极为扎眼的优化数据:通过在 eBPF LSM 拦截链路中引入基于 Inode 的缓存机制,单次文件打开时的内核 CPU 消耗直接减少了约 89.18%。在微基准测试中,内核运行周期从 280 亿次骤降至 30.3 亿次,整整省下了近九成的算力。
性能的飞跃若脱离了严谨的语义根基,往往只是把致命代价延后结算。
这个数字足够抓人眼球,以至于不少开发者惊呼找到了 eBPF 性能瓶颈的银弹。但只要把测试代码和 Linux 虚拟文件系统的底层实现摊开来看,就会发现这并非无代价的突破,而是一场建立在极端微基准测试上的数字戏法。
慢路径之痛与 10,000 槽位的 LRU 缓存
在 Linux 5.18 之后,基于 eBPF LSM 的访问控制被广泛应用在容器防护中。每当进程触发 file_open 或 security_file_permission 系统调用,安全钩子就会在内核空间就地阻断越权。

但问题在于:企业下发的防护策略几乎全部是基于文件路径的(比如禁止访问 /var/lib/postgres 下的敏感文件),而内核传给钩子函数的却只有具体的 dentry 和 inode 对象。为了判定当前操作是否合规,Agent 只能沿着 dentry->d_parent 一级一级往上爬,重构出完整路径,再与预设的策略前缀挨个比对。
这种慢路径的遍历成本极高。bomfather/agent 团队为了摆脱每次都要逐层遍历的负担,在 BPF 内部设计了一张容量为 10,000 的 BPF_MAP_TYPE_LRU_HASH 缓存表。
他们将访问凭证哈希化,组合了三个核心要素:挂载命名空间 ID(mntns_id)、挂载点 ID(mount_id)以及文件节点的 inode。一旦这个组合命中了缓存,程序就直接读取预先计算好的策略掩码位,跳过耗时的目录爬梳。
在作者展示的火焰图里,is_restricted_filepath 和 path_check_callback 这两个过去吃掉 81.9% 与 63.7% 栈采样的重度函数,在缓存生效后断崖式暴跌至约 0.02%,几乎从火焰图上直接抹去。
20 万次单文件重复打开:跑分幻象与真实世界
九成降幅的结论看似无懈可击,但只要审视其测试方法,就能看出巨大的认知落差。

作者采用的基准测试,是在单线程内对同一个绝对路径连续重复打开 200,000 次。在这套极端偏向热缓存的设计下,除了第 1 次属于缓存未命中,其余 199,999 次全部在极小的哈希表里一击即中。
但在生产系统的真实工作负载中,文件系统从不是这般风平浪静。无论是真实的数据库存取、编译流程还是容器启停,系统调用面临的是大量的临时文件生成、树状元数据扫描和多进程并发。
作者并未给出包含冷启动的测试,更缺乏真实应用场景下的端到端响应延迟与整体吞吐量变化。在缓存频繁发生驱逐换出的真实生产环境,这 90% 的红利还能剩下多少,需要打上巨大的问号。
- 提醒.脱离多进程并发与频繁换出的单文件循环微基准,测出的只是理论峰值上限,无法代表实际应用拦截吞吐。
致命错位:用 Inode 对象做路径策略的内核死穴
比跑分脱离现实更严重的,是安全防护的语义错位。

在 Linux 虚拟文件系统的体系中,inode 属于底层存储对象维度,而路径则属于目录拓扑与命名空间维度。一个 inode 可以在不同的目录树中以不同路径存在,这在语义上根本不是一一对应的映射关系。
作者意识到了硬链接引发的别名问题:当两个完全不同的路径指向同一个底层物理文件时,原有的路径安全策略就会被串联。因此他们加入了兜底逻辑:只要读取到 i_nlink != 1,就主动放弃缓存走慢路径。
但这仅仅堵住了最显眼的一个浅坑,文件系统的拓扑突变远比硬链接复杂:
- 目录重命名断代当一个上级目录被执行
rename()时,底层的所有文件inode毫发无损,但其对外的访问路径已发生剧烈形变。由于缺乏主动的主动拓扑失效机制,原先被禁止访问的敏感文件,在重命名后可能因为缓存未刷新而产生策略逃逸或误杀。 - 挂载视图混淆虽然加入了
mount_id校验,但在未开启内核较新特性mnt_id_unique的老版本内核中,ID 回收复用并不罕见;叠加 bind mount、overlayfs 的 copy-up 机制,单纯的哈希很难完整刻画复杂的挂载树。 - 节点编号复用当临时文件被频繁创建、删除(
unlink)后,底层文件系统会迅速复用刚释放的inode编号。若 LRU 表未能感知到删除事件,新的同编号文件就会错误继承旧文件的策略判断。
- 风险.缺少针对 rename、unlink 和挂载事件的拓扑代际失效链条,单纯缓存 Inode 相当于在瞬息万变的文件系统之上搭建空中楼阁,极易引入内核级安全漏洞。
路线之争:为什么成熟项目不抄近道?
面对内核遍历的高额开销,云原生安全领域的成熟项目难道不知道引入哈希表吗?事实恰恰相反,主流项目的克制正是出于对安全边界的敬畏。

| 项目 / 方案 | 路径处理机制 | 缓存策略 | 安全一致性代价 |
|---|---|---|---|
| bomfather/agent | 沿 dentry 逐级重构完整路径 | Inode LRU 缓存(拦截阻断) | 高:目录重命名与节点复用易致策略逃逸 |
| Tetragon | 4KiB per-CPU 缓冲区重构路径 | 无路径决策缓存(直接计算) | 低:每次调用保证绝对一致,但占用内核栈与内存 |
| KubeArmor | 内核态展开最多 20 层深度比对 | 逐级遍历(硬编码展开) | 低:承受较高计算开销以换取策略语义严密 |
| Tracee | 结合 device/inode/ctime 缓存 | 仅用于审计观测(不用于阻断) | 中:明确放弃拦截,避免陈旧缓存导致误判 |
| Landlock | 对象级原生规则关联(无路径重构) | 内核原生对象树继承遍历 | 极低:从架构上彻底废除字符串重构 |
作为云原生安全基石的 Tetragon,选择分配 4KiB 的 per-CPU 缓冲区,在内核态老老实实完成严密的路径重构;KubeArmor 则宁愿在 eBPF 中展开多达 20 层的循环进行逐层前缀匹配,也不敢贸然引入 Inode 缓存;而 Tracee 虽然尝试过记录设备号、Inode 和状态改变时间(ctime)的缓存,却极其严格地将其限制在可观测性数据分析中,坚决不参与线上同步阻断。
各大开源项目不是做不出九成的基准测试降幅,而是心知肚明:在拦截路径上,一致性与正确性永远高于单纯的执行速度。
真正代表架构演进方向的,是内核原生的 Landlock。它完全摒弃了在运行时重构路径字符串的旧思路,将访问控制规则直接挂载在内核原生的目录与文件对象内部,伴随文件系统的内部拓扑完成天然继承与鉴权。
把路径策略寄托在瞬息万变的 Inode 编号上,看似轻巧省力,实则是用放弃严密性换取局部的指标狂欢。在内核安全防护的深水区里,只要语义不闭合,任何走近道的提速方案,最终都会以另一种更危险的形式付出代价。
