LWN:BPF程序可更灵活地访问内存!

关注了就能看到更多这么棒的文章哦~

More flexible memory access for BPF programs

October 21, 2022
This article was contributed by David Vernet
DeepL assisted translation
https://lwn.net/Articles/910873/

BPF 程序中,所有内存访问都先要使用 verifier 以静态方式进行安全检查,verifier 在允许程序运行之前会对其进行整体分析。虽然这样就可以让 BPF 程序在内核空间能安全运行了,但它限制了 BPF 程序使用指针的方式。最近碰到的这种类型的一个限制就是,当 BPF 程序被加载时,BPF 程序中由指针所指向的内存区域的大小必须是静态检查状态下就是已知的。Joanne Koong 最近提供的一个 patch set 增强了 BPF,可以支持加载带有这种指向动态指定大小(dynamically sized)的内存区域的指针的 BPF 程序。

Verifying kernel pointers in BPF programs

为了要能安全地加载一个 BPF 程序,verifier 必须验证该程序中的所有内存访问都不会使内核 crash。这个工作很复杂,因为 "memory" 在程序中可能代表了各种不同的上下文。例如,一些指针可能指向的是 BPF 程序的 stack,而其他指针,如 kptrs,可能指向的是一个通过 kfunc 从 main kernel 传递过来的结构。这两种类型的指针都有各种使用场景,在这些场景中访问,有的是安全,有的是不安全的,因此需要 verifier 中专门一部分代码来确保对它们的任何访问都是安全的。对于 stack 指针,verifier 需要确保所有可能会读取的偏移量都是在程序的 active stack 区域内的。对于从 kfunc 返回的 kptrs,verifier 必须确保所读的偏移量都在 struct 的 BPF Type Format(BTF)信息所指定的结构的范围内(write access 受到更严格的控制)。

然而,虽然这两种不同类型的内存区域的边界是不一样的,但它们都要求所有相关 read 的 offset 都必须是静态固定的,从而能够让 verifier 来确保访问的安全性。当然,这种限制禁止了任何需要指向动态大小的数据区域的指针。例如,BPF ring-buffer map type 允许 BPF 程序向 ring buffer 中写入一些内容,供用户空间读取使用。如果所有的内存引用都需要在运行时就静态地确定下来,那么 BPF 程序就只能写那些在程序加载时已知 size 小的位置了。然而,实际上经常需要能够写入那些可以在运行时动态改变大小的内容。

dynptrs - Referencing dynamically size memory

Koong 的 patch set 添加了对 BPF 程序中访问动态大小的内存区域的支持,这个新特性称为 dynptrs。dynptrs 的主要思想是将一个指向拥有动态 size 的数据区域的指针跟 verifier 以及一些 BPF helper 函数为了判断这个区域是有效的而使用的 metadata 关联起来。Koong 的 patch set 在一个新定义的 bpf_dynptr 结构中建立了这种关联。这个 struct 对 BPF 程序来说是不透明的;在内核中,它的定义是:

/* the implementation of the opaque uapi struct bpf_dynptr */
struct bpf_dynptr_kern {
  void *data;
  u32 size;
  u32 offset;
} __aligned(8);

这个动态区域的 size 被存储在一个 32 位的 unsigned int 数里,最高 8 位被保留用于 dynptr 本身的 metadata。其中最高 bit 表示 dynptr 是否是只读的,接下来的七个 bit 表示 dynptr 所指向的内存类型。后面给 size 只留下了 24 位,也就意味着一个 dynptr 可以指向一个不大于 16MB 的区域。这个 patch set 添加了两种类型的 dynptrs 支持。BPF_DYNPTR_TYPE_LOCAL,是指向程序本地可以访问到的内存,比如一个 map 值;BPF_DYNPTR_TYPE_RINGBUF,指向 BPF_MAP_TYPE_RINGBUF map 中的数据。

Dynptrs 是通过一系列的 helper 函数来创建和访问的。可以使用 bpf_dynptr_read()辅助函数来读取 dynptr 中的内容,或者使用 bpf_dynptr_write()来对可写的 dynptr 进行写入。bpf_dynptr_read() 是用来从 dynptr 数据区域复制内存内容到另一个指定的 buffer 的,而 bpf_dynptr_write()将从程序中的一个 buffer 复制数据到 dynptr 数据区域。在执行 copy 动作之前,helper 函数会检查这里提供的 length 和 offset 是否指向 dynptr 内存区域中的有效部分。如果用户需要直接访问包含在 dynptr 中的内存区域,他们可以使用 bpf_dynptr_data() 这个 helper 函数,尽管在这种情况下,被请求的内存区域的 size 必须是静态可确定的,以便 verifier 能确保对它的任何访问都是有效的。

Local memory dynptrs

BPF_DYNPTR_TYPE_LOCAL,即 local dynptr 支持,是由该系列的第二个 patch 所添加的。BPF 中的 "local memory" 可以指程序使用的几种不同类型的内存,例如,map value、map key 和 stack memory 等。Koong 的 patch set 允许通过一个新的 helper 函数 bpf_dynptr_from_mem() 来创建本地 dynptrs。尽管存在各种各样的 local memory type,但是这个 patch set 的初版只添加了对创建指向 map value 的 local dynptrs 的支持。这个限制可能是因为 verifier 已经确保了那些收到了指向 map value 的指针的 helper 函数都能正确地初始化以及确定 size,从而使 dynptrs 的初版实现可以尽可能简单。

其他 local memory type 在未来也可以被支持,尽管每一种内存类型都需要 verifier 中添加额外逻辑来验证 bpf_dynptr_from_mem() 的输入参数。虽然在这组 patch set 中没有说明何时(或是否)会添加其他类型的 local memory,但添加对它们的支持似乎是保险的做法,这样才能在使用 API 时得到一致性更好的体验。在最初的实现中,用户没有办法知道 local dynptr 只支持 map value,于是会看到他们的程序被 verifier 拒绝了。

Dynamically sized ring-buffer entries using dynptrs

如上所述,static-sizing 的限制迫使内核在 BPF_MAP_TYPE_RINGBUF map 中所发布(published)的 ring buffer 中每一条的 size 在程序加载时都必须是静态已知的。为了解决这个问题,Koong 给出了一个 patch,其中定义了一个新的 BPF_DYNPTR_TYPE_RINGBUF 类型的 dynptr。该 patch 包括 bpf_ringbuf_reserve_dynptr() helper 函数,用于 reserve 动态 size 的 ring-buffer entry,以及 bpf_ringbuf_submit_dynptr()和 bpf_ringbuf_discard_dynptr()分别用于将这个 entry 发布到 ring buffer 区域或 discard 丢弃掉。这些 API 跟已有的用于 reserve 和 post 静态 size 的 ring buffer entry 的 API 密切相关。

Dynptrs 也被用在了新的 BPF_MAP_TYPE_USER_RINGBUF map type 这组 patch set 中,它最近被合并到了 bpf-next 中,这是笔者的 patch。这个 map type 允许用户空间向 BPF 程序发布 ring buffer entry,它提供了一个 bpf_user_ringbuf_drain() helper 函数,允许 BPF 程序从 ring buffer 中提取并消化 entry,并对每个 entry 都调用一些指定的 callback 函数。这个 callback 会接收一个指向 ring buffer entry 的 dynptr 来作为它的第一个参数。为了读取这些 entry,BPF 程序可以简单地使用 bpf_dynptr_read() 或者 bpf_dynptr_data(),如上面已经介绍过的。

Holding off on a kmalloc() type dynptr

有一点需要注意的是,上述所支持的 dynptr 类型中没有一个是用在通过 kmalloc()分配的内存上的。乍一看,这似乎是一个非常常见的使用场景,事实上在早期版本的 patch set 中提供过一个 BPF_DYNPTR_TYPE_MALLOC dynptr type 来给出支持。然而,这个 type 最终被放弃了,因为在讨论中发现了一些细微但是会影响到根基的问题,需要在支持这个功能之前先解决掉。

例如,在对 patch set 的回复中,Daniel Borkmann 提出了一个问题,就是应该由哪个 cgroup(memcg)来对这里所分配的内存进行统计(收费?)的问题。这一点确实是个好问题;在内核中代表用户空间进程所进行的分配,应该向包含了分配进程的 memcg 来收取费用。但是不是很容易能确定是哪个进程。选择加载程序的进程的 memcg 看起来挺合理,但是正如 Alexei Starovoitov 所指出的,该进程(和它的 memcg)在程序被加载后不一定还存在了。

Starovoitov 提出的另一个问题是,由 BPF 程序所分配的内存是否应该被记入 memcg 中。内核中的大多数 kmalloc()调用都没有采用这种方式来统计进去,而且,正如在 2022 年 Linux 内核维护者峰会上所强调的,BPF 程序是内核程序的实例,而不是用户程序。Borkmann 回应说,也许合理的解决方案是允许用户明确指定要负责承担费用的 memcg,而不是像 BPF 目前那样隐含指定加载任务的 memcg。这样一来,使用者就需要获得一个 memcg 的文件描述符,并在程序被加载时将其传递给内核。如果没有传递描述符,那么默认行为就是不把这部分内存算到任何一个 memcg 上去。这个建议得到了 Starovoitov 和 Andrii Nakryiko 的好评,尽管讨论在没有确定结论的情况下渐渐结束,不过 Koong 最终发送了一个后续 patch,将 BPF_DYNPTR_TYPE_MALLOC 替换为 BPF_DYNPTR_TYPE_LOCAL。

在 BPF 程序中进行动态分配,这是一个有趣的前景,所以一旦内存统计的解决方案明确下来,似乎就有可能再来看看这个功能的实现了。

Ongoing work with dynptrs

目前正在进行的工作是增加新的 dynptr 类型,从而支持网络协议栈代码中的后续 BPF 使用场景。在最近的一个 patch set 中,Koong 提议增加两种新的 dynptrs 类型,一种是用在包含了 socket buffer 的内存区域上,另一种是用在包含了一个 eXpress Data Path(XDP)buffer 的内存区域上。这些 dynptr 类型的是一样的,主要就是允许了 BPF 程序使用更符合人体工程学的 API 来读取(reading)和修改(mutating)buffer 里的内存内容。

比如假设用户想解析 TCP 数据包中包含在 struct xdp_md buffer 中的 type-length-value(TLV)header。这个结构里包含了 data 和 data_end 字段,分别代表了 packet 中数据区域的开始和结束。每个 TLV header 中包含了一个 header entry,其中包含 type 的编码,后面是 header value 的长度,然后是符合这个 length 的数据。header 中的 length 项的值可以在运行时根据不同的 packet 和 header type 来改变,所以在一个 packet 中遍历 header 需要的指针偏移量就不是一个静态确定的数值。如果没有 dynptrs 的话,用户将不得不对 packet header 的每一次读取进行显式检查,以确保它适合 xdp_md buffer 的 data 和 data_end 区域。有了 dynptrs,只要简单利用一个从前一个未知类型的 header TLV 中计算得来的 offset 调用一下 bpf_dynptr_data() 就可以获得指向下一个 header TLV 的指针,然后检查一下从 helper 这里获取的指针是不是 non-NULL 就好。

虽然这并没有实现新的使用场景,但它确实解决了 BPF networking program 中的一个重要的可用性问题,这也是一个经常会收到抱怨的地方。此外,它使得生成的 BPF program 代码受 Clang 和 LLVM 的改动的影响更加小,目前这些部分的改动有时会导致 verifier 拒绝了一个以前认为是 safe 的程序。

到目前为止,这些 patch 还没有收到任何强烈的反对意见,而且似乎也不太可能。此时此刻有另一个 patch set 也被提交到 upstream,其中增加了更多的 dynptr helper 函数。这些函数可能是未来另一篇文章的主要内容。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

format,png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值