导航

eBPF dynptr动态指针

发布时间:2 个月前 更新时间:2 months ago
Linux bpf

概述

dynptr(Dynamic Pointer) 是Verifier层面的概念。该指针指向了一个包含有内存信息元数据的结构体struct bpf_dynptr,它主要解决了在进行安全检查时,难以静态证明安全性的指针操作。

例如,当我们需要向通过Reserve系列辅助函数申请的内存空间中写入一个分段数据时非常的有用。在一般情况下,我们会通过结构体的字段偏移去进行一个写入,这在能够确定数据大小,数据偏移地址时是有效的,Verifier不会出现错误提示。但是,当我们无法确定一个准确的数据结构时,需要根据传入的数据长度和数据偏移地址进行写入时,会出现问题。因为前文的方式明确的声明了写入数据的大小,偏移地址,这是一个静态确定的值。

当我们需要根据运行时的数据,来写入数据时,这是一个动态的情况,无法确定写入边界,如果运行时写入的数据,超过了通过Reserve系列函数申请的内存空间边界的话,会是一个非常严重的事故,即在Linux内核堆栈中产生了溢出。

所以,dynptr的概念就是用以解决上述问题的概念。

struct bpf_dynptr

当我们通过 vmlinux.h 头文件去查看struct bpf_dynptr 结构体时,我们会看到如下内容:

struct bpf_dynptr {
	__u64 __opaque[2];
};

该结构体使用了 __opaque 去进行了一个占位,该占位表示的意思是不透明的,意思该结构体中的字段不可在源码编译时可见,你只需要按照__u64 __opaque[2]规定的,传入一个 128 bit,的数据即可,其中它的实现你不必关心,这里不必慌张,bpf_dynptr真正的结构体为 bpf_dynptr_kernel,在我们将该结构体传递给一个指针时函数内部会进行一次强制转换。

现在让我们来看看真实的结构体:

struct bpf_dynptr_kern {
	void *data;
	u32 size;
	u32 offset;
};
  • void *data:一个指向Linux内核堆栈空间的,存储实际数据的指针。
  • u32 size:指定该数据的大小。
  • u32 offset:指定当前写入的偏移量。

在上文中,我们已经提到了,确定写入数据需要静态确定三个东西:

  • 可写入的最长数据
  • 写入数据的大小
  • 写入数据在内存中的偏移地址

常用的dynptr操作

bpf_dynptr_from_mem

创建一个dynptr通过给定的map值或全局变量,它的作用就是我们可以在任意的地方进行一个运行时不确定大小、偏移量的内存写入操作。它是后续所有返回dynptr的底层操作。

函数原型如下:

static long (* const bpf_dynptr_from_mem)(void *data, __u32 size, __u64 flags, struct bpf_dynptr *ptr) = (void *) 197;

形参:

  • void *data:必须是一个指向map值或全局变量的指针
  • __u32 size:该数据的大小,最大大小由 DYNPTR_MAX_SIZE 常量定义
  • __u64 flags:未使用,传入0即可
  • struct bpf_dynptr *ptr:我们需要先声明一个结构体

返回值:

  • 0:成功
  • -E2BIG:超出最大数据大小
  • -EINVAL:如果flags不为0

bpf_dynptr_read

从指定的 dynptr 中读取指定len的数据到dst中,读取的数据从src的offset处开始。

函数原型:

static long (* const bpf_dynptr_read)(void *dst, __u32 len, const struct bpf_dynptr *src, __u32 offset, __u64 flags) = (void *) 201;

形参:

  • void *dst:目标缓冲区地址
  • __u32 len:读取数据的长度
  • const struct bpf_dynptr *src:传入的 dynptr
  • __u32 offset:从指定的偏移处开始
  • __u64 flags:未使用,传入0即可

返回值:

  • 0:成功
  • -E2BIG:如果offset + len超过了 src 的数据长度
  • -EINVAL:src 是一个无效的指针或flags不为0

bpf_dynptr_write

从指定的 src 处读取len长度的数据,写入到dynptr指定的offset处。

函数原型:

static long (* const bpf_dynptr_write)(const struct bpf_dynptr *dst, __u32 offset, void *src, __u32 len, __u64 flags) = (void *) 202;

形参:

  • const struct bpf_dynptr *dst:dynptr指针
  • __u32 offset:dst 的偏移地址
  • void *src:读取数据的指针
  • __u32 len:读取的数据长度
  • __u64 flags:除非是skb类型的dynptr,否则该参数为 0

对于skb类型的dynptr: _在bpf_dynptr_write()之后,dynptr的所有数据片都会自动失效。这是因为写入可能会拉出skb并更改底层数据包缓冲区。

_  For _flags_, please see the flags accepted by        **bpf_skb_store_bytes**().

返回值:

  • 0:成功
  • -E2BIG:如果offset + len超过了 src 的数据长度
  • -EINVAL
    1. src 是一个无效的指针
    2. dst 是一个只读dynptr
    3. flags不正确
  • skb类型的dynptr,返回的错误与bpf_skb_store_bytes()一致

bpf_dynptr_data

该辅助函数的作用是返回一个指向dynptr指向的底层数据的指针,说人话就是帮我们进行指针运算。但是使用该函数有几个要求:

  • len必须是一个在编写时就确定的值,使用这种方式其实和不使用dynptr是一样的,但是dynptr提供了这种互转的能力。需要注意的是,返回的指针不是全局可用的,dynptr结构我们一般在栈中去存放,所以通过dynptr取出的底层数据指针的生命也与dynptr一致,切忌不可在尾调用时或者与其它eBPF程序共享时传递。
  • skb和xdp类型的dynptr不能使用该辅助函数去获取底层数据指针。它们应该使用bpf_dynptr_slicebpf_dynptr_slice_rdwr

函数原型:

static void *(* const bpf_dynptr_data)(const struct bpf_dynptr *ptr, __u32 offset, __u32 len) = (void *) 203;

形参:

  • const struct bpf_dynptr *ptr:dynptr指针
  • __u32 offset:需要返回的指针的偏移量
  • __u32 len:该指针指向的数据的长度

返回值:

  • 指向底层数据的指针
  • NULL
    1. dynptr 是只读的
    2. dynptr 无效
    3. offset 和 len 超出了界限

bpf_ringbuf_reserve_dynptr

这个函数属于是dynptr应用场景最多的情况,我们一般都是通过 ringbuf 去传递不定长数据、有序的数据、不确定数据结构的数据。而该函数返回一个指向了ringbuf预留内存空间的dynptr。当然,当然你也可以先预留,然后再使用上面的 bpf_dynptr_from_mem,它只是对这个步骤的一个封装。

函数原型:

static long (* const bpf_ringbuf_reserve_dynptr)(void *ringbuf, __u32 size, __u64 flags, struct bpf_dynptr *ptr) = (void *) 198;

形参:

  • void *ringbuf:指向环形缓冲区的指针,操作环形缓冲区必须要该指针,该指针不可用以写入数据,只能够销毁、提交、扩容缓冲区使用。
  • __u32 size:预留的缓冲区大小。
  • __u64 flags:必须为0。
  • struct bpf_dynptr *ptr:返回的 dynptr 指针。

返回值:

  • 0:成功
  • !0:失败,-开头的错误参数常量。

需要注意的是,无论该ringbuf预留失败与否,在失败时你需要调用bpf_ringbuf_discard_dynptr()去销毁ringbuf,在预留成功时,你写入数据完成后,也需要调用bpf_ringbuf_submit_dynptr()去提交ringbuf。就算在不使用dynptr ringbuf的途中也是如此,都必须明确写明,否则将无法通过 Verifier 的验证。

bpf_dynptr_adjust

该辅助函数用以调整dynptr的一些元数据。

函数原型:

int bpf_dynptr_adjust(const struct bpf_dynptr *p, u32 start, u32 end)

形参:

  • const struct bpf_dynptr *p:dynptr的指针。
  • u32 start:对应 offset 字段。
  • u32 end:end 对应size字段,如果end小于size,那么将缩小dynptr。注意,缩小并不是释放dynptr所指向的底层数据的内存空间,而是你只能够使用那么多。数据并不会被销毁。如果不想调整,但是这个值又不能够为0,可以通过下面的bpf_dynptr_size辅助函数,来填入默认size大小。

bpf_dynptr_size

在概述部分我们就已经得知,因为__opaque声明,我们对struct bpf_dynptr结构体内部字段是不透明的,无法直接使用.的方式去访问size字段,所以dynptr机制提供了该辅助函数,当我们需要用到dynptr的size字段时,可以通过该辅助函数进行获取。

函数原型:

__u32 bpf_dynptr_size(const struct bpf_dynptr *p)

形参:

  • const struct bpf_dynptr *p:指向 dynptr 的指针

返回值:

  • __u32:该 dynptr 的 size 字段内容

演示

下面是一个不恰当的案例,使用dynptr应该是在不确定offset和len的情况下,但如果是那种情况需要配合用户态应用程序来进行演示,太过于麻烦,但是基本使用就是这样,需要注意的点我都有提到,下面的演示中也很好的诠释了这一点,你可以使用任意的用户态eBPF SDK去加载该程序。

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

struct
{
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 1024 * 1024);
} rb SEC(".maps");

struct event_struct
{
    __u32 syscall_id;
    __u64 pid_tgid;
    __u64 uid_gid;
} __attribute__((packed));

SEC("tracepoint/raw_syscalls/sys_enter")
int trace_sys_enter(struct trace_event_raw_sys_enter *ctx)
{
    struct bpf_dynptr dynptr;

    int ret;

    if (ctx->id != 59)
        return 0;
        
    __u64 pid_tgid = bpf_get_current_pid_tgid();
    __u64 uid_gid = bpf_get_current_uid_gid();
    __u32 syscall_id = ctx->id;

    // 尝试预留。如果失败,立即丢弃并返回。
    // 这里的关键是针对失败情况**显式地**调用 discard。
    ret = bpf_ringbuf_reserve_dynptr(&rb, sizeof(struct event_struct), 0, &dynptr);
    if (ret != 0)
    {
        bpf_printk("BPF: Failed to reserve ringbuf, ret: %d\n", ret);
        // 即使 reserve 返回错误,验证器也希望对 dynptr 变量进行 discard。
        // 这使得 reserve 失败的路径变得明确。
        bpf_ringbuf_discard_dynptr(&dynptr, 0); // 针对失败路径进行 discard
        return 0;                               // 丢弃后立即返回
    }

    // 下面所有的write操作,都必须判断是否成功,并显式声明是否进行错误处理
    if (bpf_dynptr_write(&dynptr, 0, &(syscall_id), sizeof(__u64), 0)) {
        bpf_ringbuf_discard_dynptr(&dynptr, 0); // 针对写入失败进行 discard
        return 0;
    }

    if (bpf_dynptr_write(&dynptr, offsetof(struct event_struct, pid_tgid), &(pid_tgid), sizeof(__u64), 0)) {
        bpf_ringbuf_discard_dynptr(&dynptr, 0); // 针对写入失败进行 discard
        return 0;
    }

    if (bpf_dynptr_write(&dynptr, offsetof(struct event_struct, uid_gid), &(uid_gid), sizeof(__u64), 0)) {
        bpf_ringbuf_discard_dynptr(&dynptr, 0); // 针对写入失败进行 discard
        return 0;
    }

    bpf_ringbuf_submit_dynptr(&dynptr, 0); // 针对成功路径进行 submit
    return 0;
}

char __license[] SEC("license") = "Dual MIT/GPL";

引用

以上为dynptr的基本内容,更多详细的可以通过docs.ebpf.io了解