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

How do I copy variable length data using bpf_probe_read() in EBPF programs?

我试图通过 tracepoint/syscalls/sys_enter_readtracepoint/syscalls/sys_exit_read 上的 运行 ebpf 程序在 read() 系统调用中转储用户 space 缓冲区的内容。这个想法是在 sys_enter_read tracepoint 期间保存用户缓冲区地址,并且,当 read() returns 时,在 sys_exit_read 内,将用户缓冲区的内容复制到 perf 缓冲区中.所以我正在使用 bpf_probe_read() 从 userspace 缓冲区复制内容。然而,验证者在加载时抱怨。据我了解,验证者希望确保对 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;

    // for the time being, copy to a stack buffer instead of 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程序加载成功。

您需要先初始化 tmp_buffer 数组,然后再将其传递给助手,例如:

char tmp_buffer[128] = {0};

这是因为内核验证程序发现您将此数组传递给了内核助手。验证者不知道助手对缓冲区做了什么:就其所知,它可能是一个向用户 space 发送数据的助手(例如,如果您调用 bpf_trace_printk())。在这种情况下,将未初始化的数据发送给用户 space 会带来安全风险,因为缓冲区中的未初始化数据可能是敏感的和可利用的。

因此,如果您之前没有初始化数据,验证程序会阻止您将分配在 eBPF 堆栈上的缓冲区传递给帮助程序。这就是invalid indirect read from stack R1的意思:R1是传递给helper的第一个参数(即tmp_buffer),读取不正确,因为内存还没有初始化。