How to Audit and Intercept File Read/Write Operations Using eBPF

This guide explains how to leverage eBPF’s Kprobe, Tracepoint, and LSM features to audit file read/write activity, extract process and file details, and optionally block operations using helpers like bpf_send_signal or bpf_override_return, with complete code examples and configuration steps.

Linux Code Review Hub
Linux Code Review Hub
Linux Code Review Hub
How to Audit and Intercept File Read/Write Operations Using eBPF

Attackers often read configuration files or modify system files to maintain persistence. This article shows how to use eBPF to audit and intercept file read/write operations, covering three main eBPF mechanisms: Kprobe/Kretprobe, Tracepoint, and LSM.

1. Auditing File Read/Write Operations

We first decide which information to capture: process ID, process name, file name, and the file open mode.

1.1 Using eBPF Kprobe/Kretprobe

The kernel function vfs_open is traced. Its signature is:

int vfs_open(const struct path *path, struct file *file)

From the path argument we obtain the file name via path->dentry->d_name. The open mode is read from the file argument. The core eBPF program looks like:

static void get_file_path(const struct path *path, char *buf, size_t size) {
    struct qstr dname;
    dname = BPF_CORE_READ(path, dentry, d_name);
    bpf_probe_read_kernel(buf, size, dname.name);
}

SEC("kprobe/vfs_open")
int BPF_KPROBE(kprobe_vfs_open, const struct path *path, struct file *file) {
    pid_t tid = bpf_get_current_pid_tgid();
    struct event_t event = {};
    event.pid = tid >> 32;
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    event.fmode = BPF_CORE_READ(file, f_mode);
    get_file_path(path, event.filename, sizeof(event.filename));
    bpf_map_update_elem(&entries, &tid, &event, BPF_NOEXIST);
    return 0;
}

SEC("kretprobe/vfs_open")
int BPF_KRETPROBE(kretprobe_vfs_open, long ret) {
    pid_t tid = bpf_get_current_pid_tgid();
    struct event_t *event = bpf_map_lookup_elem(&entries, &tid);
    if (!event)
        return 0;
    event->ret = (int)ret;
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, event, sizeof(*event));
    bpf_map_delete_elem(&entries, &tid);
    return 0;
}

1.2 Using eBPF Tracepoint

We can also trace the openat system call via the tracepoint sys_enter_openat. The event structure provides arguments where args[1] is the filename and args[3] is the open mode.

SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint_syscalls__sys_enter_openat(struct trace_event_raw_sys_enter *ctx) {
    pid_t tid = bpf_get_current_pid_tgid();
    struct event_t event = {};
    event.pid = tid >> 32;
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    event.fmode = (int)BPF_CORE_READ(ctx, args[3]);
    const char *filename_ptr = (const char *)BPF_CORE_READ(ctx, args[1]);
    bpf_probe_read_user_str(event.filename, sizeof(event.filename), filename_ptr);
    bpf_map_update_elem(&entries, &tid, &event, BPF_NOEXIST);
    return 0;
}

SEC("tracepoint/syscalls/sys_exit_openat")
int tracepoint_syscalls__sys_exit_openat(struct trace_event_raw_sys_exit *ctx) {
    pid_t tid = bpf_get_current_pid_tgid();
    struct event_t *event = bpf_map_lookup_elem(&entries, &tid);
    if (!event)
        return 0;
    event->ret = (int)BPF_CORE_READ(ctx, ret);
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, event, sizeof(*event));
    bpf_map_delete_elem(&entries, &tid);
    return 0;
}

1.3 Using eBPF LSM Hook

Linux Security Modules (LSM) can be extended with eBPF starting from kernel 5.7. First ensure the kernel is built with CONFIG_BPF_LSM=y and that bpf appears in /sys/kernel/security/lsm. If not, add lsm=... ,bpf to GRUB_CMDLINE_LINUX and reboot.

After enabling LSM, we can attach to the file_open hook:

static void get_file_path(const struct file *file, char *buf, size_t size) {
    struct qstr dname = BPF_CORE_READ(file, f_path.dentry, d_name);
    bpf_probe_read_kernel(buf, size, dname.name);
}

SEC("lsm/file_open")
int BPF_PROG(lsm_file_open, struct file *file) {
    struct event_t event = {};
    event.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    event.fmode = BPF_CORE_READ(file, f_mode);
    get_file_path(file, event.filename, sizeof(event.filename));
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
    return 0; // allow
}

2. Intercepting File Operations

To decide whether to block an operation we can compare the current process name. The example blocks only operations issued by the cat command.

static __always_inline bool str_eq(const char *a, const char *b, int len) {
    for (int i = 0; i < len; i++) {
        if (a[i] != b[i])
            return false;
        if (a[i] == '\0')
            break;
    }
    return true;
}
static __always_inline int str_len(char *s, int max_len) {
    for (int i = 0; i < max_len; i++) {
        if (s[i] == '\0')
            return i;
    }
    if (s[max_len - 1] != '\0')
        return max_len;
    return 0;
}

In the tracepoint program we fetch the current command name and apply the comparison:

SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint_syscalls__sys_enter_openat(struct trace_event_raw_sys_enter *ctx) {
    char target_comm[TASK_COMM_LEN] = "cat";
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    if (!str_eq(event.comm, target_comm, str_len(target_comm, TASK_COMM_LEN)))
        return 0; // not cat, allow
    // interception logic follows
    ...
}

2.1 Interception with bpf_send_signal

We can terminate the offending process by sending a signal:

long ret = bpf_send_signal(SIGKILL);
return 0;

2.2 Interception with bpf_override_return

We store an error code in a map during the entry probe and replace the return value in the exit probe:

// entry probe stores error
bpf_map_update_elem(&override_tasks, &tid, &err, BPF_NOEXIST);
// exit probe overrides return
bpf_override_return(ctx, *err);
bpf_map_delete_elem(&override_tasks, &tid);

2.3 Interception via LSM Hook

Returning a non‑zero value from the file_open LSM hook blocks the open operation. The example returns -1 for the cat command:

SEC("lsm/file_open")
int BPF_PROG(lsm_file_open, struct file *file) {
    // ... collect event data ...
    if (!str_eq(event.comm, target_comm, str_len(target_comm, TASK_COMM_LEN)))
        return 0; // allow
    return -1; // block
}

3. Summary

The article demonstrated three ways to audit file read/write operations with eBPF—Kprobe/Kretprobe, Tracepoint, and LSM—and three techniques to intercept those operations using bpf_send_signal, bpf_override_return, and LSM return codes. Full code snippets are provided for each method, along with kernel configuration steps for enabling eBPF LSM.

For further eBPF security applications, see the book eBPF Cloud‑Native Security: Principles and Practice by Huang Zhugang and Kuang Dahu.

eBPFLSMTracepointLinux SecurityKprobeFile Auditing
Linux Code Review Hub
Written by

Linux Code Review Hub

A professional Linux technology community and learning platform covering the kernel, memory management, process management, file system and I/O, performance tuning, device drivers, virtualization, and cloud computing.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.