BPF 重定向到邻居的辅助函数为什么会在以太网头很短的包上触发内核 BUG,修复又为什么在接收路径和发送路径上读取真实头部长度要使用不同的字段?
因为 skb->mac_len 只在接收路径上被填充。一个用 mac_len 是否小于 ETH_HLEN 为 bpf_redirect_neigh() 的 pull 做保护的修复,在 ingress 上是安全的;但在发送路径上——本地 socket 生成、经 tc egress 出去的包,或经 netkit 转发的包——每个 skb 的 mac_len 都是 0,所以同一个保护会把这些路径上所有合法的 redirect-neighbor 流量静默丢掉。在两条路径上都有效的头部长度字段是 skb_network_offset(),即从 skb->data 到 network_header 的距离;修正后的修复改为从这个字段读取长度。
机制
-
Bug 本体。
bpf_redirect_neigh()经由skb_do_redirect()进入net/core/filter.c的__bpf_redirect_neigh()。原代码是ethh = eth_hdr(skb),只检查了mac_header低于network_header,然后调用skb_pull(skb, sizeof(*ethh))——即一次 14 字节ETH_HLEN的 pull。__skb_pull()执行skb->len -= len后,若在线性部分下溢就BUG()。VLAN pop-eth 之后,一个特权 tc 程序可以把 skb 的有效 MAC 头缩到 1 字节、线性 headroom 只剩 1 字节(复现器是bpf_skb_change_head(skb, 1, 0)),再调用bpf_redirect_neigh();旧检查通过,14 字节 pull 触发 BUG。 -
v1 修复与它的回归。修复在 pull 前加保护、其后重读
ethh:当mac_len小于ETH_HLEN、或pskb_may_pull()无法把头部线性化时丢弃,然后ethh = eth_hdr(skb);。pskb_may_pull()不 panic 地线性化头部,mac_len检查丢掉真正过短的包。但skb->mac_len只在接收路径被写入,由__netif_receive_skb_core()调用skb_reset_mac_len()把它设为network_header减mac_header。发送路径没有任何地方写mac_len;__dev_queue_xmit()和dev_loopback_xmit()只调用skb_reset_mac_header()而不调用skb_reset_mac_len(),所以本地生成的包携带mac_len == 0。在 tc egress 和 netkit 上,0 < ETH_HLEN恒为真,每个合法的 redirect-neighbor skb 都会落到丢弃路径(kfree_skb、-ENOTSUPP)——一个也会进入 stable 的回归。 -
与路径无关的修复。
__bpf_redirect_neigh()运行时,两条路径上skb->data都停在 L2 头起始处:ingress 上sch_handle_ingress()在skb_do_redirect()前 push 了mac_len字节;egress 上dev_hard_header()已 push 好 L2 头,__dev_queue_xmit()把mac_header设到那个偏移。所以真实的 L2 头长度就是network_header减data,恰好是skb_network_offset()。修正后的保护在skb_network_offset()小于ETH_HLEN、或pskb_may_pull()无法线性化时丢弃;它仍拒绝来自bpf_skb_change_head的 1 字节头,在mac_len为 0 的 egress 与 netkit 上同样正确。 -
内核本来就清楚这点。
act_mirred.c在 ingress 取mac_len、其余路径取skb_network_offset()——它早就把 ingress 之外的mac_len视为不可靠。而bpf_redirect_neigh的 selftest 全部挂在 tc ingress,那里mac_len有效,因此抓不到 egress 或 netkit 的回归。
验证与排障路径
-
看哪个字段是 0。在 egress 或 netkit 路径上,观察包上
mac_len(0)与skb_network_offset()(真实 L2 头长度),看哪个匹配真实头部。egress 路径从不调用skb_reset_mac_len,所以mac_len按构造就是 0。 -
确定性复现原 BUG。在 tc ingress 挂一个特权 SCHED_CLS 程序,做 VLAN pop-eth,调用
bpf_skb_change_head(skb, 1, 0)再调用bpf_redirect_neigh();在未修复的内核上,__skb_pull()内部的skb->len下溢检查会触发。 -
证明回归。把同一个 redirect-neighbor 程序挂在 tc egress 上处理本地 socket 生成的包,或经 netkit 转发,观察 v1 的
mac_len保护以-ENOTSUPP丢掉该包;skb_network_offset()版本则保留它。
局限
- 该保护仍会丢掉任何 L2 头短于
ETH_HLEN的 skb;这是有意为之,不是缺陷。 pskb_may_pull()可能重新分配skb->head,所以修复在其后重读eth_hdr(skb),而不是复用保护前取的指针。mac_header高于network_header是另一种失败模式,与头部过短不同;skb_network_offset()覆盖的是过短的情况,两者不是同一个检查。
References
- Kernel
net/core/filter.c—__bpf_redirect_neigh与原始的skb_pull(ETH_HLEN)路径。 - Kernel
include/linux/skbuff.h—skb_reset_mac_len、skb_reset_mac_header、skb_network_offset、__skb_pull。 - Kernel
net/sched/act_mirred.c— ingress 取mac_len、其余路径取skb_network_offset()的既有写法。 - Kernel
include/uapi/linux/if_ether.h—ETH_HLEN(14)与ETH_ALEN(6)。 - Commit
b4ab31414970("bpf: Add redirect_neigh helper as redirect drop-in") — redirect-neighbor 辅助函数的Fixes:目标。 - Public bpf mailing-list patch thread: fix
skb_pull()BUG in__bpf_redirect_neigh()— 公开的补丁与 egress/netkit 回归评审。
当日社区讨论
一封公开的 bpf 列表补丁修复了 __bpf_redirect_neigh() 里的 skb_pull() BUG:特权 tc 程序在 VLAN pop-eth 之后把 MAC 头缩短即可触发。第一版用 mac_len 是否小于 ETH_HLEN 来保护 pull,但自动评审发现 mac_len 只在接收路径上被设置,在 tc egress 与 netkit 上恒为 0,这个保护会丢掉那些路径上所有合法的 redirect-neighbor 流量;线程达成共识,后续版本改为从 skb_network_offset() 读取长度,该字段在两条路径上都有效。目前的讨论范围很窄:边界在哪一个字段承载每条路径的 L2 头长度,而现有的 redirect-neighbor selftest 全部挂在 ingress,因此抓不到 egress 的回归。
本次运行的存档来源在 7 天窗口内产出 0 条消息(两个 opt-in 存档来源在快照时为空),因此上面的问题取自被允许的公开 bpf 邮件列表,经其公开的 atom feed 审阅,补丁线程作为一手参考。eunomia-bpf 与 sched-ext 的 Discord 频道以及 r/eBPF 在本次运行未覆盖(没有可用的可见浏览器会话),故标记为未覆盖而非安静。
继续阅读
返回索引
eBPF 问答
这里每天收录一个关于 eBPF、Linux 可观测性、性能分析、运行时扩展或安全问题的回答。每篇回答都以公开的一手资料为依据,并在末尾附上匿名化的当日社区讨论摘要。
上一篇 / 上一页
为什么不可睡眠的 BPF fentry 与 fexit 程序会悄悄跳过同一 CPU 上其他任务的调用,被跳过的调用又为什么会记成递归丢失?
因为 trampoline 的递归保护是 per-CPU 的,而且是在 preemption 仍然开启的状态下获取的。不可睡眠的 fentry/fexit 程序经由 BPF trampoline 执行,其入口路径会递增 prog-active,一个 per-CPU 计数器,而它是在 rcureadlockdontmigrate() 下获取的。这个锁只禁用了 CPU 迁移,并没有禁用
下一篇 / 下一页
为什么 iocost 的 cgroup 代价模型文件只能在内建模型的 auto 与 user 系数集之间切换,而 BPF struct ops 代价模型接管设备之后,这个文件为什么不再选择模型?
因为 cgroup 文件是内建模型的接口,而 BPF 程序是带外挂载的。blk-iocost 的 cgroup 代价模型文件的写路径解析 ctrl= 令牌(auto 或 user)、model= 令牌(恒为 linear)和七个线性系数令牌,并翻转 usercostmodel 位;它没有任何可运行的程序。公开的补丁系列把一个 BPF struct ops 实现绑定到某个设备的 iocost 实例上,经由 struct ops link
- 最后更新
- 2026年10月28日
- 首次发布
- 2026年10月28日
- 贡献者
- Yunwei
这个页面有帮助吗?