概述
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
:- src 是一个无效的指针
- dst 是一个只读dynptr
- flags不正确
- skb类型的dynptr,返回的错误与
bpf_skb_store_bytes()
一致
bpf_dynptr_data
该辅助函数的作用是返回一个指向dynptr指向的底层数据的指针,说人话就是帮我们进行指针运算。但是使用该函数有几个要求:
- len必须是一个在编写时就确定的值,使用这种方式其实和不使用dynptr是一样的,但是dynptr提供了这种互转的能力。需要注意的是,返回的指针不是全局可用的,dynptr结构我们一般在栈中去存放,所以通过dynptr取出的底层数据指针的生命也与dynptr一致,切忌不可在尾调用时或者与其它eBPF程序共享时传递。
- skb和xdp类型的dynptr不能使用该辅助函数去获取底层数据指针。它们应该使用
bpf_dynptr_slice
和bpf_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
:- dynptr 是只读的
- dynptr 无效
- 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了解