在EBPF程序中如何使用bpf_probe_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

