LWN: BPF可以调用内核函数了!
关注了就能看到更多这么棒的文章哦~
Calling kernel functions from BPF
By Jonathan Corbet
May 13, 2021
DeepL assisted translation
https://lwn.net/Articles/856005/
内核中的 BPF 虚拟机可以支持从用户空间加载程序之后在内核的上下文中安全地执行。然而,如果不能同内核其他部分交互的话,作用就很有限了。BPF 和内核之间的接口一直保持在很小的范围,这其中有很多原因,比如保持安全性以及保持内核对系统的控制。然而,5.13 内核包含了一个功能,今后可以被用来极大地拓宽这些可用的接口:也就是能从 BPF program 直接调用内核函数。
推动这个功能的最直接原因是要在 BPF 中实现 TCP 拥塞控制算法,这是 Martin KaFai Lau 在 5.6 内核中就加入的一项功能。在 BPF 中实现的拥塞控制代码中重新实现了一些内核中已经存在的函数,这并不是一个好做法。如果可能的话,最好是能直接使用内核中现有的函数。这次新增的函数调用机制(也是由 Lau 实现的)就将这种可能性变为了现实。
Making functions available to BPF
在 BPF program 中,要使用一个内核函数,现在只需要对它声明成 extern 并像其他 C 函数一样调用它即可。而在内核里则需要做更多的工作。BPF program 只能访问一组特定的经过允许的函数,而这组函数只可以供指定的 BPF program type 使用,因此内核内的代码必须要确保这些函数在正确的上下文中可用。举例来说,这次的代码提交就是为了让 tcp_slow_start() 对 BPF 可用,但只有拥塞控制程序才可以使用。
向 BPF 程序 "export" 某些函数的过程,就是向与 program type 相关的 bpf_verifier_ops structure 添加一个新函数:
bool (*check_kfunc_call)(u32 kfunc_btf_id);
当 BPF verifer 看到一个对外部函数的调用时,就会执行这个函数,其中 kfunc_btf_id 是分配给 BPF program 要调用的函数的 BPF type format(BTF)ID。如果这个调用应该被允许,那么此函数就返回 true。比如说 tcp_slow_start() 是唯一一个用这种方式提供的函数的话,那么就可以写这样一个函数:
static bool bpf_tcp_ca_check_kfunc_call(u32 id)
{
return id == BTF_ID(func, tcp_slow_start);
}
如果有很多函数需要 export 的话,不需要用一长串 if 语句来判断,还有更简单的方法,请看相关 commit 中的例子。
除了检查函数是否可用之外,BPF verifier 还要进行其他一系列检查。例如检查传递给函数的参数及其类型是否正确,不正确的 program 将会被拒绝。只有当 verifier 能够确认这个 program 是安全的,才会允许调用,尽管 verifier 其实不能真正知道被调用的函数内部发生了什么,也不知道会出什么错误。
Some questions
到目前为止,拥塞控制程序是唯一使用了这个功能的 program type,但不难想象,今后会有其他 program 出现。这种能力引出了一些有趣的问题,以及如何在未来使用这种功能。
其中第一个问题是:这项能力与多年来一直是 BPF 一部分的 BPF helper 机制有何不同?changelog 中并没有解释这一点,所以编者只能猜测一下。BPF helper program 这些代码是专门供 BPF 程序使用的,必须经过特别的声明,而且需要填写一个 bpf_func_proto 结构并提供给 verifier,可以参看 bpf_map_lookup_elem() 相关代码示例。要将现有的内核函数作为一个 BPF helper 使用的话,意味着要写一个 wrapper 函数,然后再完成上述这些步骤。
用新方法使一个内核函数可被调用的话,只需要定义一个允许调用的 "check" 函数,剩下的就由 BPF 子系统来完成。人们可能会认为,一开始实现 helper 的时候就应该以这种方式来实现,但其实很多必备的底层基础设施是在 helper 机制出现多年后才被开发出来的。比如必须要 BTF,必须要 BPF Linux security module(以前所说的 KRSI)。如果一开始就有这些基础设施的话,可能就不会有 BPF helper 了。
也就是说,BPF helper 的优点是只为了 BPF program 使用的,而内核函数是为了被内核中各个部分调用而存在的。内核中没有稳定的 ABI,所以经常会看到 BPF export 出来的内核函数接口的变化甚至比起 BPF helper 接口变化更频繁。在这个添加了函数调用能力的 commit 中明确说明不保证 ABI:
白名单上的函数不保证作为固定 ABI。这些函数已经被现有的内核 tcp-cc 使用。如果其中任何一个改变了,那么无论是 in-tree 还是 out-of-tre 的 kernel tcp-cc 实现代码都必须更改。
很想知道如果一个内核内部的改动破坏了某个很流行的 BPF program,用户开始抱怨的时候会发生什么。一般来说,提供给 BPF 的功能不算是内核 ABI 的一部分,但这个策略从未被 Linus Torvalds 明确地承认或赞同过。
BPF helper 也被设计成可以从 BPF 上下文中进行安全的调用。换句话说,就是可以从内核之外调用。常规的内核函数在编写时没有考虑到可能会有恶意的调用者。BPF 子系统作为一个整体则竭力确保 BPF program 不会导致崩溃或者被入侵,但该子系统并不能知道在它调用的内核函数内部在做些什么事情,也不能保证给某个函数调用传递的所有参数是符合规范的。如果把错误的函数提供给了 BPF,那么一个错误程序或者恶意程序就可能会利用它们来制造麻烦。
最后,这种机制看起来有点像在内核本身的 export 机制之外的一个后门。export symbol 给 module 调用的时候都要在相关代码旁边加上 EXPORT_SYMBOL() 这个声明,并且这些经常引起人们的关注,以及引发是否该用这种方式把内核内部的东西暴露给外界使用的讨论。而针对 BPF program 的 export 函数是一种比较低调的做法,相关代码可能远离相关函数的定义位置。极端来说,似乎没有什么办法可以阻止某人注册这样的检查函数:
static bool export_the_world(u32 kfunc_btf_id)
{
return true;
}
如果添加了这个函数的话,那么几乎内核中所有函数都可以被一个正确类型的 BPF program 调用。这不会是什么好事情。理论上来说这样的函数会在代码 review 中被发现,但值得思考的是,有多少人真的 review 过了这组 patch 中加入的 BPF 调用函数的测试代码?这些代码已经被加入到了(完全无关的)traffic-control classifier program type 中。这组(无害的)代码将出现在所有启用了 traffic-control 的系统中。看起来 export 一个错误的函数给 BPF program 从而有意或无意地创造 bug,并不算困难。
这些问题中某些也许可以通过向 BPF 核心代码注册一个得到许可的内核函数列表来解决,也就是不能自己来决定要 export 哪些函数。但目前实现的代码并不是这么做的。
尽管如此,这个 BPF function-calling 机制已经被合并,会被包含在 5.13 版本中。据推测,在未来的版本中大家足够警惕,防止内核函数被不恰当地 export 出来。如果管理得当,这个特性可以被用来为 BPF program 提供许多功能,大大增加了可以用 BPF 做的有用的事情。很期待看到这个特性的后续发展。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~