eBPF系列二:例子——openat2

迫于Linux eBPF文档过少,我边学习边把对其的理解记录下来,供后来者参考。
本文是eBPF系列的第二篇:例子——openat2。

Introduction

在计算机中运行程序、读写文件,都会涉及到文件的打开操作,Linux v5.10与文件打开相关的系统调用有open() / creat() / openat() / openat2这四类,在使用glibc v2.32时,几乎所有的文件打开操作使用的都是openat2()这个系统调用。

openat2()是POSIX标准定义的系统调用之一,用于文件的创建或打开,它有4个参数,其中第一个参数dirfd为文件夹的描述符,第二个参数pathname为文件路径。

这里实现一个eBPF程序,他能获取系统调用openat()的前两个参数信息。

Instance

这些源码在这里

Example 1

在v5.10版本的内核上,系统调用入口SYSCALL_DEFINE4(openat2...)对参数做了一些简单的检查后,调用的是do_sys_openat2()进行进一步处理,其因此可以使用kprobe hook do_sys_openat2()间接地打印openat2()的参数信息。它的第一、二个参数含义等同于openat2(),因此打印前两个参数信息即可。相关源码主要如下:

1
2
3
4
5
6
7
8
9
10
SEC("kprobe/do_sys_openat2")
int hello(struct pt_regs *ctx) {
const int dirfd = PT_REGS_PARM1(ctx);
const char *pathname = (char *)PT_REGS_PARM2(ctx);
char fmt[] = "@dirfd='%d' @pathname='%s'";

bpf_trace_printk(fmt, sizeof(fmt), dirfd, pathname);

return 0;
}

运行:

1
2
$ make hello openat1_kern.o
$ sudo ./hello openat1_kern.o

Example 2

参数pathnamedo_sys_openat2()是个指向用户态程序空间的char类型的指针,若想把文件名复制到eBPF程序中,则需要借助bpf_probe_read_user_str()了:

1
2
3
char msg[256];

bpf_probe_read_user_str(msg, sizeof(msg), pathname);

Internal

Linux区分了不同特权等级下程序可访问的虚拟内存空间范围,它是通过access_ok()检查struct thread_info中的addr_limit来实现的。有一组API {get,set}_fs()可用于在kernel运行时中控制可访问的内存空间范围。

Note:

  1. struct thread_info是CPU架构专属类型,并非每类都有addr_limit,对x86来讲,它是段寄存器FS
  2. set_fs()会引起一些security bugs,因此当前Linux中在尽力去除这组API[1][2]

Example 3

这里写一写怎么直接hook系统调用的入口,即SYSCALL_DEFINE4(openat2...)

SYSCALL_DEFINE4一步步展开如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SYSCALL_DEFINE4
--> SYSCALL_DEFINEx
--> SYSCALL_METADATA // syscall tracepoint的封装
__SYSCALL_DEFINEx

// for x86
__SYSCALL_DEFINEx
--> __X64_SYS_STUBx // amd64使用
__IA32_SYS_STUBx // ia32使用

// for amd64
__X64_SYS_STUBx
--> __SYS_STUBx(x64, sys##name, SC_X86_64_REGS_TO_ARGS(x, __VA_ARGS__)))
--> long __##abi##_##name(const struct pt_regs *regs)

拼接起来,amd64架构,系统调用openat2()的入口函数名为__x64_sys_openat2(),参数类型是struct pt_regs *。因此eBPF程序这么写:

1
2
3
4
5
6
7
8
9
10
11
SEC("kprobe/sys_openat")
int hello(struct pt_regs *ctx) {
char fmt[] = "@dirfd='%d' @pathname='%s'";
struct pt_regs *real_regs = (struct pt_regs *)PT_REGS_PARM1(ctx);
int dirfd = PT_REGS_PARM1_CORE(real_regs);
char *pathname = (char *)PT_REGS_PARM2_CORE(real_regs);

bpf_trace_printk(fmt, sizeof(fmt), dirfd, pathname);

return 0;
}

代码中SEC("kprobe/sys_openat")表示kprobe的hook point为sys_openat,实际上用户态程序hello在调用load_and_attach()时候会检查kprobe的hook point前缀是否是sys_,若是对amd64则自动添加__x64_前缀

macro PT_REGS_PARMx_COREbpf_probe_read_kernel()做了封装,可以简单地认为用于获取hook func的第x个参数。因hook func的参数是struct pt_regs *,所以需要使用bpf_probe_read_kernel()取得struct pt_regs,进而获取得到系统调用SYSCALL_DEFINE4(openat2...)所示的参数信息。

Example 4

Linux内部API经常变更,使用kprobe hook特定的函数名不具有普适性。Linux为系统调用提供了tracepoint,若用tracepoint例子则这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct syscalls_enter_openat_args {
unsigned short common_type;
unsigned char common_flags;
unsigned char common_preempt_count;
int common_pid;
long syscall_nr;
long dfd;
long filename_ptr;
long flags;
long mode;
};

SEC("tracepoint/syscalls/sys_enter_openat")
int hello(struct syscalls_enter_openat_args *ctx) {
char fmt[] = "@dirfd='%d' @pathname='%s'";

bpf_trace_printk(fmt, sizeof(fmt), ctx->dfd, (char *)ctx->filename_ptr);

return 0;
}

struct syscalls_enter_openat_args成员信息来自tracefs中的文件events/syscalls/sys_enter_openat2/format

Reference