抹茶发现 netfilter userspace library "libnftnl" 的 examples/nft-set-elem-add.c 跑不起来,看 syscall 也看不懂,毕竟 netlink msg 谁™能看懂?
一起痛骂 “libnftnl 是什么垃圾” 和 “你的 7.2 内核太新了,新内核全是 bug 很合理” 之后,我也好奇起来了,netlink syscall 报错溯因对我来说也是个谜,正好学习一下,而且这周工作写文档太恶心了想裸奔。
经过一堆准备工作之后:
1. 修改代码让它变成一个“启动暂停、 ctrl-c 继续运行” 的两阶段程序,延长进程生命方便我用 pid 过滤事件
2. 下载 linux-image-$(uname -r)-dbgsym 方便映射符号
3. 准备 @eBPFTalk001 的 bpfsnoop 方便用 lbr 回溯内核执行流
4. 准备 brendangregg/perf-tools 方便用 funcgraph
5. 下载 Ubuntu-hwe-6.17-6.17.0-35.35_24.04.1 源码
神秘的 Linux 内核系列又回来啦!
首先要找个合适的回溯点。直接从 __x64_sys_sendto syscall 回溯 lbr 得不到太多信息,全是 kfree_skb 之类的清理,所以找找看 netfilter netlink error 相关的 kprobe,结果还真找到一个:
然后用 lbr 检查内核是如何执行到这里的:
还有另一种思路,看整个 syscall 的 funcgraph,也能看到 nfnl_err_add 和完整的执行流
两份 trace log 一结合,立刻能知道是在 nft_data_init() 里返回了 -EINVAL,然后用 lbr 跳转记录仔细跟一下源码,看到这一条跳转:
对应的源码是
看懂了吧,len != desc->len 所以 return -EINVAL,看下 desc->len 是 nft_set->klen 由 nft table 的 set type 决定的长度,比如我是用
定义的 ipv4_addr type, desc->len = klen = 4;但是 nft-set-elem-add.c 里传给 netlink 的 uint16_t data 长度是 2,所以内核检查不通过,返回 -EINVAL,QED。
你以为我很高兴吗,不,我很悲伤,因为我其实一开始就用 LLM 解答万物了,把 Linux 源码 + nft-set-elem-add.c 源码 + strace 日志扔给 codex5.5 medium,它只用了三十秒就找到了问题;然后我自己再用上面这些眼花缭乱的 tracing 手段去追溯源码,用了一个小时。投降了,已皈依 LLM 神教饶我狗命🐕
一起痛骂 “libnftnl 是什么垃圾” 和 “你的 7.2 内核太新了,新内核全是 bug 很合理” 之后,我也好奇起来了,netlink syscall 报错溯因对我来说也是个谜,正好学习一下,而且这周工作写文档太恶心了想裸奔。
经过一堆准备工作之后:
1. 修改代码让它变成一个“启动暂停、 ctrl-c 继续运行” 的两阶段程序,延长进程生命方便我用 pid 过滤事件
2. 下载 linux-image-$(uname -r)-dbgsym 方便映射符号
3. 准备 @eBPFTalk001 的 bpfsnoop 方便用 lbr 回溯内核执行流
4. 准备 brendangregg/perf-tools 方便用 funcgraph
5. 下载 Ubuntu-hwe-6.17-6.17.0-35.35_24.04.1 源码
神秘的 Linux 内核系列又回来啦!
首先要找个合适的回溯点。直接从 __x64_sys_sendto syscall 回溯 lbr 得不到太多信息,全是 kfree_skb 之类的清理,所以找找看 netfilter netlink error 相关的 kprobe,结果还真找到一个:
$ bpftrace -p $(pidof nft-set-elem-add) -e 'k:*nf*err* {printf("%s\n", probe);}'
Attached 11 probes
kprobe:nfnl_err_add然后用 lbr 检查内核是如何执行到这里的:
$ ./bpfsnoop -k nfnl_err_add --output-lbr --filter-pid $(pidof nft-set-elem-add) --mode entry
__nla_validate_parse+0xb6 (lib/nlattr.c:655) -> __nla_parse+0x23 (lib/nlattr.c:734)
__nla_parse+0x35 (lib/nlattr.c:734) -> nft_data_init+0x77 (net/netfilter/nf_tables_api.c:11894)
nft_data_init+0xae (net/netfilter/nf_tables_api.c:11847) -> nft_data_init+0x115 (net/netfilter/nf_tables_api.c:11890)
nft_data_init+0x11a (net/netfilter/nf_tables_api.c:11890) -> nft_data_init+0xba (net/netfilter/nf_tables_api.c:11912)
nft_data_init+0xe0 (net/netfilter/nf_tables_api.c:11912) -> nft_add_set_elem+0x2be (net/netfilter/nf_tables_api.c:7380) 还有另一种思路,看整个 syscall 的 funcgraph,也能看到 nfnl_err_add 和完整的执行流
$ ./funcgraph -p $(pidof nft-set-elem-add) -m 50 __x64_sys_sendto
6) | nf_tables_newsetelem [nf_tables]() {
6) 0.634 us | nft_set_lookup_global [nf_tables]();
6) | nft_add_set_elem [nf_tables]() {
6) 0.586 us | nft_data_init [nf_tables]();
6) 1.659 us | }
6) 7.547 us | }
6) | nfnl_err_add [nfnetlink]() {
6) | __kmalloc_cache_noprof() {
6) 0.106 us | __cond_resched();
6) 1.109 us | }
6) 1.444 us | }两份 trace log 一结合,立刻能知道是在 nft_data_init() 里返回了 -EINVAL,然后用 lbr 跳转记录仔细跟一下源码,看到这一条跳转:
nft_data_init+0xae (net/netfilter/nf_tables_api.c:11847) -> nft_data_init+0x115 (net/netfilter/nf_tables_api.c:11890)对应的源码是
11846 if (desc->len) {
11847 if (len != desc->len)
11848 return -EINVAL;
[...]
11890 return -EINVAL;
[...]看懂了吧,len != desc->len 所以 return -EINVAL,看下 desc->len 是 nft_set->klen 由 nft table 的 set type 决定的长度,比如我是用
nft add set ip t s '{ type ipv4_addr; }'定义的 ipv4_addr type, desc->len = klen = 4;但是 nft-set-elem-add.c 里传给 netlink 的 uint16_t data 长度是 2,所以内核检查不通过,返回 -EINVAL,QED。
你以为我很高兴吗,不,我很悲伤,因为我其实一开始就用 LLM 解答万物了,把 Linux 源码 + nft-set-elem-add.c 源码 + strace 日志扔给 codex5.5 medium,它只用了三十秒就找到了问题;然后我自己再用上面这些眼花缭乱的 tracing 手段去追溯源码,用了一个小时。投降了,已皈依 LLM 神教饶我狗命