#221025 直接使用 bpf_map_lookup_elem helper 函数返回的指针指向的数据安全吗?

TL;DR 可以,必须的。

用户空间的库函数

虽然名字一样,但用户空间和 bpf 程序调用的 bpf_map_lookup_elem 函数不一样。

先看用户空间的 bpf_map_lookup_elem 函数,这个函数由 libbpf 库提供,定义位于 tools/lib/bpf/bpf.h 文件中。

LIBBPF_API int bpf_map_lookup_elem(int fd, const void *key, void *value);

int bpf_map_lookup_elem(int fd, const void *key, void *value)
{
    union bpf_attr attr;

    memset(&attr, 0, sizeof(attr));
    attr.map_fd = fd;
    attr.key = ptr_to_u64(key);
    attr.value = ptr_to_u64(value);

    return sys_bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}

static inline int sys_bpf(enum bpf_cmd cmd, union bpf_attr *attr,
                        unsigned int size)
{
    return syscall(__NR_bpf, cmd, attr, size);
}

这个函数比 bpf 程序调用的 helper 函数多了一个 value 参数,这个参数指向一块 buffer,这块 buffer 由调用方申请,用来保存返回的 value 数据。跟 helper 函数直接返回 map 中 value 的指针不一样。

而且这个函数是对 bpf 系统调用的封装,所有的用户空间 bpf 函数最终都是通过 bpf 系统调用和内核的 bpf 系统打交道。不同功能对应不同的 cmd,参数通过 attr 这个 union struct 传递。

内核中根据 cmd 调用到上层指定的功能函数。

SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
|- __sys_bpf
|- switch cmd
   case BPF_MAP_CREATE:
        err = map_create(&attr)
        break;
   case BPF_MAP_LOOKUP_ELEM:
        err = map_lookup_elem(&attr)
        break;
   ...
   case BPF_PROG_LOAD:
        err = bpf_prog_load(&attr, uattr)
        break

根据 cmd bpf_map_lookup_elem 函数会调用下面这个函数来完成:

map_lookup_elem(union bpf_attr *attr)
|- int ufd = attr->map_fd
|- struct fd f = fdget(ufd)
|- struct bpf_map *map = __bpf_map_get(f)
|- ...
|- bpf_map_copy_value
|  |- rcu_read_lock()
|  |- map->ops->map_lookup_elem(map, key)
|  |- rcu_read_unlock()
|  ...
|- copy_to_user(uvalue, value, value_size)

如果是 BPF_MAP_TYPE_HASH 类型的 map,最后就是调用 htab_map_lookup_elem 这个函数。

struct bpf_htab {
    struct bpf_map map;
    ...
}
struct bpf_map {
    const struct bpf_map_ops *ops;
    ...
}
const struct bpf_map_ops htab_map_ops = {
    ...
    .map_lookup_elem = htab_map_lookup_elem,
    ...
};

对于用户空间的 bpf_map_lookup_elem,它是复制数据返回(不可能直接访问到内核的数据嘛),所以没有安全问题。

BPF 程序特供版:Helper 函数

接下来,来看 bpf 程序调用的 helper 函数,helper 函数在 tools/lib/bpf/bpf_helpers.h 中定义,bpf 程序需要 include 这个头文件,这个头文件里 include 了一个 bpf_helper_defs.h 文件,这个文件是脚本根据 include/uapi/linux/bpf.h 文件自动生成的。

// bpf_helpers.h
// include/uapi/linux/bpf.h + scripts/bpf_doc.py 自动生成下面这个文件
// scripts/bpf_doc.py" --header --file include/uapi/linux/bpf.h > src/bpf_helper_defs.h
#include "bpf_helper_defs.h"

include/uapi/linux/bpf.h 中和 Helper 函数相关的主要是下面这些内容:

/* Start of BPF helper function descriptions:
* ...
* void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
*   Description
*           Perform a lookup in *map* for an entry associated to *key*.
*   Return
*           Map value associated to *key*, or **NULL** if no entry was
*           found.
* ...
*/

//
#define ___BPF_FUNC_MAPPER(FN, ctx...)  \
    FN(unspec, 0, ##ctx)                \
    FN(map_lookup_elem, 1, ##ctx)       \
    ...

#define __BPF_FUNC_MAPPER_APPLY(name, value, FN) FN(name),
#define __BPF_FUNC_MAPPER(FN) ___BPF_FUNC_MAPPER(__BPF_FUNC_MAPPER_APPLY, FN)

#define __BPF_ENUM_FN(x, y) BPF_FUNC_ ## x = y,
enum bpf_func_id {
    ___BPF_FUNC_MAPPER(__BPF_ENUM_FN)
    __BPF_FUNC_MAX_ID,
};
// 上面这一大堆宏最终会给每个 Helper 函数定义一个如下的函数 id。
// enum bpf_func_id {
//     BPF_FUNC_bpf_map_lookup_elem = 1
//     ...
//  }

scripts/bpf_doc.py 脚本会将上面注释中的 Helper 函数定义说明和 Helper 函数 ID 定义合并生成下面给 bpf 程序用的头文件。其中 bpf_map_lookup_elem 定义如下:

// bpf_helper_defs.h
static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *) 1;

这样,编译出来的 bpf 程序的 call 指令 BPF_CALL 的函数地址就被替换成了一个函数 id。

bpf 程序加载的时候会在 verifer 中验证 bpf 指令是否安全以及将 bpf 指令即时向下编译为原生指令,这个过程中会执行函数的 link,将函数 id 翻译成最终实际的函数地址。

bpf_prog_load
|- ...
|- bpf_check
|- ...
|- do_misc_fixups
|  |- ...
|  |- const struct bpf_map_ops *ops;
|  |- ...
|  |- switch (insn->imm) {
|  |  case BPF_FUNC_map_lookup_elem:
|  |     insn->imm = BPF_CALL_IMM(ops->map_lookup_elem);
|  |     // #define BPF_CALL_IMM(x) ((void *)(x) - (void *)__bpf_call_base)
|  |     continue;
|- ...
|- fixup_call_args
    |- jit_subprogs
        |- bpf_int_jit_compile // arch/x86/net/bpf_jit_comp.c
            |- do_jit
               |- switch (insn->code) {
                  case BPF_JMP | BPF_CALL:
                       func = (u8 *) __bpf_call_base + imm32;
                       emit_call(&prog, func, image + addrs[i - 1])

对于 BPF_MAP_TYPE_HASH 类型的 map,这个 call 的函数地址最终被翻译成了 htab_map_lookup_elem 函数。

const struct bpf_map_ops htab_map_ops = {
...
    .map_lookup_elem = htab_map_lookup_elem,
...
};

问题和结论

从上面的分析来看,bpf 程序调用 bpf_map_lookup_elem (实际调用的是 htab_map_lookup_elem 函数)的时候,并没有加 rcu 读锁,而且就算加了读锁,返回的是 map 中原始数据的指针,这个指针指向的数据在函数调用结束后还能安全的使用吗?不会被更新或者删除掉吗?

答案是肯定的,安全。因为虽然 bpf 程序调用 helper 函数的时候没有加读锁,但整个 bpf 程序的执行都在一个更大范围的 rcu 读锁中。这个读锁保证了 xdp 程序中访问 map 数据的安全性。

local_bh_disable();
...
act = bpf_prog_run_xdp(xdp_prog, xdp);
...
local_bh_enable();

local_bh_disable/local_bh_enable 对标记了一个 rcu 的读临界区(critical section)。

BPF relies heavily on RCU protection for its data structures, but because the BPF program invocation happens entirely within a single local_bh_disable() section in a NAPI poll cycle, this usage is safe. The reason that this usage is safe is that readers can use anything that disables BH when updaters use call_rcu() or synchronize_rcu(). anything that disables bottom halves also marks an RCU-bh read-side critical section, including local_bh_disable() and local_bh_enable(), local_irq_save() and local_irq_restore(), and so on

如果要原地更新指针指向的数据,如果不是 percpu 的 map,需要使用原子操作或者加锁。

../_images/bpf-map-rcu.svg

References:

如何添加一个新的 BPF Helper 函数

bpf_ktime_get_tai_ns 这个函数比较简单,以其为例。https://github.com/torvalds/linux/commit/c8996c98f703b09afe77a1d247dae691c9849dc1

第一步,如前所述,在 include/uapi/linux/bpf.h 中添加新 Helper 函数的调用描述文档以及 函数 ID。

diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
@@ -5341,6 +5341,18 @@ union bpf_attr {
  *         **-EACCES** if the SYN cookie is not valid.
  *
  *         **-EPROTONOSUPPORT** if CONFIG_IPV6 is not builtin.
+ *
+ * u64 bpf_ktime_get_tai_ns(void)
+ * Description
+ *         A nonsettable system-wide clock derived from wall-clock time but
+ *         ignoring leap seconds.  This clock does not experience
+ *         discontinuities and backwards jumps caused by NTP inserting leap
+ *         seconds as CLOCK_REALTIME does.
+ *
+ *         See: **clock_gettime**\ (**CLOCK_TAI**)
+ * Return
+ *         Current *ktime*.
+ *
  */
 #define __BPF_FUNC_MAPPER(FN)              \
    FN(unspec),                     \
@@ -5551,6 +5563,7 @@
    FN(tcp_raw_gen_syncookie_ipv6), \
    FN(tcp_raw_check_syncookie_ipv4),       \
    FN(tcp_raw_check_syncookie_ipv6),       \
+   FN(ktime_get_tai_ns),           \
    /* */

第二步,实现这个函数,这部分一般通过 BPF_CALL_* 宏来定义并实现。bpf_ktime_get_tai_ns 比较简单,没有参数,直接使用 BPF_CALL_0 宏就行。

diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
@@ -198,6 +198,18 @@
+BPF_CALL_0(bpf_ktime_get_tai_ns)
+{
+   /* NMI safe access to clock tai */
+   return ktime_get_tai_fast_ns();
+}
+

第三步,通过一个 bpf_func_proto 类型的结构体将这个 BPF Helper 函数详细的调用方法:有几个参数、参数是什么类型的、参数大小限制啊、返回值是什么样的……这些信息提供给 BPF Verifier,Verfier 加载 BPF 程序遇到 bpf_ktime_get_tai_ns 调用的时候,需要这些信息来判断调用是否正确合法。

另外,每种类型的 BPF 程序都有一个 *_func_proto 函数(比如 XDP 就是 xdp_func_proto),这个函数传入函数 ID 返回该 ID 对应的 Helper 函数的 bpf_func_proto 结构体指针,Helper 函数可以在哪些类型的 BPF 程序中调用,就需要在这些类型对应的 *_func_proto 函数中添加一个 switch 分支将新加的函数加上。如果所有的 BPF 类型程序都可以调用,可以加入特殊的 bpf_base_func_proto 函数中,不用所有的 *_func_proto 中都加一遍。

diff --git a/include/linux/bpf.h b/include/linux/bpf.h
@@ -2349,6 +2349,7 @@
 extern const struct bpf_func_proto bpf_tail_call_proto;
+extern const struct bpf_func_proto bpf_ktime_get_tai_ns_proto;
 extern const struct bpf_func_proto bpf_get_current_comm_proto;

diff --git a/kernel/bpf/core.c b/kernel/bpf/core.c
@@ -2623,6 +2623,7 @@
 const struct bpf_func_proto bpf_ktime_get_coarse_ns_proto __weak;
+const struct bpf_func_proto bpf_ktime_get_tai_ns_proto __weak;

 const struct bpf_func_proto bpf_get_current_uid_gid_proto __weak;

diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
@@ -198,6 +198,18 @@
+const struct bpf_func_proto bpf_ktime_get_tai_ns_proto = {
+   .func           = bpf_ktime_get_tai_ns,
+   .gpl_only       = false,
+   .ret_type       = RET_INTEGER,
+};
+
@@ -1617,6 +1629,8 @@ bpf_base_func_proto(enum bpf_func_id func_id)
        return &bpf_ktime_get_ns_proto;
    case BPF_FUNC_ktime_get_boot_ns:
        return &bpf_ktime_get_boot_ns_proto;
+   case BPF_FUNC_ktime_get_tai_ns:
+           return &bpf_ktime_get_tai_ns_proto;
    case BPF_FUNC_ringbuf_output:
        return &bpf_ringbuf_output_proto;
    case BPF_FUNC_ringbuf_reserve: