Linux Security Modules (LSMs) are the gatekeepers of your kernel, and eBPF can let you eavesdrop on their every decision.

Let’s see this in action. Imagine you want to know every time a process tries to execve a file that’s not owned by root. This is a common security event, and we can hook into the LSM_TARGET_EXEC event.

First, we need a simple eBPF program that attaches to this LSM hook.

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>

SEC("lsm/target_exec")
int BPF_KPROBE(lsm_target_exec_hook, struct linux_binprm *bprm) {
    // In a real scenario, you'd do more here:
    // - Get current process PID
    // - Check bprm->file->f_path.dentry->d_name.name
    // - Compare owner with bprm->cred->uid
    // - Log or alert if criteria are met

    // For demonstration, let's just print a message
    bpf_printk("LSM: Attempted execve for %s\n", bprm->file->f_path.dentry->d_name.name);

    return 0; // Allow the operation to proceed
}

char _license[] SEC("license") = "GPL";

To compile and load this, you’d typically use bpftool or a framework like libbpf. After loading, you’d inspect the output using bpftool prog show to get the program ID and then bpftool prog trace id <program_id> to see the printk messages.

Now, let’s build a mental model of how this works. LSMs are designed as a pluggable framework within the Linux kernel. When a sensitive kernel operation occurs (like opening a file, creating a process, or binding to a network port), the kernel invokes registered LSM hooks. These hooks are essentially callbacks provided by security modules like SELinux or AppArmor. What eBPF allows us to do is to inject our own code directly into these LSM hook points. Instead of relying on the pre-defined actions of an LSM module, we can intercept the arguments passed to the hook, inspect them, and even decide whether to allow or deny the operation (though for simple observation, returning 0 is sufficient).

The SEC("lsm/target_exec") macro is crucial. It tells the eBPF verifier and loader to place this program at the specific LSM hook point corresponding to the target_exec event. The BPF_KPROBE macro indicates we’re using a kprobe-style attachment, meaning the eBPF program will run when the kernel function associated with target_exec is entered. The arguments to our eBPF function (struct linux_binprm *bprm) are precisely the arguments that the kernel passes to the actual LSM hook. This gives us direct access to the context of the operation.

Within the eBPF program, bprm->file->f_path.dentry->d_name.name gives us the filename being executed. In a real-world security monitoring scenario, you would expand this: get the current process ID using bpf_get_current_pid_tgid(), check the owner of the file using bprm->cred->uid, and then conditionally log or trigger an alert if the owner isn’t root, for example. The bpf_printk function is a simple way to emit messages that can be retrieved using tools like bpftool or cat /sys/kernel/debug/tracing/trace_pipe.

The one thing most people don’t realize is that the LSM framework is far more granular than just the high-level policy enforcement you see with SELinux or AppArmor. There are hundreds of specific LSM hooks for almost every conceivable kernel action, from file_alloc_anon_inode to inode_unlink. eBPF’s ability to attach to these fine-grained hooks means you can build incredibly detailed audit trails or implement custom, lightweight security policies that don’t require the complexity of a full LSM module. You’re not just observing; you’re getting a direct, contextual view of kernel security decisions as they happen.

Once you’ve successfully hooked into LSM events, the next logical step is to explore how to dynamically modify LSM behavior based on the events you observe, potentially denying operations with custom eBPF logic.

Want structured learning?

Take the full Ebpf course →