从 seccomp filter 学习内核 bpf 自定义 hook 点的设计
目录
本文以 seccomp filter 为例,探讨内核中 bpf 自定义 hook 点的一种设计实现,主要围绕如下几个核心问题叙述:
- bpf hook 点如何注入到 seccomp filter 代码中?需要哪些参数?不同参数的含义是什么?
- bpf 代码如何动态 attach 到执行入口中?
- bpf 代码的校验与翻译在哪里做?校验有哪些条件?jit 在哪个步骤进行?
- bpf 代码过滤的数据如何准备?
- 定义 bpf 规则过滤结果的行为
基线信息
内核源码版本
4.18
bpf hook 点如何注册到已有代码中?
可以通过使用 BPF_PROG_RUN
宏在已有代码中添加 bpf hook 点。BPF_PROG_RUN
宏的定义如下:
#define BPF_PROG_RUN(filter, ctx) (*(filter)->bpf_func)(ctx, (filter)->insnsi)
filter 参数代表了一个 bpf_prog
结构,此结构的 bpf_func 为 bpf 代码的执行入口。bpf_func 依赖如下两个参数:
- ctx 参数(表示 bpf 规则执行的上下文信息)
- insn 参数(代表实际的 bpf 指令码)
在 net 模块中搜索,能够搜索到如下类似使用逻辑:
./net/bpf/test_run.c:20: ret = BPF_PROG_RUN(prog, ctx);
./net/sched/act_bpf.c:53: filter_res = BPF_PROG_RUN(filter, skb);
./net/sched/act_bpf.c:57: filter_res = BPF_PROG_RUN(filter, skb);
./net/sched/cls_bpf.c:103: filter_res = BPF_PROG_RUN(prog->filter, skb);
./net/sched/cls_bpf.c:107: filter_res = BPF_PROG_RUN(prog->filter, skb);
seccomp filter 的实现中,引用 BPF_PROG_RUN 宏注入 bpf 过滤点相关代码如下:
static u32 seccomp_run_filters(const struct seccomp_data *sd,
struct seccomp_filter **match)
{
u32 ret = SECCOMP_RET_ALLOW;
/* Make sure cross-thread synced filter points somewhere sane. */
struct seccomp_filter *f =
READ_ONCE(current->seccomp.filter);
/* Ensure unexpected behavior doesn't result in failing open. */
if (WARN_ON(f == NULL))
return SECCOMP_RET_KILL_PROCESS;
/*
* All filters in the list are evaluated and the lowest BPF return
* value always takes priority (ignoring the DATA).
*/
for (; f; f = f->prev) {
u32 cur_ret = BPF_PROG_RUN(f->prog, sd);
if (ACTION_ONLY(cur_ret) < ACTION_ONLY(ret)) {
ret = cur_ret;
*match = f;
}
}
return ret;
}
上述代码调用 BPF_PROG_RUN 的逻辑在一个 for 循环里,表明一个进程支持绑定多个 seccomp 过滤规则,最终返回的值最小的结果它表示最高优先级。
bpf 代码如何动态 attach 到执行入口中?
在跳入到内核代码之前,先复习下之前上手 seccomp 使用的一个用户态程序,原文链接如下:
上手 seccomp,其中注入 bpf 的代码如下:
tatic int install_filter(int nr, int arch, int error) {
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
..........................................................................
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}
上述代码首先构建了一个 sock_filter 结构,然后绑定到一个 sock_fprog 结构上,以此结构地址作为 prctl 系统调用的最后一个参数将规则注入到当前程序上。
prctl 系统调用最终调用到 seccomp_attach_filter
函数将加载后的规则 attach 到进程上,核心代码如下:
filter->prev = current->seccomp.filter;
current->seccomp.filter = filter;
bpf 代码的校验与翻译在哪里做?校验有哪些条件?jit 在哪个步骤进行?
在 prctl attach bpf 规则到进程的过程中,seccomp_prepare_user_filter 负责从用户空间拷贝数据,然后调用 seccomp_prepare_filter 构建必要的数据结构,调用 bpf 模块的代码加载一个 bpf_prog。
bpf_prog_create_from_user bpf 函数完成 bpf 代码的校验翻译,校验可从业务逻辑上分为如下两部分:
- bpf 模块自身的校验
- 引用 bpf 代码模块的校验
seccomp 的实现中,第二部分由 seccomp_check_filter
函数负责,阅读此函数代码发现它不只有校验的功能,还有一些修正的逻辑以保证 bpf 规则正常工作,这部分逻辑修改了 bpf 规则,需要在翻译之前执行。
jit 在每架构实现函数 bpf_int_jit_compile 中进行,翻译成功后生成的代码入口会被设置到 prog 结构的 bpf_func 变量中,jited 变量也被设置为 1 表示 jit 成功,相关代码如下:
prog->bpf_func = (void *)image;
prog->jited = 1;
需要注意的是在 x86 架构上,bpf_jit_compile 函数是一个空函数,实际的翻译过程是在 bpf_migrate_filter 函数中完成的。
上述描述过程部分调用函数栈如下:
bpf_prepare_filter
bpf_check_classic
bpf_migrate_filter(fp);
bpf_prog_select_runtime
bpf_prog_select_func(fp);
bpf_int_jit_compile(fp)-- 不同架构实现不同
bpf 代码过滤的数据如何准备?有哪些限定?为什么要添加这些限定?
seccomp 规则的过滤数据定义如下:
struct seccomp_data {
int nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[6];
};
上述参数均与特定的架构绑定,x86、x64 架构中在 syscall_trace_enter 函数的如下代码位置进行填充:
if (work & _TIF_SECCOMP) {
struct seccomp_data sd;
sd.arch = arch;
sd.nr = regs->orig_ax;
sd.instruction_pointer = regs->ip;
#ifdef CONFIG_X86_64
if (arch == AUDIT_ARCH_X86_64) {
sd.args[0] = regs->di;
sd.args[1] = regs->si;
sd.args[2] = regs->dx;
sd.args[3] = regs->r10;
sd.args[4] = regs->r8;
sd.args[5] = regs->r9;
} else
#endif
{
sd.args[0] = regs->bx;
sd.args[1] = regs->cx;
sd.args[2] = regs->dx;
sd.args[3] = regs->si;
sd.args[4] = regs->di;
sd.args[5] = regs->bp;
}
ret = __secure_computing(&sd);
if (ret == -1)
return ret;
}
此后将生成的过滤数据作为参数传递给 __secure_computing 来完成 bpf 过滤系统调用并执行最高优先级上绑定的行为函数。
static void populate_seccomp_data(struct seccomp_data *sd)
{
struct task_struct *task = current;
struct pt_regs *regs = task_pt_regs(task);
unsigned long args[6];
sd->nr = syscall_get_nr(task, regs);
sd->arch = syscall_get_arch();
syscall_get_arguments(task, regs, 0, 6, args);
sd->args[0] = args[0];
sd->args[1] = args[1];
sd->args[2] = args[2];
sd->args[3] = args[3];
sd->args[4] = args[4];
sd->args[5] = args[5];
sd->instruction_pointer = KSTK_EIP(task);
}
定义 bpf 规则过滤结果的行为
bpf 代码过滤有它的结果,最简单的过滤结果就是成功、失败,实际使用中它可以支持多种不同的结果,上层逻辑可以根据不同的结果执行不同的行为。
seccomp 的过滤结果有如下几种类型定义:
#define SECCOMP_RET_KILL_PROCESS 0x80000000U /* kill the process */
#define SECCOMP_RET_KILL_THREAD 0x00000000U /* kill the thread */
#define SECCOMP_RET_KILL SECCOMP_RET_KILL_THREAD
#define SECCOMP_RET_TRAP 0x00030000U /* disallow and force a SIGSYS */
#define SECCOMP_RET_ERRNO 0x00050000U /* returns an errno */
#define SECCOMP_RET_USER_NOTIF 0x7fc00000U /* notifies userspace */
#define SECCOMP_RET_TRACE 0x7ff00000U /* pass to a tracer or disallow */
#define SECCOMP_RET_LOG 0x7ffc0000U /* allow after logging */
#define SECCOMP_RET_ALLOW 0x7fff0000U /* allow */
当多个类型命中时,执行优先级最高的(数字最小的)类型,可以执行如下命令查看当前系统支持的 seccomp 过滤行为:
cat /proc/sys/kernel/seccomp/actions_avail
总结
本文从概览的角度整理了几个内核实现自定义 bpf hook 点的核心问题,以 seccomp 实现为例进行了描述。从实现层面看还是有一定的复杂度,在后面的文章中可以探讨下 bpf hook 点如何与内核已有的探针机制——tracepoint 结合起来,实现更为灵活的功能。