BPF-LSM (Security Hooks)
BPF-LSM is the Linux Security Module (LSM) that lets a privileged userspace process attach eBPF programs to the kernel’s
security_*hook points and have them return an allow/deny verdict at runtime — implementing system-wide Mandatory Access Control (MAC) and audit policy without writing a kernel module, compiling a policy language, or rebooting (prog_lsm.rst, v6.12). It is a real LSM named"bpf"—security/bpf/hooks.cregisters it withDEFINE_LSM(bpf)(hooks.c, v6.12) — that runs alongside SELinux and AppArmor in the same hook chain rather than replacing them. The program type isBPF_PROG_TYPE_LSMwith expected attach typeBPF_LSM_MAC(bpf.h, v6.12); a program targets a specific hook by BTF id and returns0to allow or a negative errno (e.g.-EPERM) to deny. The single most important idea is that this turns the kernel’s static, hundreds-strong array of security hooks into a programmable surface: anybpf_lsm_<hook>checkpoint that SELinux could interpose on, a verified BPF program can now interpose on too — and because the program runs under the verifier, it cannot crash the kernel, loop forever, or read arbitrary memory.
This is the §5 security-program-type leaf of the Linux eBPF MOC. It owns the eBPF mechanism — the program type, the attach path, the return-value semantics, and where the bpf LSM sits in the hook chain. The LSM framework itself (how security_*() call sites work, how multiple modules stack) is owned by The Linux Security Module Framework in the Linux Security MOC; cross-link, do not duplicate.
Version pin
Every concrete fact here is read from the Linux 6.12 LTS git tag (released 2024-11-17). The
bpfLSM (program typeBPF_PROG_TYPE_LSM) first appeared in Linux 5.7 — verified by a boundary check:kernel/bpf/bpf_lsm.cdoes not exist at thev5.6tag (404) but is present atv5.7. The set of whichsecurity_*hooks are attachable, and which are sleepable, changes between releases; the lists below are the 6.12 sets.
Mental Model: a programmable LSM that votes “deny” or stays silent
The Linux kernel guards privileged operations with the LSM framework: at hundreds of points — opening a file, mmap’ing memory with execute permission, creating a socket, sending a signal, loading a kernel module — the kernel calls a security_<operation>() function. Each such function consults every registered security module (SELinux, AppArmor, Yama, Landlock, …) and, if any of them objects, the operation is denied. BPF-LSM adds one more voter to that chain whose vote is supplied by a sandboxed BPF program you loaded at runtime.
Think of it as two nested aggregations. Inside the bpf LSM, the programs you attach to a single hook chain together: each one runs, each one can deny, and the first denial wins. Outside, the bpf LSM’s combined verdict is just one entry in the kernel-wide LSM chain, evaluated after the major MAC modules because bpf sits last in the default initialization order. The net effect is a one-way ratchet: BPF-LSM can only ever add a restriction to what the static LSMs already permitted — it can never grant an operation the rest of the stack denied.
flowchart TB SYS["syscall reaches a guarded operation<br/>e.g. file_open()"] --> SEC["security_file_open(file)<br/>(LSM framework call site)"] SEC --> CHAIN["call_int_hook: walk LSMs in order<br/>stop at first non-default return"] CHAIN --> SEL["selinux_file_open()"] SEL -->|"allow (returns default)"| APP["apparmor_file_open()"] APP -->|"allow (returns default)"| BPFSHIM["bpf_lsm_file_open()<br/>(noinline shim + BPF trampoline)"] BPFSHIM --> P1["BPF prog A → ret"] P1 -->|"ret == 0"| P2["BPF prog B → ret<br/>(sees A's ret as 4th arg)"] P2 -->|"ret == 0"| ALLOW["operation proceeds"] P1 -->|"ret < 0 (deny)"| DENY["short-circuit: return -EPERM"] P2 -->|"ret < 0 (deny)"| DENY SEL -->|"deny"| DENYO["short-circuit: bpf never runs"]
The path a guarded operation takes through the LSM chain with BPF-LSM active. What it shows: security_file_open() walks the registered modules in CONFIG_LSM order; SELinux and AppArmor run first, and only if they both return the hook’s default (allow) value does control reach bpf_lsm_file_open(), the noinline shim that the attached BPF programs hang off via a modify-return trampoline. The BPF programs chain — each sees the previous program’s return value, and the first one to return a negative errno short-circuits the whole chain into a denial. The insight to take: BPF-LSM is consulted late and can only deny — if a higher-priority LSM already denied, bpf_lsm_file_open() is never even reached (the kernel chain short-circuits on the first non-default return), so a BPF policy can tighten the system’s security but never loosen it.
KRSI: where BPF-LSM came from
BPF-LSM grew out of KRSI — Kernel Runtime Security Instrumentation — proposed by KP Singh (Google) at the 2019 Linux Security Summit North America (LWN 798157). The motivation was that the existing tools were a poor fit for detection and response security: SELinux/AppArmor express static policy but cannot be reprogrammed cheaply at runtime, while audit and tracing could observe but not enforce. KRSI’s insight was that the LSM framework already provides exactly the right enforcement points — the security_*() hooks are the kernel’s curated list of “operations worth a policy decision” — so if you could attach a verified BPF program to each hook, you would get programmable MAC and audit using machinery that already existed. As LWN summarized the prototype, “a prototype of KRSI is implemented as a Linux security module (LSM) that allows eBPF programs to be attached to the kernel’s security hooks” (LWN 798157). KRSI was merged into mainline as the bpf LSM and BPF_PROG_TYPE_LSM in Linux 5.7 (2020); the in-tree documentation file (Documentation/bpf/prog_lsm.rst) still carries the 2020 Google copyright.
Mechanical Walk-through: how a BPF program becomes a security verdict
1. The static hooks become noinline anchor functions
The framework defines every LSM hook once, in include/linux/lsm_hook_defs.h, as an LSM_HOOK(...) macro line. kernel/bpf/bpf_lsm.c includes that header with a macro definition that turns each hook into a noinline nop function named bpf_lsm_<hook> that simply returns the hook’s default value (bpf_lsm.c, v6.12):
#define LSM_HOOK(RET, DEFAULT, NAME, ...) \
noinline RET bpf_lsm_##NAME(__VA_ARGS__) \
{ \
return DEFAULT; \
}
#include <linux/lsm_hook_defs.h>So for the file_open hook the kernel gets a real symbol bpf_lsm_file_open() that, with no BPF program attached, returns 0 (allow). These functions are deliberately noinline so they have a stable address a BPF trampoline can patch. A second include of the same header builds a BTF id set, bpf_lsm_hooks, enumerating every one of these functions; that set is the verifier’s whitelist of legal attach targets.
2. security/bpf/hooks.c wires the anchors into the LSM chain
The bpf LSM registers itself as a normal security module whose hook callbacks are those bpf_lsm_<hook> anchor functions (hooks.c, v6.12):
static struct security_hook_list bpf_lsm_hooks[] __ro_after_init = {
#define LSM_HOOK(RET, DEFAULT, NAME, ...) \
LSM_HOOK_INIT(NAME, bpf_lsm_##NAME),
#include <linux/lsm_hook_defs.h>
#undef LSM_HOOK
LSM_HOOK_INIT(inode_free_security, bpf_inode_storage_free),
LSM_HOOK_INIT(task_free, bpf_task_storage_free),
};
static const struct lsm_id bpf_lsmid = { .name = "bpf", .id = LSM_ID_BPF };
DEFINE_LSM(bpf) = { .name = "bpf", .init = bpf_lsm_init, .blobs = &bpf_lsm_blob_sizes };When bpf_lsm_init() runs, it calls security_add_hooks(...), slotting bpf_lsm_file_open (and every sibling) into the framework’s per-hook static-call tables. So security_file_open() will, as part of its chain, call bpf_lsm_file_open() — which is exactly the nop anchor a BPF program can later override. The two extra hooks (inode_free_security, task_free) free the per-object BPF local storage that LSM programs can attach to inodes and tasks.
3. Attaching a program rewrites the anchor with a modify-return trampoline
When you attach a BPF_PROG_TYPE_LSM/BPF_LSM_MAC program to bpf_lsm_file_open, the kernel builds a BPF trampoline at that function’s entry. The trampoline type is the critical detail: bpf_attach_type_to_tramp() in kernel/bpf/trampoline.c maps BPF_LSM_MAC to BPF_TRAMP_MODIFY_RETURN (trampoline.c, v6.12, lines 513–520) — the same machinery as an fmod_ret program, not a passive fexit. A modify-return trampoline runs the BPF program before the original function body and lets the program substitute the return value: if the program returns non-zero, the trampoline returns that value and skips the original. This is precisely what makes BPF-LSM an enforcement mechanism and not just observation — a program returning -EPERM causes bpf_lsm_file_open() to return -EPERM, which the framework treats as a denial.
When multiple programs are attached to the same hook they form a modify-return chain. Each program receives the previous program’s return value as an extra trailing argument, and the chain short-circuits on the first non-zero return. The doc’s canonical example makes this explicit (prog_lsm.rst, v6.12):
SEC("lsm/file_mprotect")
int BPF_PROG(mprotect_audit, struct vm_area_struct *vma,
unsigned long reqprot, unsigned long prot, int ret)
{
/* ret is the return value from the previous BPF program
* or 0 if it's the first hook.
*/
if (ret != 0)
return ret;
...
if (is_heap)
return -EPERM;
return 0;
}Line by line: SEC("lsm/file_mprotect") tells libbpf the program type is BPF_PROG_TYPE_LSM and the attach target is the file_mprotect hook (resolved to the BTF id of bpf_lsm_file_mprotect). BPF_PROG(...) is a libbpf macro that unpacks the trampoline’s context into the named typed arguments. The first three arguments mirror the real file_mprotect LSM hook signature; the fourth argument ret is the modify-return chaining value — the verdict of whatever BPF program ran before this one. The early if (ret != 0) return ret; honors a prior denial. Returning -EPERM denies the mprotect; returning 0 lets the chain (and ultimately the operation) continue.
4. The verifier confines the return value
Because the return value is a security decision, the verifier checks it tightly. bpf_lsm_get_retval_range() decides the allowed range per hook (bpf_lsm.c, v6.12):
int bpf_lsm_get_retval_range(const struct bpf_prog *prog,
struct bpf_retval_range *retval_range)
{
if (!prog->aux->attach_func_proto->type)
return -EINVAL; /* void hook: no return value */
if (btf_id_set_contains(&bool_lsm_hooks, prog->aux->attach_btf_id)) {
retval_range->minval = 0;
retval_range->maxval = 1; /* boolean hooks: 0 or 1 */
} else {
/* the common case: 0 on success, negative errno on failure */
retval_range->minval = -MAX_ERRNO;
retval_range->maxval = 0;
}
return 0;
}So for the overwhelming majority of hooks the verifier rejects, at load time, any program whose R0 could leave the range [-MAX_ERRNO, 0] — meaning the only legal verdicts are 0 (allow) or a negative errno (deny). A small set of boolean hooks (e.g. inode_xattr_skipcap, and the audit/xfrm match hooks listed in bool_lsm_hooks) instead return 0 or 1. Hooks that the framework declares void (no decision) cannot return anything; the verifier enforces that too (check_return_code() in kernel/bpf/verifier.c handles the BPF_PROG_TYPE_LSM case by calling get_func_retval_range).
5. The kernel-wide chain: call_int_hook and short-circuiting
The outer aggregation lives in security/security.c. Each security_*() function expands the call_int_hook macro, which walks the registered LSMs and stops at the first one whose return differs from the hook’s default (security.c, v6.12):
#define __CALL_STATIC_INT(NUM, R, HOOK, LABEL, ...) \
do { \
if (static_branch_unlikely(&SECURITY_HOOK_ACTIVE_KEY(HOOK, NUM))) { \
R = static_call(LSM_STATIC_CALL(HOOK, NUM))(__VA_ARGS__); \
if (R != LSM_RET_DEFAULT(HOOK)) \
goto LABEL; /* first non-default wins */ \
} \
} while (0);The hook’s default (LSM_RET_DEFAULT) is, for almost all access-control hooks, 0 (allow). So a module that wants to deny returns a negative errno, which is != 0, which triggers the goto and short-circuits the chain with that errno. Two consequences for BPF-LSM follow directly from this code:
- It can only restrict. Because the chain stops at the first denial, a BPF program cannot turn a prior LSM’s
-EPERMback into0— by the time the chain would reachbpf_lsm_file_open(), thegotohas already jumped past it. BPF-LSM is reached only for operations the earlier LSMs permitted, and its only power then is to deny. - Order matters. Where
bpfsits inCONFIG_LSMdecides whether it runs before or after SELinux/AppArmor. In the default ordering (below),bpfis last.
Configuration: enabling BPF-LSM and the lsm= ordering
CONFIG_BPF_LSM
The feature is compiled in by CONFIG_BPF_LSM, defined in kernel/bpf/Kconfig (Kconfig, v6.12):
config BPF_LSM
bool "Enable BPF LSM Instrumentation"
depends on BPF_EVENTS
depends on BPF_SYSCALL
depends on SECURITY
depends on BPF_JIT
The dependencies are instructive: BPF-LSM needs the BPF syscall and the JIT (the verifier and trampolines), the LSM framework (SECURITY), and BPF_EVENTS (the tracing/BTF machinery that resolves hooks by name). With this off, the bpf_lsm_* anchors and DEFINE_LSM(bpf) are compiled out entirely.
Why "bpf" must be in CONFIG_LSM / the lsm= boot list
Compiling the module in is necessary but not sufficient. The LSM framework only initializes modules that appear in the ordered CONFIG_LSM string (overridable at boot with the lsm= kernel command-line parameter). A module left off that list “will be ignored,” per the Kconfig help text (security/Kconfig, v6.12). The v6.12 default (for the common SELinux-default build) is:
default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,ipe,bpf"
Two things to read off this string. First, bpf is present by default in 6.12 — so on a stock kernel with CONFIG_BPF_LSM=y, BPF-LSM is enabled. (Historically, on some older or hardened configs bpf was not in the default list, which is the origin of the widespread advice “add bpf to your lsm= line”; on 6.12 the default already includes it, but a lockdown- or distro-customized CONFIG_LSM may not, so verify with cat /sys/kernel/security/lsm.) Second, bpf is last. The string is the initialization and evaluation order, so SELinux, Smack, TOMOYO, AppArmor, and IPE all get to vote before bpf. That is the concrete source of the “can only further restrict” property: by position, BPF-LSM never overrides the major MACs.
Uncertain
Verify: that the
lsm=ordering exactly equals the LSM evaluation order for every hook on 6.12 (i.e. that no hook reorders modules). Reason: thecall_int_hookwalk uses per-hook static-call tables populated inCONFIG_LSMorder, which is the documented behavior, but I did not trace every hook’s table population. To resolve: readlsm_for_each_hook/security_add_hooksordering insecurity/security.cand confirm the static-call slots are filled strictly inordered_lsm_init()order. The “bpf runs after the major LSMs by default” conclusion is robust regardless, sincebpfis last in the string. uncertain
To inspect the active set and order on a running system: cat /sys/kernel/security/lsm prints the comma-separated list of initialized modules in order; if bpf is absent, BPF-LSM programs will fail to attach. To override at boot: lsm=...,bpf on the kernel command line.
Failure Modes and Common Misunderstandings
- “My BPF-LSM program loaded but never denies anything.” The most common cause is that
bpfis not in the active LSM list — check/sys/kernel/security/lsm. Ifbpfis missing, thebpf_lsm_*anchors were never wired into the chain, so your program is attached to a function the framework never calls. AddbpftoCONFIG_LSM/lsm=. - “Returning a positive value to allow.” Allow is
0, deny is a negative errno. Returning a positive value will fail the verifier’s range check ([-MAX_ERRNO, 0]) at load for ordinary hooks, except for the handful of boolean hooks that expect0/1. The mental shortcut “non-zero allows” is wrong for LSM — non-zero denies. - “Attaching to a hook the verifier rejects with
attach_btf_id ... points to disabled hook.” Abpf_lsm_disabled_hooksBTF set inbpf_lsm.cexplicitly forbids attaching to certain hooks (e.g.vm_enough_memory,getprocattr,setprocattr,inode_getsecurity) because their semantics or calling context make a BPF override unsafe (bpf_lsm.c, v6.12). The verifier returnsattach_btf_id %u points to disabled hook. - “
LSM programs must have a GPL compatible license.”bpf_lsm_verify_prog()rejects non-GPL programs outright — BPF-LSM is a GPL-only surface. Setchar _license[] SEC("license") = "GPL";. - Sleepable vs non-sleepable hooks. Some LSM hooks run in a context where the BPF program may sleep (e.g. to read a file hash via
bpf_ima_file_hash), and only those — listed in thesleepable_lsm_hooksBTF set — acceptSEC("lsm.s/...")sleepable programs. Attaching a sleepable program to a non-sleepable hook fails verification. - Confusing the two short-circuits. A denial inside the BPF chain (one of your programs returns
-EPERM) is different from the kernel-wide short-circuit (SELinux denied, sobpfnever ran). If your audit logging program “misses” events, it may be because a higher-priority LSM denied first and the chain never reachedbpf.
Alternatives and When to Choose Them
- vs. SELinux / AppArmor. The major MACs (SELinux, AppArmor) express static, comprehensive policy compiled from a dedicated language and loaded as a labeled or path-based ruleset. BPF-LSM is programmable and dynamic: you load, update, and unload policy as code at runtime, with full access to BPF maps and helpers for stateful decisions (rate limits, allow-lists keyed on arbitrary data). Choose the major MACs for a complete, audited, distribution-supported policy; choose BPF-LSM for targeted, fast-iterating, observability-driven enforcement — and note they coexist: BPF-LSM runs after them and can only tighten.
- vs. seccomp. seccomp filters by syscall number and argument registers using classic BPF, before the syscall does its work. BPF-LSM hooks the semantic operations deeper in the kernel (after argument resolution: a real
struct file, a realstruct inode), so it can make decisions seccomp cannot (e.g. “deny opening files under this mount” rather than “deny theopenatsyscall”). seccomp is unprivileged and per-process; BPF-LSM is privileged and system-wide. - vs. kprobe/fentry tracing. A kprobe or fentry program can observe a security-relevant function but cannot reliably deny the operation. BPF-LSM is the enforcement-grade equivalent: it attaches at curated, stable decision points and its return value is honored as a verdict.
Production Notes
BPF-LSM is the foundation of runtime-security tooling such as Cilium Tetragon and KubeArmor, which load BPF-LSM (and tracing) programs to enforce and observe process-execution, file-access, and capability policy in Kubernetes clusters. The pattern in production is: ship a small static BPF object built with CO-RE against vmlinux BTF, attach to a curated set of hooks (bprm_check_security for exec control, file_open/path_* for file policy, socket_connect/socket_bind for network policy, task_prctl/capable for privilege policy), and stream denials to userspace via the ring buffer. Because BPF-LSM runs after the major MACs, operators commonly run it with SELinux/AppArmor rather than instead of them, using BPF-LSM for the policies the static MACs cannot express conveniently and updating those policies without recompiling a system policy. The chief operational caveat is the ABI-by-BTF coupling: hooks are addressed by BTF id, so a policy compiled against one kernel’s BTF must be re-relocated (via CO-RE) for another — and the set of attachable hooks can change across kernel versions, so a hook present on 6.12 is not guaranteed on an older or newer kernel.
See Also
- BPF Program Types — the §5 dispatcher;
BPF_PROG_TYPE_LSMis one entry inenum bpf_prog_type - struct_ops and sched_ext — sibling §5 program type; the other BTF-targeted, non-event-driven program family
- fentry fexit and BPF Trampolines — BPF-LSM is built on the same modify-return trampoline machinery (
BPF_TRAMP_MODIFY_RETURN) - BTF (BPF Type Format) — supplies the
attach_btf_idthat targets a specificbpf_lsm_<hook>anchor - BPF Links and Attachment Lifecycle —
bpf_program__attach_lsmreturns a link that detaches on close - The Linux Security Module Framework · LSM Hooks and the security_ Call Sites · LSM Stacking and Module Ordering — the LSM framework this module plugs into (owned by Linux Security MOC)
- SELinux · AppArmor · Seccomp and seccomp-BPF — the static MACs and syscall filter BPF-LSM complements
- Linux eBPF MOC — parent MOC (§5 Program Types and Attach Points) · Linux Security MOC — the security-domain parent (§I Programmable Security)