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.c registers it with DEFINE_LSM(bpf) (hooks.c, v6.12) — that runs alongside SELinux and AppArmor in the same hook chain rather than replacing them. The program type is BPF_PROG_TYPE_LSM with expected attach type BPF_LSM_MAC (bpf.h, v6.12); a program targets a specific hook by BTF id and returns 0 to 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: any bpf_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 bpf LSM (program type BPF_PROG_TYPE_LSM) first appeared in Linux 5.7 — verified by a boundary check: kernel/bpf/bpf_lsm.c does not exist at the v5.6 tag (404) but is present at v5.7. The set of which security_* 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 -EPERM back into 0 — by the time the chain would reach bpf_lsm_file_open(), the goto has 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 bpf sits in CONFIG_LSM decides whether it runs before or after SELinux/AppArmor. In the default ordering (below), bpf is 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: the call_int_hook walk uses per-hook static-call tables populated in CONFIG_LSM order, which is the documented behavior, but I did not trace every hook’s table population. To resolve: read lsm_for_each_hook/security_add_hooks ordering in security/security.c and confirm the static-call slots are filled strictly in ordered_lsm_init() order. The “bpf runs after the major LSMs by default” conclusion is robust regardless, since bpf is 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 bpf is not in the active LSM list — check /sys/kernel/security/lsm. If bpf is missing, the bpf_lsm_* anchors were never wired into the chain, so your program is attached to a function the framework never calls. Add bpf to CONFIG_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 expect 0/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.” A bpf_lsm_disabled_hooks BTF set in bpf_lsm.c explicitly 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 returns attach_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. Set char _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 the sleepable_lsm_hooks BTF set — accept SEC("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, so bpf never ran). If your audit logging program “misses” events, it may be because a higher-priority LSM denied first and the chain never reached bpf.

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 real struct inode), so it can make decisions seccomp cannot (e.g. “deny opening files under this mount” rather than “deny the openat syscall”). 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