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

在EBPF程序中如何使用bpf_probe_read()复制可变长度数据?

问题:eBPF跟踪read系统调用时复制可变长度用户缓冲区验证失败如何解决

我尝试通过在tracepoint/syscalls/sys_enter_read和tracepoint/syscalls/sys_exit_read上运行eBPF程序,导出read()系统调用中用户空间缓冲区的内容。实现思路是在sys_enter_read tracepoint触发时保存用户缓冲区地址,待read()返回后,在sys_exit_read的处理逻辑中将用户缓冲区内容复制到perf buffer。我使用bpf_probe_read()从用户空间缓冲区拷贝内容,但程序加载时验证器报错,据我了解是验证器要求size参数必须有明确边界导致加载失败,请问如何实现可变长度数据的正常复制?

本次测试运行在5.13版本内核上,实现代码如下:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <stddef.h>

struct read_exit_ctx {
    unsigned long long unused;
    int __syscall_nr;
    long ret;
};

struct read_enter_ctx {
    unsigned long long unused;
    int __syscall_nr;
    unsigned int padding;
    unsigned long fd;
    char* buf;
    size_t count;
};

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, int);
    __type(value, void*);
} saved_read_ctx SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_read")
int trace_read_enter(struct read_enter_ctx *ctx)
{
    int zero = 0;
    void *p = ctx->buf;
    bpf_map_update_elem(&saved_read_ctx, &zero, &p, BPF_ANY);
    return 0;
}


SEC("tracepoint/syscalls/sys_exit_read")
int trace_read_exit(struct read_exit_ctx *ctx)
{
    char tmp_buffer[128];
    #define KEY_SIZE (sizeof(tmp_buffer) - 1)

    int zero = 0;
    void **ubuf = bpf_map_lookup_elem(&saved_read_ctx, &zero);
    if (!ubuf) {
        return 0;
    }

    if (ctx->ret <= 0) {
        return 0;
    }

    unsigned int ret = ctx->ret;

    // 此处暂时将内容复制到栈缓冲区,而非perf buffer
    bpf_probe_read(tmp_buffer, ret & KEY_SIZE, *ubuf);
    return 0;
}

char _license[] SEC("license") = "GPL";

程序加载报错日志如下:

# ./a.out
libbpf: load bpf program failed: Permission denied
libbpf: -- BEGIN DUMP LOG ---
libbpf:
R1 type=ctx expected=fp
; int trace_read_exit(struct read_exit_ctx *ctx)
0: (bf) r6 = r1
1: (b7) r1 = 0
; int zero = 0;
2: (63) *(u32 *)(r10 -132) = r1
last_idx 2 first_idx 0
regs=2 stack=0 before 1: (b7) r1 = 0
3: (bf) r2 = r10
;
4: (07) r2 += -132
; void **ubuf = bpf_map_lookup_elem(&saved_read_ctx, &zero);
5: (18) r1 = 0xffff920183aa8e00
7: (85) call bpf_map_lookup_elem#1
; if (!ubuf) {
8: (15) if r0 == 0x0 goto pc+8
 R0_w=map_value(id=0,off=0,ks=4,vs=8,imm=0) R6_w=ctx(id=0,off=0,imm=0) R10=fp0 fp-136=mmmm????
; if (ctx->ret <= 0) {
9: (79) r2 = *(u64 *)(r6 +16)
10: (b7) r1 = 1
; if (ctx->ret <= 0) {
11: (6d) if r1 s> r2 goto pc+5
 R0=map_value(id=0,off=0,ks=4,vs=8,imm=0) R1=inv1 R2=inv(id=0,umin_value=1,umax_value=9223372036854775807,var_off=(0x0; 0x7fffffffffffffff)) R6=ctx(id=0,off=0,imm=0) R10=fp0 fp-136=mmmm????
; bpf_probe_read(tmp_buffer, ret & KEY_SIZE, *ubuf);
12: (79) r3 = *(u64 *)(r0 +0)
 R0=map_value(id=0,off=0,ks=4,vs=8,imm=0) R1=inv1 R2=inv(id=0,umin_value=1,umax_value=9223372036854775807,var_off=(0x0; 0x7fffffffffffffff)) R6=ctx(id=0,off=0,imm=0) R10=fp0 fp-136=mmmm????
; bpf_probe_read(tmp_buffer, ret & KEY_SIZE, *ubuf);
13: (57) r2 &= 127
14: (bf) r1 = r10
;
15: (07) r1 += -128
; bpf_probe_read(tmp_buffer, ret & KEY_SIZE, *ubuf);
16: (85) call bpf_probe_read#4
invalid indirect read from stack R1 off -128+0 size 127
processed 16 insns (limit 1000000) max_states_per_insn 0 total_states 1 peak_states 1 mark_read 1

libbpf: -- END LOG --
libbpf: failed to load program 'trace_read_exit'
libbpf: failed to load object './EXE'

编辑补充:我按照评论提示初始化了tmp_buffer[128]数组后,BPF程序成功加载。


解决方案

你遇到的报错本质不是size参数边界的问题,从验证器日志的invalid indirect read from stack R1 off -128+0 size 127信息可以判断,问题出在未初始化的栈缓冲区校验规则上:5.x版本的eBPF验证器要求所有被写入的栈空间必须先完成初始化,避免栈内残留的内核数据泄露。

直接修复方案

你已经验证了初始化栈缓冲区的有效性,仅需要在声明缓冲区时完成初始化即可:

char tmp_buffer[128] = {0};

初始化后验证器会识别到整个128字节的栈空间都已被合法初始化,允许最大127字节的写入操作,即可通过校验。

可变长度数据读取的优化实现

如果需要读取超过栈大小的缓冲区内容,或者解决现有逻辑的并发缺陷,可以参考以下优化点:

  • 不要用单元素数组存储用户缓冲区地址,单元素数组在多CPU并发场景下会出现地址覆盖问题,改用BPF_MAP_TYPE_TASK_STORAGE或者BPF_MAP_TYPE_HASH以pid/tid作为key存储上下文,避免并发冲突。
  • 读取用户空间内容如果是字符串场景,改用bpf_probe_read_user_str()更合适;普通二进制数据场景用bpf_probe_read_user()配合min(ret, 缓冲区最大长度)做边界裁剪,比按位与的写法可读性更高。
  • 5.13版本内核已经支持BPF环形缓冲区(BPF_RINGBUF),相比perf buffer更适合传输可变长度数据,不需要提前在内核态分配固定大小的缓冲区,用户态读取效率也更高。
  • 更高版本内核(5.18+)可以直接用bpf_dynptr_from_user()系列接口处理动态长度数据,不需要提前分配固定大小的缓冲区,验证器也能自动完成边界校验。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 05:48:03