eBPF helper 函數的設計與實現

您是否想爲內核添加一個新的 eBPF 輔助(helper)函數,但不知道從何入手?或者,您是否曾遇到過類似於 R2 type=ctx expected=fp, pkt, pkt_meta, map_value 的 eBPF verifier 報錯?本文將從代碼層面對 eBPF 輔助函數在內核中的設計與實現進行深入淺出的分析。相信在閱讀本文後,您不僅能夠輕鬆應對由於錯誤調用輔助函數導致的 eBPF verifier 問題,還能瞭解如何實現一個新的 eBPF 輔助函數。

本文首先簡單介紹了 eBPF 輔助函數的概念,並探討了其在內核中的設計,包括哪些重要組成部分。隨後,結合輔助函數 bpf_perf_event_output 的實現,幫助讀者瞭解實現一個 eBPF 輔助函數所需的要素。最後,我們通過一段代碼分析了調用輔助函數時傳入不匹配的參數類型導致 eBPF verifier 報錯的問題。

簡介

什麼是 eBPF 輔助函數?eBPF 輔助函數是內核提供給開發者的接口。

爲什麼要有 eBPF 輔助函數呢?爲什麼不能像驅動一樣直接調用內核函數呢?這主要是爲了保證系統安全。由於 eBPF 程序運行在內核態,爲了防止不當調用內核函數導致系統崩潰或安全漏洞,eBPF 程序只能調用內核提供的 eBPF 輔助函數。

截止目前內核共提供了 210 多個 eBPF 輔助函數,具體詳細列表可見內核源碼文件:include/uapi/linux/bpf.h

eBPF 輔助函數的設計

在內核中,struct bpf_func_proto 描述了 eBPF 輔助函數的定義、入參類型、返回值類型等重要信息。這些信息的指定主要是爲了通過 eBPF verifier 的安全驗證,確保傳入數據的可靠性,避免傳入錯誤的參數導致系統崩潰。struct bpf_func_proto 的具體形式的代碼片段如下所示:

struct bpf_func_proto {
 //eBPF 輔助函數具體實現
 u64 (*func)(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5);
 bool gpl_only;
 bool pkt_access;
 bool might_sleep;
 // 返回類型
 enum bpf_return_type ret_type;
 union {
  // 參數類型
  struct {
   enum bpf_arg_type arg1_type;
   enum bpf_arg_type arg2_type;
   enum bpf_arg_type arg3_type;
   enum bpf_arg_type arg4_type;
   enum bpf_arg_type arg5_type;
  };
  enum bpf_arg_type arg_type [5];
 };
 union {
  // 當參數類型爲 ARG_PTR_TO_BTF_ID,需要指明參數的 BTF 編號
  struct {
   u32 *arg1_btf_id;
   u32 *arg2_btf_id;
   u32 *arg3_btf_id;
   u32 *arg4_btf_id;
   u32 *arg5_btf_id;
  };
  u32 *arg_btf_id [5];
  struct {
   size_t arg1_size;
   size_t arg2_size;
   size_t arg3_size;
   size_t arg4_size;
   size_t arg5_size;
  };
  size_t arg_size [5];
 };
 // 返回參數的 BTF 編號
 int *ret_btf_id;
 bool (*allowed)(const struct bpf_prog *prog);
};

其中,func 表示該 eBPF 輔助函數的具體實現,實現了特定的功能。bpf_return_type 描述該 eBPF 輔助函數的返回參數類型,而 argx_type 描述該函數的入參類型。下面將對入參類型和返回值類型進行解析。

入參類型

入參類型分爲基本類型和擴展類型。擴展類型在基本類型的基礎上,添加了空指針類型,即允許入參爲空指針。另外,當參數類型爲 ARG_PTR_TO_BTF_ID 時,則需要在 struct bpf_func_proto 的成員 argx_btf_id 指明具體的 btf 編號。

注:BTF 編號可以看成內核數據類型的編號,通過該編號可以確定數據類型。

基本類型

基本類型大致包含三類:

  1. 指針類型,指針類型又可以進行細分:1)具體類型的指針類型,如 ARG_PTR_TO_SOCKET 表示 struct socket 指針;2)由 BTF 編號確定數據類型的指針類型,如 ARG_PTR_TO_BTF_ID 表示某一內核數據類型指針,且該內核數據類型由 BTF 編號指定;3)指向某一類型內存的指針,如 ARG_PTR_TO_MAP_KEY 指向 eBPF 程序棧內存的指針。

  2. 整數類型,如 ARG_CONST_SIZE 表示整數,且該整數的值不能爲 0;

  3. 任意類型,即 ARG_ANYTHING,其表示任意類型,但是需要初始化該值,否則 eBPF verifier 會報 未初始化 等相關錯誤。

完整的基本類型如下表所示:

GMS81t

擴展類型

包含的擴展類型如下表所示:

FlqKbl

返回值類型

同參數類型類似,返回值類型也分爲基本類型和擴展類型。擴展類型也是在基本類型的基礎添加了空指針類型。

基本類型

5mUIdT

擴展類型

擴展類型是在基本類型的基礎上,添加了空指針類型,表示返回值可能是空指針,那麼 eBPF verifier 需要考慮針對空指針進行安全驗證。

1s7HyG

eBPF 輔助函數的實現

本小節以 bpf_perf_event_output 爲例介紹 eBPF 輔助函數的實現。eBPF 輔助函數 bpf_perf_event_output 是應用最廣泛的一個,其主要功能是將數據通過 perf 緩衝區傳送給用戶態程序。實現 bpf_perf_event_output 需要完成以下三個步驟:

  1. 定義 struct bpf_func_proto 結構體,爲 bpf_perf_event_output 輔助函數指定功能函數、參數類型、返回值類型等;

  2. bpf_perf_event_output 輔助函數分配唯一的編號;

  3. bpf_perf_event_output 與特定的 eBPF 程序類型綁定,以確保只有該類型的程序才能調用該輔助函數。

定義 struct bpf_func_proto

BPF_CALL_5 (bpf_perf_event_output, struct pt_regs *, regs, struct bpf_map *, map,
    u64, flags, void *, data, u64, size)
{
 ......
 return err;
}

static const struct bpf_func_proto bpf_perf_event_output_proto = {
 .func  = bpf_perf_event_output,
 .gpl_only = true,
 .ret_type = RET_INTEGER,
 .arg1_type = ARG_PTR_TO_CTX,
 .arg2_type = ARG_CONST_MAP_PTR,
 .arg3_type = ARG_ANYTHING,
 .arg4_type = ARG_PTR_TO_MEM | MEM_RDONLY,
 .arg5_type = ARG_CONST_SIZE_OR_ZERO,
};

bpf_perf_event_output 的入參類型分別是:

  1. ARG_PTR_TO_CTX: struct pt_regs 指針

  2. ARG_CONST_MAP_PTR: struct bpf_map 指針

  3. ARG_ANYTHING:任意類型,且數值已初始化

  4. ARG_PTR_TO_MEM | MEM_RDONLY: 指向棧、報文或 eBPF map 元素值的指針

  5. ARG_CONST_SIZE_OR_ZERO: 整數且該整數值可爲 0

返回值類型是整數類型:RET_INTEGER

添加編號

在完成 struct bpf_func_proto 的定義之後,需要爲其分配一個唯一的編號。下面的代碼片段通過將其擴展爲 BPF_FUNC_perf_event_output 宏定義,並將該輔助函數的編號設置爲 25,即 #define BPF_FUNC_perf_event_output 25

注:該代碼片段位於內核源文件:include/uapi/linux/bpf.h

#define ___BPF_FUNC_MAPPER (FN, ctx...) 
 FN (unspec, 0, ##ctx)    \
 ......
 FN (perf_event_output, 25, ##ctx)  \
 ......

綁定 eBPF 程序類型

最後一步是要指定允許調用該輔助函數的 eBPF 程序類型。例如,下面的代碼片段中,允許 BPF_PROG_TYPE_KPROBE 類型的 eBPF 程序調用 bpf_perf_event_output 輔助函數。如果未指定允許調用該輔助函數的程序類型的 eBPF 程序調用了該輔助函數,則在 eBPF 程序加載過程會出現類似於 unknown func bpf_perf_event_output#25 的 eBPF verifier 錯誤提示。

static const struct bpf_func_proto *
kprobe_prog_func_proto (enum bpf_func_id func_id, const struct bpf_prog *prog)
{
 switch (func_id) {
 case BPF_FUNC_perf_event_output:
  return &bpf_perf_event_output_proto;
 ......
 default:
  return bpf_tracing_func_proto (func_id, prog);
 }
}
const struct bpf_verifier_ops kprobe_verifier_ops = {
 .get_func_proto  = kprobe_prog_func_proto,  // 驗證改類型的 eBPF 程序是否可調用 func_id 所代表的輔助函數
 .is_valid_access = kprobe_prog_is_valid_access,
};

小試牛刀

在理解了上述的理論知識後,我們可以來看看如何定位並解決開篇提到的問題:R2 type=ctx expected=fp, pkt, pkt_meta, map_value。下面是引起該錯誤的代碼示例,讀者可以分析該代碼存在哪些問題以及如何解決這些問題。

struct
{
    __uint (type, BPF_MAP_TYPE_HASH);
    __type (key, struct sock *);
    __type (value, struct sockmap_val);
    __uint (max_entries, 1024);
} sockmap SEC (".maps");

struct sockmap_val
{
    int nothing;
};

SEC ("tracepoint/tcp/tcp_rcv_space_adjust")
int tp__tcp_rcv_space_adjust (struct trace_event_raw_tcp_event_sk *ctx)
{
    struct sockmap_val *sv = bpf_map_lookup_elem (&sockmap, &ctx->skaddr);
    if (sv)
        bpf_printk ("% d\n", sv->nothing);
    return 0;
}

問題解析

首先解釋一下錯誤信息 R2 type=ctx expected=fp, pkt, pkt_meta, map_value 的含義。該錯誤表示 R2 寄存器的數據類型應該是指向棧內存的指針、報文指針、或者 eBPF map 的元素值指針,但實際數據類型是 ctx,即指向 struct pt_regs 的指針。因此,該問題實際上是因爲數據類型不匹配引起的。

在調用 bpf_map_lookup_elem (&sockmap, &ctx->skaddr) 函數時,我們傳遞的參數 &ctx->skaddr 是 ctx 類型參數,而不是 fp 類型參數。那麼爲什麼會有這個限制呢?

根據上文所述,eBPF 輔助函數的入參類型是通過 struct bpf_func_proto 進行定義的。我們可以參考 bpf_map_lookup_elem 輔助函數在內核代碼中的實現來解釋這個問題。在該函數的代碼片段中,可以看到它的第二個入參類型爲 ARG_PTR_TO_MAP_KEY,即指向 eBPF 程序棧內存的指針,也就是 fp。

const struct bpf_func_proto bpf_map_lookup_elem_proto = {
 .func  = bpf_map_lookup_elem,
 .gpl_only = false,
 .pkt_access = true,
 .ret_type = RET_PTR_TO_MAP_VALUE_OR_NULL,
 .arg1_type = ARG_CONST_MAP_PTR,
 .arg2_type = ARG_PTR_TO_MAP_KEY,
};

解決方案

針對這個問題,一般的解決方法是先定義一個棧變量,將 ctx->skaddr 的值存儲到棧上,例如 u64 skaddr = ctx->skaddr,然後在調用 bpf_map_lookup_elem 函數時,將該棧變量的地址 &skaddr 作爲函數的參數傳遞進去。

總結

本文重點介紹了 eBPF 輔助函數在內核中的設計,並描述了參數類型、返回值類型等重要概念。以 bpf_perf_event_output 爲例,介紹了實現一個 eBPF 輔助函數的核心要素。eBPF 輔助函數在開發 eBPF 程序中扮演着重要的角色,深入地瞭解 eBPF 輔助函數的設計和實現可以幫助解決開發過程中的許多相關問題。

如果你有 eBPF 相關的問題,可以通過微信號 wenan_mao 聯繫進羣或直接掃描二維碼到” 酷玩 BPF 學習交流羣 “:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/tgAoXvbKmgB1hRp2EE1TAQ