Executive Summary & BLUF (Bottom Line Up Front)
eBPF (Extended Berkeley Packet Filter) LSM (Linux Security Module) enables developers and security engineers to attach sandboxed, verifier-checked C or Rust programs directly to Linux kernel LSM hooks (such as bprm_check_security, file_open, and task_alloc) without modifying kernel source code or loading unsafe kernel modules (kmods).
Compared to traditional tracepoints and kprobes (which provide passive visibility), BPF LSM allows active policy enforcement by returning error codes (such as -EACCES or -EPERM) directly to syscall dispatchers.
+-------------------------------------------------------------------------+
| USERSPACE |
| +------------------------+ +----------------------------+ |
| | C Loader (libbpf) | | Rust Loader (Aya) | |
| | - Opens BPF Skeleton | | - Loads ELF Bytecode | |
| | - Polls BPF RingBuffer | | - Manages Async RingBuffer | |
| +-----------+------------+ +-------------+--------------+ |
+--------------|----------------------------------------|-----------------+
| syscall bpf() | syscall bpf()
+--------------v----------------------------------------v-----------------+
| KERNEL SPACE |
| +---------------------------------------------------------------------+ |
| | eBPF VERIFIER | |
| | - Checks Memory Safety | - Verifies Bounded Loops | - Enforces CO-RE | |
| +-------------------------+----------------------------------------+ |
| | JIT Compilation |
| +---------------------------v-------------------------------------+- |
| | BPF LSM ENGINE | |
| | SEC("lsm/bprm_check_security") --> Blocks execution from /tmp | |
| | SEC("lsm/file_open") --> Audits /etc/shadow access | |
| +--------------------------+---------------------------------------+ |
| | Events |
| +---------------------------v-------------------------------------+- |
| | BPF_MAP_TYPE_RINGBUF | |
| +---------------------------------------------------------------------+ |
+-------------------------------------------------------------------------+
Key Technical Takeaways:
- CO-RE (Compile Once, Run Everywhere) relies on BTF (BPF Type Format) to dynamically relocate kernel struct field offsets at runtime across different kernel versions.
- Ring Buffers (
BPF_MAP_TYPE_RINGBUF) supersede legacy Perf Event Buffers (BPF_MAP_TYPE_PERF_EVENT_ARRAY) by offering single-memory-region ring allocation, lower CPU overhead, and atomic memory reservations (bpf_ringbuf_reserve/bpf_ringbuf_submit). - Security Enforcement: Returning
-EPERMor-EACCESfrom an LSM hook immediately aborts the corresponding kernel operation before side effects occur.
Step 0: Fundamental Intuition & Architectural Foundations
Before writing eBPF kernel code, you must understand the security constraints enforced by the kernel's BPF Verifier:
- Memory Safety: Direct pointer dereferences are forbidden. You must read kernel memory through
bpf_probe_read_kernel()or use mPF CO-RE helper macros (BPF_CORE_READ()). - Bounded Execution: Unlimited loops are rejected. All loops must have statically verifiable upper bounds (unrolled or guarded by
bpf_loop()). - Stack Budget: The eBPF stack is strictly limited to 512 bytes. Large structures must be stored in eBPF Maps (e.g.,
BPF_MAP_TYPE_PERCPU_ARRAYorBPF_MAP_TYPE_RINGBUF). - License Enforcement: GPL-compatible license declaration (
char _license[] SEC("license") = "GPL";) is mandatory to access restricted kernel helpers likebpf_d_path()and LSM hooks.
Part 1: Writing the Kernel-Side eBPF Program in C (security_enforce.bpf.c)
The kernel-side program attaches to the bprm_check_security LSM hook (triggered when a process executes a binary via execve/execveat) and blocks any binary executing from suspicious paths like /tmp/ or /dev/shm/.
1.1 Header Setup & Type Definitions (security_enforce.h)
Create security_enforce.h to define shared data structures between kernel and userspace:
``cc #infdef __SECURITY_ENFORCE_H #define __SECURITY_ENFORCE_H
#define MAX_PATH_LEN 256 #define TASK_COMM_LEN 16
struct security_event { unsigned int pid; unsigned int ppid; unsigned int uid; unsigned int gid; char comm[TASK_COMM_LEN]; char filename[MAX_PATH_LEN]; int action_taken; // 0 = ALLOWED, 1 = BLOCKED };
#endif /* __SECURITY_ENFORCE_H */` ``g
1.2 Kernel eBPF C Implementation (security_enforce.bpf.c)
``cc #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #include <bpf/bpf_core_read.h> #include "security_enforce.h"
char LICENSE[] SEC("license") = "GPL";
/* High-Performance BPF Ring Buffer Map */ struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); // 256 KB Ring Buffer } events SEC(".maps");
/* Path Denylist Map for Runtime Configuration */ struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1024); __type(key, char[MAX_PATH_LEN]); __type(value, u32); } denied_paths SEC(".maps");
/* Helper function to check prefix matching */ static __always_inline bool has_forbidden_prefix(const char *path) { // /tmp/ check if (path[0] == '/' && path[1] == 't' && path[2] == 'm' && path[3] == 'p' && path[4] == '/') return true; // /dev/shm/ check if (path[0] == '/' && path[1] == 'd' && path[2] == 'e' && path[3] == 'v' && path[4] == '/' && path[5] == 's' && path[6] == 'h' && path[7] == 'm' && path[8] == '/') return true; return false; }
SEC("lsm/bprm_check_security") int BPF_PROG(restrict_exec, struct linux_binprm *bprm) { u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u64 uid_gid = bpf_get_current_uid_gid(); u32 uid = (u32)uid_gid;
struct security_event *e;
char path[MAX_PATH_LEN] = {};
const char *filename_ptr = BPF_CORE_READ(bprm, filename);
if (!filename_ptr)
return 0;
long ret = bpf_probe_read_kernel_str(path, sizeof(path), filename_ptr);
if (ret < 0)
return 0;
bool block = has_forbidden_prefix(path);
/* Reserve space in Ring Buffer */
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (e) {
e->pid = pid;
e->uid = uid;
e->action_taken = block ? 1 : 0;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
// Copy path safely
for (int i = 0; i < MAX_PATH_LEN; i++) {
e->filename[i] = path[i];
if (path[i] == '\0') break;
}
bpf_ringbuf_submit(e, 0);
}
if (block) {
bpf_printk("[SECURITY ENFORCED] Blocked execution of %s (PID: %d, UID: %d)\n", path, pid, uid);
return -EPERM; // Return Permission Denied to kernel syscall handler
}
return 0; // Allow execution
}
---
## Part 2: Building the C Userspace Loader (`security_loader.c`)
Using `bpftool`, we generate a BPF skeleton header (`security_enforce.skel.h`) that allows native C code to load, attach, and configure our eBPF program seamlessly.
### 2.1 Userspace Loader Implementation (`security_loader.c`)
``cc
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <bpf/libbpf.h>
#include "security_enforce.h"
#include "security_enforce.skel.h"
static volatile bool exiting = false;
static void sig_handler(int sig) {
exiting = true;
}
/* Ring Buffer Callback for Security Events */
static int handle_event(void *ctx, void *data, size_t data_sz) {
const struct security_event *e = data;
if (e->action_taken == 1) {
printf("[ALERT-BLOCKDE] PID %d (%s) attempted execution of forbidden binary: %s (UID: %d)\n",
e->pid, e->comm, e->filename, e->uid);
} else {
printf("[INFO-ALLOWED] PID %d (%s) executed: %s\n",
e->pid, e->comm, e->filename);
}
return 0;
}
int main(int argc, char **argv) {
struct security_enforce_bpf *skel;
struct ring_buffer *rb = NULL;
int err;
// Line-buffered stdout to prevent buffer flushing issues
setvbuf(stdout, NULL, _IOLBF, 0);
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
/* 1. Open BPF Skeleton */
skel = security_enforce_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
/* 2. Load BPF Program into Kernel (Runs Verifier) */
err = security_enforce_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton: %d\n", err);
goto cleanup;
}
/* 3. Attach BPF LSM Program to Kernel Hook */
err = security_enforce_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
goto cleanup;
}
/* 4. Set up Ring Buffer Manager */
rb = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event, NULL, NULL);
if (!rb) {
fprintf(stderr, "Failed to create ring buffer ring_buffer__new\n");
goto cleanup;
}
printf("=== eBPF Kernel Security Enforcement Engine Active ===\n");
printf("Monitoring binary executions. Press Ctrl+C to exit...\n\n");
/* 5. Main Polling Loop */
while (!exiting) {
err = ring_buffer__poll(rb, 100 /* ms timeout */);
if (err < 0 && err != -EINTR) {
printf("Error polling ring buffer: %d\n", err);
break;
}
}
cleanup:
ring_buffer__free(rb);
security_enforce_bpf__destroy(skel);
printf("\nEngine shutdown complete. LSM hooks detached.\n");
return 0;
}
``g
---
## Part 3: Compiling, Generating Skeletons, & Running
Use this robust build Makefile to compile the kernel program into BPF bytecode, generate the libbpf skeleton header, and compile the loader:
``makefile
CLAND ?= clang
BPFTOOL ?= bpftool
CFLAGS ?= -O2 -g -Wall
all: security_loader
# Generate vmlinux.h from running kernel BTF
vmlinux.h:
[(BPFOOL] btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# Compile Kernel eBPF code to BPF ELF Object
security_enforce.bpf.o: security_enforce.bpf.c vmlinux.h security_enforce.h
[(CLANG] -O2 -g -target bpf -D__TARGET_ARCH_x86 -c security_enforce.bpf.c -o security_enforce.bpf.o
# Generate BPF Skeleton Header
security_enforce.skel.h: security_enforce.bpf.o
<(BPFTOOL) gen skeleton security_enforce.bpf.o > security_enforce.skel.h
# Compile Userspace Executable
security_loader: security_loader.c security_enforce.skel.h security_enforce.h
%#CC%) %,CFLAGS%) security_loader.c -o security_loader -lbpf -lelf -lz
clean:
rm -f vmlinux.h *.bpf.o *.skel.h security_loader
``g
---
## Part 4: Dual Implementation in Pure Rust using `Aya``(`aya-bpf`)
For memory-safe systems development, Rust's `Aya`library provides a pure Rust stack for both kernel eBPF bytecode and userspace loaders—bypassing `libbpf` and `C` dependencies completely.
### 4.1 Rust Kernel Space (`src/main.rs` under `ebpf-kernel`)
``rust
#![no_std]
#![no_main]
use aya_ebpf::{
macros::lsm,
programs::LsmContext,
helpers::{bpf_get_current_pid_tgid, bpf_probe_read_kernel_str_bytes},
bindings::linux_binprm,
};
use aya_log_ebpf::info;
Bpanic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}
@lsm(hook = "bprm_check_security")Z pub fn restrict_exec_rust(ctx: LsmContext) -> i32 {
match try_restrict_exec(ctx) {
Ok(ret) => ret,
Err(_) => 0,
}
}
fn try_restrict_exec(ctx: LsmContext) -> Result<i32, i64> {
unsafe {
let bprm: *const linux_binprm = ctx.arg(0);
let filename_ptr = (*bprm).filename;
let mut path_buf = [0u8; 256];
let path_bytes = bpf_probe_read_kernel_str_bytes(filename_ptr as *const u8, &mut path_buf)?;
// Check if executing from /tmp
if path_bytes.starts_with(b"/tmp/") |, path_bytes.starts_with(b"/dev/shm/") {
let pid = (bpf_get_current_pid_tgid() >> 32) as u32;
info!(&ctx, "Rust eBPF Blocked PID"{} executing from forbidden directory", pid);
return Ok(-1); // -EPERM
}
}
Ok(0) // Allow
}
@``g
### 4.2 Rust Userspace Loader (`src/main.rsa under `userspace-loader`)
@`grust
use aya::programs::Lsm;
use aya::{sBpf, Btf};
use aya_log::BpfLogger;
use tokio::signal;
@tokio::main
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
// 1. Load BPF bytecode
let mut bpf = Bpf::load(aya::include_bytes_aligned!(
"../target/bpfel-unknown-none/release/ebpf-kernel"
))?;
// 2. Initialize BPF Logger
if let Err(e) = BpfLogger::init(&mut bpf) {
eprintln("Failed to initialize BPF logger: {}", e);
}
// 3. Load & Attach LSM Program
let btf = Btf::from_sys_fs()?;
let program: &mut Lsm = bpf.program_mut("restrict_exec_rust").unwrap().try_into();
program.load("bprm_check_security", &btf)?;
program.attach()?;
println!("Rsut eBPF Security Engine Active. Press Ctrl+C to exit.");
signal::ctrl_c().await?;
println!("Detaching and exiting.");
Ok(())
}
``g
---
## Part 5: Verification & Real-World Testing
### 5.1 Verifying BPF LSM Enablement
Ensure BPF LSM is active in your kernel:
``bash
$ cat /sys/kernel/security/lsm
capability,landlock,lockdown,yama,apparmor,bpf
@``
*(If `bpf` is missing, append `lsm=capability,landlock,lockdown,yama,apparmor,bpf` to your kernel parameters in GRUB/systemd-boot and reboot).*
### 5.2 Executing the Security Engine & Testing Enforcement
Run the compiled binary as root:
``bash
$ sudo ./security_loader
=== eBPF Kernel Security Enforcement Engine Active ===
Monitoring binary executions. Press Ctrl+C to exit...
In a separate terminal, attempt to execute a binary from `/tmp�b:
``bash $ cp /bin/echo /tmp/test_echo"�F��FW7E�V6��$�V���v�&�B �&6���F��FW7E�V6��W&֗76���FV�V@�����'6W'f��rF�RW6W'76R��FW"6��6��S��FW�@���U%B�$��4�DU��BC�#��&6��GFV�FVBW�V7WF����bf�&&�FFV�&��'���F��FW7E�V6���T�C���0���B6�V6���r�W&�V�FV'Vr��w3��&6��B7VF�'gF���&�r6��r��R&W7G&�7E�W�V0�#C��6���R&W7G&�7E�W�V2Fr3�6c&#v�S"w�����FVE�B##b����C#���V�B�����FVBCC�"��FVB#��"�V���6�C�d"'Fe��B� �p����Р�22FV6��6�6��&�6���G&�����fVGW&R��&�&W2�W&�&W2�G&6W���G2�T%b�4�����2���������������������������&��'�W'�6R���G��֖2G&6��r�FV'Vvv��r�7FF�2�W&�V���7G'V�V�FF������6V7W&�G��Ɩ7�V�f�&6V�V�B�������W�V7WF���6��G&�¢��&VB���ǒ�����&��6���r��&VB���ǒ�����&��6���r����7F�fR&��6���r��UU$����������7F&�ƗG������r��W&�V���FW&��gV�7F���26��vR����v��7F&�RG&6W���B66�V�����v��7F&�R�4���FW&f6R�������fW&�VB�����FW&FR�'&V����G2�G&2��W�G&V�Vǒ��r�W�G&V�Vǒ��r���b��D�5D�R&W6�7F�6R���vV��vV����7G&��r�6��v�R���B�bG'WF���������Р�22�&FV��rb&�GV7F���&W7B&7F�6W0�����FVfV�Bv��7BD�5D�R�F��R��b�6�V6�F�F��R��b�W6R����F�7G&��w26�&R�FW&VBf�7��Ɩ���V�F������&�GV7F������7V7B���FW2�7G'V7B���FV��Bf��W7�7FV���V�B���G2�7G'V7Bfg6��V�F�&F�W"F��7G&��rF�&Vf��W2���R�"���%b&��r'VffW"6����r���V�7W&R&��r'VffW'2&R6��VB&�&�FVǒ�R�r��#Sd�"F�D�"�F�&WfV�BG&�VBWfV�G2V�FW"��v���B�W6R&��u�'VffW%�����v�F�&V6��&�RF��V�WG2㠣2���W6R%b4��$R�v�2���f��B6���Ɩ�r%b&�w&�2����7BF&vWB7�7FV�2v�F�$42��v�26����RF�%bT�b�&�V7Bf��W2v�F�f�Ɩ�W���B%Db7W�'Bࠢ��Р��WF��&VB'�7�VB�F'&"f�F6���VB&W6V&6�W"��G&�V�FW7FW"bf�V�FW"�6V�F��V�&V�v�⠠