基于ebpf-go加载探测NFSd函数的eBPF程序时遇到BPF验证器权限错误的求助
Great question! 你的推测完全正确——问题核心确实出在nfsd模块的BTF信息没有被eBPF程序正确识别和关联上,尤其是struct nfsd4_compound_state这类属于nfsd内核模块而非vmlinux的结构体。下面我来拆解问题原因,并给出常规的解决方法:
为什么会出现这些问题?
1. R2 !read_ok权限错误的根源
当你挂载fentry探针到nfsd4_write时,内核的BPF验证器会拿着你的程序里的参数类型,和内核中实际nfsd4_write函数的参数类型做严格比对。如果你的程序里的struct nfsd4_compound_state定义(来自nfsd-btf.h)没被验证器正确关联到nfsd模块的BTF信息上,它就会觉得第二个参数cstate(也就是错误里的R2)的内存是不安全的,直接给你打回票,报权限拒绝。
2. UID值异常的原因
访问rqstp时输出的UID全是乱码,是因为struct svc_rqst虽然在vmlinux中有定义,但nfsd模块对这个结构体做了模块特有的扩展,而你用bpftool生成的vmlinux.h只包含了内核核心部分的定义,和nfsd模块里实际的svc_rqst结构偏移不匹配,导致BPF_CORE_READ读错了内存位置。
解决步骤:正确加载模块BTF并关联类型
针对内核模块(比如nfsd)特有的结构体,加载eBPF程序时必须让内核验证器能获取到模块的BTF信息,常规操作分为以下几步:
1. 确保模块已加载,头文件未被修改
首先确认nfsd模块已经在运行:
lsmod | grep nfsd
同时,不要手动修改bpftool生成的vmlinux.h和nfsd-btf.h——手动修改会破坏结构体的内存偏移,直接导致类型匹配失败。
2. 用BPF CO-RE显式关联模块类型
BPF CO-RE(一次编译,到处运行)提供了宏来帮你关联模块的BTF类型,避免偏移不匹配。在你的eBPF C代码中添加类型关联:
#include "nfsd-btf.h" #include "vmlinux.h" #include <bpf/bpf_core_read.h> #include <bpf/bpf_helpers.h> // 显式告诉BPF验证器,这些结构体来自nfsd模块 BPF_CORE_TYPE(struct nfsd4_compound_state, "nfsd"); BPF_CORE_TYPE(struct svc_rqst, "nfsd"); SEC("fentry/nfsd4_write") int write_ops(struct svc_rqst *rqstp, struct nfsd4_compound_state *cstate, union nfsd4_op_u *u) { // 先做空指针检查,避免验证器报错 if (!rqstp || !cstate) { return 0; } // 用BPF_CORE_READ安全读取嵌套字段 u32 uid = BPF_CORE_READ(rqstp, rq_cred.cr_uid.val); struct dentry *dentry = BPF_CORE_READ(cstate, current_fh.fh_dentry); if (!dentry) { return 0; } const char *parent_name = BPF_CORE_READ(dentry, d_parent, d_name, name); const char *file_name = BPF_CORE_READ(dentry, d_name, name); u64 ino = BPF_CORE_READ(dentry, d_inode, i_ino); bpf_printk("UID: %u, Write to Inode: %lu, Filename: %s/%s\n", uid, ino, parent_name, file_name); return 0; }
3. 编译时保留BTF信息
编译eBPF程序时,确保clang保留BTF调试信息,这样内核验证器才能正确解析类型:
clang -target bpf -D__TARGET_ARCH_x86_64 -I. -O2 -g -c your_program.bpf.c -o your_program.bpf.o
(-g参数是关键,它会生成BTF信息嵌入到目标文件中)
4. 在ebpf-go中启用BTF自动加载
修改你的Go代码,在加载eBPF对象时配置BTFOptions,让ebpf-go自动处理模块BTF的关联:
import ( "log" "time" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf" ) func main() { var objs collectorObjects opts := ebpf.CollectionOptions{ BTF: &ebpf.BTFOptions{ LogLevel: ebpf.BTFLogLevelDebug, // 可选,开启后能看到BTF加载的调试日志 }, } if err := loadCollectorObjects(&objs, &opts); err != nil { log.Fatal("Loading eBPF objects:", err) } defer objs.Close() link, err := link.AttachTracing(link.TracingOptions{ Program: objs.WriteOps, }) if err != nil { log.Fatal("Attaching Fentry:", err) } defer link.Close() log.Printf("Program Loaded!\n") time.Sleep(10*time.Seconds) }
调试技巧:如果还是报错怎么办?
如果调整后依然有问题,可以用这些方法排查:
- 查看nfsd模块的BTF原始信息,确认
nfsd4_write的参数类型:bpftool btf dump file /sys/kernel/btf/nfsd | grep -A 10 nfsd4_write - 查看内核日志,BPF验证器会输出更详细的错误细节:
dmesg -w - 编译后用
bpftool查看eBPF程序的字节码,分析验证器报错的具体位置:bpftool prog dump xlated obj your_program.bpf.o
总结
当探测的内核函数参数包含模块特有的结构体时,核心要点是:
- 正确生成并保留模块的BTF头文件;
- 用BPF CO-RE的类型关联机制让内核验证器识别模块类型;
- 加载时启用BTF自动处理。
按照以上步骤调整后,你的eBPF程序应该能正常加载,并正确获取NFSd的写入信息了。
内容的提问来源于stack exchange,提问作者Luigui1729

