You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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
    

总结

当探测的内核函数参数包含模块特有的结构体时,核心要点是:

  1. 正确生成并保留模块的BTF头文件;
  2. 用BPF CO-RE的类型关联机制让内核验证器识别模块类型;
  3. 加载时启用BTF自动处理。

按照以上步骤调整后,你的eBPF程序应该能正常加载,并正确获取NFSd的写入信息了。

内容的提问来源于stack exchange,提问作者Luigui1729

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 12:28:12