The Linux Security Module Framework
The Linux Security Module (LSM) framework is the kernel’s in-tree plumbing for mandatory access control (MAC): a set of hook points scattered through every security-relevant code path into which one or more security modules — SELinux, AppArmor, Smack, TOMOYO, Yama, Landlock, the BPF-LSM, lockdown, IPE, the integrity modules — register callbacks that get to veto an operation the classic UNIX permission checks would otherwise allow. The decisive design choice, and the source of the name, is that the framework itself enforces no policy. It only provides the registration machinery, the per-object storage, and the dispatch; what a denial means is entirely the module’s business. LSM exists because, at the Linux Kernel 2.5 Summit in 2001, the National Security Agency presented Security-Enhanced Linux and argued for merging it, and Linus Torvalds declined to bless any single security model — he instead required “a new infrastructure that would provide the necessary support to kernel modules for implementing security… avoid[ing] the need to choose among the existing competing projects” (Wright et al., USENIX Security 2002). That interface shipped as a standard part of Linux 2.6 in December 2003, with SELinux as its first user.
This note covers the framework as plumbing: the history and the three requirements Torvalds imposed, the restrictive-hook design property that makes MAC possible at all, the hook taxonomy, the registration mechanism (DEFINE_LSM, struct lsm_info, the __init-time ordering done by ordered_lsm_init()), the static-call dispatch that replaced the old hook-list walk, the per-object security blobs whose redesign made today’s LSM stacking possible, and the lsm_id-based userspace API added in 6.8. The companion note LSM Hooks and the security_ Call Sites goes deeper on the security_*() wrappers and macro expansion; LSM Stacking and Module Ordering on the ordering rules; Major vs Minor LSMs on the exclusivity classes. For the distinction the whole framework exists to serve, see Mandatory vs Discretionary Access Control.
Version pin. Everything here is verified against the Linux 6.12 source tree. 6.12 (released 2024-11-17) is a long-term-support (LTS) branch and is still receiving stable updates as of 2026-08, which is why it is the vault’s pin even though mainline has moved well past it. Where a fact changed after 6.12, the change is named with its release.
Mental Model — A Hook Rack, Not a Guard
The cleanest way to think about LSM is to separate two roles that newcomers conflate. The framework is a rack of empty hooks bolted to the kernel’s load-bearing walls: one beside the code that opens a file, one beside the code that creates a socket, one beside the code that sends a signal, and so on for 268 distinct decision points (counted directly: grep -c '^LSM_HOOK(' include/linux/lsm_hook_defs.h on the v6.12 tree returns 268). A security module is a coat hung on those hooks — a bundle of callback functions, one per hook it cares about, that the kernel will invoke at exactly that decision point. The framework guarantees the callback runs; the callback decides allow or deny. Remove every module and the rack is still there, but every hook does nothing and the kernel behaves as if LSM were absent (modulo the always-present capability module, discussed below).
flowchart TB subgraph KERNEL["Kernel decision points (the hooks)"] OPEN["do_dentry_open()<br/>→ security_file_open()"] SOCK["__sock_create()<br/>→ security_socket_create()"] KILL["do_send_sig_info()<br/>→ security_task_kill()"] end OPEN --> DISP["LSM dispatch<br/>(static-call table)"] SOCK --> DISP KILL --> DISP DISP -->|"per hook, in CONFIG_LSM order"| M1["SELinux callback"] DISP --> M2["AppArmor callback"] DISP --> M3["Landlock callback"] DISP --> M4["BPF-LSM programs"] M1 -->|"deny? → first non-zero wins"| RET["aggregate return"] M2 --> RET M3 --> RET M4 --> RET RET -->|"0 = allow / -EACCES = deny"| KERNEL
The framework as a hook rack. What it shows: the kernel calls a security_*() wrapper at each decision point; the wrapper dispatches through a shared table to every registered module’s callback for that hook, in a fixed order, and aggregates their verdicts. The insight: the framework is pure mechanism — it owns the rack and the dispatch, never the policy. SELinux, AppArmor, Landlock, and BPF-LSM are interchangeable coats; the kernel code beside the hook neither knows nor cares which modules are loaded.
Where the hook sits in a real syscall
The abstract picture above becomes concrete the moment you trace one system call. open("/etc/shadow", O_RDONLY) does not reach an LSM hook first, and that ordering is the whole reason MAC works the way it does.
flowchart TB SYS["open("/etc/shadow", O_RDONLY)<br/>→ do_sys_openat2()"] --> RESOLVE["path_openat()<br/>resolve name → struct dentry/inode"] RESOLVE --> MAYOPEN["may_open()"] MAYOPEN --> IPERM["inode_permission()<br/>generic_permission(): mode bits vs cred uid/gid"] IPERM -->|"DAC fails"| CAPCHK{"capable(CAP_DAC_OVERRIDE)?<br/>(a permissive hook — see below)"} CAPCHK -->|"no"| EACCES["-EACCES · LSM never consulted"] CAPCHK -->|"yes"| PASS IPERM -->|"DAC passes"| PASS["kernel is about to grant"] PASS --> HOOK["security_inode_permission()<br/>then, on the struct file:<br/>security_file_open()"] HOOK --> SEL{"SELinux / AppArmor /<br/>Landlock / BPF-LSM<br/>each return 0 or -errno"} SEL -->|"any non-zero"| DENY["first non-zero errno<br/>propagates out of open()"] SEL -->|"all zero"| GRANT["file handed back"]
One open() descending through the layers. What it shows: name resolution happens first, so the LSM sees a fully-resolved struct inode/struct file, not a user-supplied string; then DAC runs; then, only if the kernel was about to grant, the LSM hook is consulted. The insight to take: the placement is what makes LSM immune to time-of-check-to-time-of-use races (the paper’s stated motivation — hooking the syscall table instead “is not race-free, may require code duplication, and may not adequately express the full context needed to make security policy decisions”) and it is also why an LSM can only take permission away. A DAC failure short-circuits before the hook is ever reached.
Why It Exists — The “Whose Model Wins” Problem
By 2001 there were several serious, mutually incompatible Linux security projects: the NSA’s SELinux (type enforcement with a label on every object), DTE (domain and type enforcement), RSBAC, Medusa, LIDS, Openwall, and Immunix’s SubDomain (the ancestor of AppArmor). Each wanted its checks in the mainline kernel, and each implemented those checks differently. The USENIX paper written by the framework’s own authors states the problem plainly: “The union set of desired features would be highly functional, but also so invasive as to be unacceptable to the mainstream Linux community” (Wright et al. 2002, §2).
Torvalds’ answer was not “no” but “generalize it.” The paper records the three requirements he set out by email — worth quoting because every structural oddity of LSM traces back to one of them. The framework must be:
- “truly generic, where using a different security model is merely a matter of loading a different kernel module” — this is why there is a
union security_list_optionsof function pointers rather than a policy engine; - “conceptually simple, minimally invasive, and efficient” — this is why hooks are restrictive rather than authoritative (below), and why the 6.10-era static-call rework mattered so much;
- “able to support the existing POSIX.1e capabilities logic as an optional security module” — this is why the
capabilityLSM exists as an ordinary module rather than as special-cased kernel code, and, awkwardly, why the framework had to grow permissive hooks at all.
Extraction caveat, handled
The three-item list above is split across a two-column page break in the USENIX PDF.
pdftotext -layoutsilently interleaves it with figure captions and drops the middle bullet from its natural position; runningpdftotextwithout-layoutrecovers “conceptually simple, minimally invasive, and efficient; and” as a stranded line. Both extractions were cross-checked before quoting. This is the standard failure mode of two-column PDF extraction and is worth assuming, not discovering.
The framework was designed by a cross-vendor team — WireX (Wright, Cowan), NAI Labs (Smalley), Intercode (Morris), and the IBM Linux Technology Center (Kroah-Hartman), per the paper’s author affiliations — and was merged into Linux 2.6 in December 2003. Two architectural constraints from that era still define LSM today. First, hooks mediate access to internal kernel objects at the point the kernel is about to act on them, not by interposing on system-call entry. The paper’s phrasing of the abstraction is the one to memorize: LSM “seeks to allow modules to answer the question ‘May a subject S perform a kernel operation OP on an internal kernel object OBJ?’” Second, hooks sit after the classic discretionary checks, which produces the restrictive property covered next.
timeline title LSM framework milestones (verified releases) 2001 : Kernel 2.5 Summit — NSA presents SELinux; Torvalds asks for a generic framework instead 2002 : LSM design + implementation published at USENIX Security 11 2003 : LSM merged in Linux 2.6 (Dec) — SELinux is the first user 2010 : AppArmor merged in 2.6.36 after years out of tree 2018 : v4.20-era work begins on infrastructure-managed security blobs 2019 : v5.1 — infrastructure-managed blobs land, unblocking minor-LSM stacking 2021 : v5.13 — Landlock merged; unprivileged, stackable, ABI-versioned 2024 : v6.8 — lsm_id, LSM_ID_* numbers, and the three lsm_*_self_attr/list_modules syscalls 2024 : v6.10-6.11 — hook lists replaced by per-hook static-call arrays 2024 : v6.12 LTS — IPE (Integrity Policy Enforcement) merged; the tree this note is pinned to
The framework’s evolution. What it shows: LSM’s shape is the accumulation of four distinct waves — the 2001–2003 founding, the 2019 blob redesign, the 2021–2024 arrival of modern stackable modules, and the 2024 performance and userspace-API rework. The insight: any write-up that describes LSM as “one major module, chosen at boot, with hooks on a linked list” is describing the kernel as it was before 2019 and before 2024. Those are the two most common staleness bugs in LSM documentation, including in places you would not expect — see the callout on the kernel’s own admin guide below.
Restrictive, Not Authoritative — the Property That Defines MAC
The single most important structural fact about LSM is stated by its designers as a simplification, and it happens to be exactly what makes mandatory access control work.
“A consequence of the ‘stay simple’ design decision is that LSM hooks are primarily restrictive: where the kernel was about to grant access, the module may deny access, but when the kernel would deny access, the module is not consulted.” — Wright et al. 2002, §3
The alternative design, which LSM deliberately rejected, is the authoritative hook: a hook that replaces the kernel’s decision entirely and can turn a “no” into a “yes.” Authoritative hooks are strictly more expressive — RSBAC and Medusa both wanted them — but the paper explains why they lost: “Providing for authoritative hooks (where the module can override either decision) would require many more hooks into the Linux kernel,” because “the Linux kernel ‘short-circuits’ many decisions early when error conditions are detected.” Every early return -EACCES in the kernel would have needed a hook in front of it.
The consequence is the property administrators actually care about. Enabling an LSM can only ever remove permissions. A policy change cannot accidentally grant a process access DAC refused; if a process suddenly cannot do something after a policy load, the policy denied it. That is why an SELinux type or an AppArmor profile can confine even a process whose owner would have permitted the action — the definition of “mandatory” in Mandatory vs Discretionary Access Control.
But “LSM can only restrict” is a near-truth, and the honest version is more interesting. The paper documents a deliberate exception:
“However, the POSIX.1e capabilities logic requires the ability to grant accesses that would ordinarily be denied at a coarse level of granularity. In order to support this logic as a security module, LSM provides some minimal support for these permissive hooks, where the module can grant access the kernel was about to deny. The permissive hooks are typically coupled with a simple DAC check, and allow the module to override the DAC restriction… These hooks are limited to the extent that the kernel already consults the POSIX.1e
capable()function.”
flowchart LR subgraph R["Restrictive hook — the overwhelming majority"] R1["kernel about to GRANT"] --> R2{"LSM callback"} R2 -->|"return 0"| R3["granted"] R2 -->|"return -EACCES"| R4["denied"] R5["kernel about to DENY"] --> R6["denied<br/>(LSM never called)"] end subgraph P["Permissive hook — security_capable() only"] P1["DAC check FAILED"] --> P2{"security_capable(CAP_DAC_OVERRIDE)"} P2 -->|"return 0"| P3["GRANTED anyway"] P2 -->|"return -EPERM"| P4["denied"] end
Restrictive vs permissive hooks. What it shows: the restrictive hook is a veto placed on the grant path; the permissive hook is an override placed on the deny path, and it exists in exactly one family — the capability check. The insight: “an LSM can only take permission away” is true of the 267 restrictive hooks and false of security_capable(), whose whole job is to let the capability module hand a process a privilege the mode bits refused. This is not a loophole for MAC modules: SELinux and AppArmor implement capable restrictively (they can veto a capability the baseline granted, not conjure one), so the composite behaviour is still “capabilities can widen, MACs can only narrow.”
Note the interaction: because the modules run in CONFIG_LSM order and the capability module is always first, the capability module’s permissive answer is produced before SELinux or AppArmor sees the hook, and their restrictive answers can still veto it. The ordering is not incidental.
The Hook Taxonomy — What 268 Hooks Actually Cover
Hooks are declared once, in include/linux/lsm_hook_defs.h, as a list of LSM_HOOK(RET, DEFAULT, NAME, args...) macro invocations. That single file is the framework’s schema: it is #included several times with different definitions of LSM_HOOK to generate the function-pointer union, the static-call table, and the per-hook wrappers. Counting the v6.12 file and grouping by the object each hook mediates gives the framework’s real surface area:
| Object family | Hooks | Representative hooks | What it mediates |
|---|---|---|---|
inode_* | 45 | inode_permission, inode_create, inode_unlink, inode_getattr, inode_setxattr | Every filesystem-object operation, on the resolved inode |
task_* | 20 | task_kill, task_setnice, task_prctl, task_getsid | Process-to-process operations and per-task attributes |
socket_* | 17 | socket_create, socket_bind, socket_connect, socket_sendmsg | The BSD socket API surface |
sb_* | 15 | sb_mount, sb_umount, sb_remount, sb_pivotroot | Superblock / mount-namespace operations |
file_* | 15 | file_open, file_permission, file_ioctl, file_mprotect | Operations on an already-open struct file |
path_* | 13 | path_mknod, path_symlink, path_chmod, path_link | Pathname-flavoured variants (CONFIG_SECURITY_PATH), used by AppArmor and TOMOYO |
xfrm_* | 11 | xfrm_policy_alloc_security, xfrm_state_pol_flow_match | IPsec policy and state labelling |
bpf_* | 11 | bpf_map, bpf_prog_load, bpf_token_create | eBPF object access (Linux eBPF MOC) |
msg_*, shm_*, sem_*, ipc_* | 20 | shm_shmat, msg_queue_msgsnd, ipc_permission | System V IPC |
cred_*, bprm_* | 10 | bprm_creds_for_exec, cred_prepare, cred_transfer | Credential lifecycle and execve transitions |
kernel_* | 7 | kernel_module_request, kernel_read_file, kernel_load_data | Loading modules, firmware, and other kernel-consumed files |
key_* | 4 | key_alloc, key_permission, key_getsecurity | Kernel keyrings |
perf_event_* | 4 | perf_event_open, perf_event_read | Performance-event access |
uring_* | 3 | uring_override_creds, uring_sqpoll, uring_cmd | io_uring, added because it bypasses ordinary syscall paths |
| Everything else | ~73 | capable, settime, syslog, userns_create, locked_down, getselfattr, setselfattr, binder_*, audit_*, tun_dev_*, sctp_*, secmark_*, bdev_*, ib_* | Cross-cutting checks with no single object family |
Hook counts by object family in v6.12 lsm_hook_defs.h (268 total). What it shows: the distribution is not uniform — filesystem objects alone account for roughly a third of the surface, and the tail is a long list of subsystem-specific checks accreted over twenty years. The insight: the hook set is not a designed taxonomy; it is a record of every place a security project needed to say no. New entries appear whenever a subsystem opens a new privileged path — userns_create (added in 6.1) and the three uring_* hooks exist because user namespaces and io_uring created privilege surfaces the older hooks could not see.
Two entries in that tail matter disproportionately. security_capable() is the permissive hook discussed above and the mechanism behind every CAP_* check (POSIX Capabilities). security_locked_down() is consulted by the lockdown LSM and carries an enum of 30 named reasons defined at the top of security/security.c — from LOCKDOWN_MODULE_SIGNATURE (“unsigned module loading”) through LOCKDOWN_KEXEC, LOCKDOWN_BPF_WRITE_USER, and LOCKDOWN_KCORE — split into an integrity half and a confidentiality half by the LOCKDOWN_INTEGRITY_MAX and LOCKDOWN_CONFIDENTIALITY_MAX sentinels. The reason strings live in the framework rather than in the lockdown module specifically “to allow all security modules to use the same descriptions for auditing purposes” (comment in security/security.c).
The Data Structures — lsm_id, security_hook_list, lsm_static_call
Four small structures in include/linux/lsm_hooks.h carry the whole framework. Understanding how they point at each other is understanding LSM.
classDiagram class lsm_info { +const char* name +enum lsm_order order +unsigned long flags +int* enabled +int (*init)(void) +lsm_blob_sizes* blobs } class lsm_id { +const char* name +u64 id } class security_hook_list { +lsm_static_call* scalls +union security_list_options hook +const lsm_id* lsmid } class lsm_static_call { +static_call_key* key +void* trampoline +security_hook_list* hl +static_key_false* active } class lsm_static_calls_table { +lsm_static_call file_open[MAX_LSM_COUNT] +lsm_static_call inode_permission[MAX_LSM_COUNT] +lsm_static_call ...268 arrays } class lsm_blob_sizes { +int lbs_cred +int lbs_file +int lbs_inode +int lbs_task +int lbs_sock +int ...10 more } lsm_info --> lsm_blob_sizes : declares storage need lsm_info ..> lsm_id : module registers both security_hook_list --> lsm_id : who owns this callback security_hook_list --> lsm_static_call : scalls = the per-hook slot array lsm_static_calls_table *-- lsm_static_call : one array per hook
The framework’s object graph at v6.12. What it shows: lsm_info is the boot-time descriptor (what to initialise, in what order, how much blob space); lsm_id is the identity used at runtime and exported to userspace; security_hook_list is one callback plus a pointer to the array of static-call slots for its hook; and lsm_static_calls_table is a struct with one fixed-size array per hook. The insight: the callback and its identity are separate objects on purpose. security_add_hooks() stamps hooks[i].lsmid = lsmid for every hook a module registers, so at audit time the framework can name which module produced a denial without the module having to say so.
The lsm_id structure and the LSM_ID_* numbers in include/uapi/linux/lsm.h are a genuinely recent addition (merged for 6.8, authored by Casey Schaufler). The uapi header allocates a stable numeric identity to each in-tree module — LSM_ID_CAPABILITY 100, LSM_ID_SELINUX 101, LSM_ID_SMACK 102, LSM_ID_TOMOYO 103, LSM_ID_APPARMOR 104, LSM_ID_YAMA 105, LSM_ID_LOADPIN 106, LSM_ID_SAFESETID 107, LSM_ID_LOCKDOWN 108, LSM_ID_BPF 109, LSM_ID_LANDLOCK 110, LSM_ID_IMA 111, LSM_ID_EVM 112, LSM_ID_IPE 113 — with the header reserving “values 1–99… for potential future use” and treating 0 (LSM_ID_UNDEF) as invalid. Before this, “which LSM said no?” was answerable only by parsing a comma-separated string out of /sys/kernel/security/lsm; now it is a number in an ABI.
Static-call dispatch — how the callback actually gets called
Older descriptions of LSM (and a great deal of still-circulating tutorial material) say that each hook owns an RCU-protected linked list of security_hook_list nodes, and that call_int_hook walks it making indirect calls. That was true through roughly 6.9 and is no longer how v6.12 works. The list was replaced by a per-hook array of static calls — self-modifying direct call sites patched at boot — because indirect calls through a function pointer must go through a retpoline on Spectre-mitigated hardware, and LSM hooks sit in the hottest paths in the kernel.
The generated machinery, all in security/security.c, works like this:
/* one static call + one static key per (hook, slot), unrolled MAX_LSM_COUNT times */
#define DEFINE_LSM_STATIC_CALL(NUM, NAME, RET, ...) \
DEFINE_STATIC_CALL_NULL(LSM_STATIC_CALL(NAME, NUM), \
*((RET(*)(__VA_ARGS__))NULL)); \
DEFINE_STATIC_KEY_FALSE(SECURITY_HOOK_ACTIVE_KEY(NAME, NUM));MAX_LSM_COUNT is not a magic constant — it is computed at compile time by include/linux/lsm_count.h, a header of pure preprocessor arithmetic that emits a 1, token for each of the fourteen in-tree LSMs whose CONFIG_ symbol is enabled (capability, SELinux, Smack, AppArmor, TOMOYO, Yama, LoadPin, lockdown, SafeSetID, BPF-LSM, Landlock, IMA, EVM, IPE) and then counts the tokens with COUNT_ARGS(). A kernel built with only capability + AppArmor + Yama gets MAX_LSM_COUNT == 3 and three slots per hook; the table costs nothing for modules you did not build.
At registration time lsm_static_call_init() walks a hook’s slot array, finds the first slot whose ->hl is still NULL, patches that call site to point at the module’s callback via __static_call_update(), and flips the slot’s static key on:
static void __init lsm_static_call_init(struct security_hook_list *hl)
{
struct lsm_static_call *scall = hl->scalls;
int i;
for (i = 0; i < MAX_LSM_COUNT; i++) {
/* Update the first static call that is not used yet */
if (!scall->hl) {
__static_call_update(scall->key, scall->trampoline,
hl->hook.lsm_func_addr);
scall->hl = hl;
static_branch_enable(scall->active);
return;
}
scall++;
}
panic("%s - Ran out of static slots.\n", __func__);
}sequenceDiagram autonumber participant K as Kernel call site<br/>(do_dentry_open) participant W as security_file_open() participant S0 as slot 0 (capability) participant S1 as slot 1 (AppArmor) participant S2 as slot 2 (Landlock) participant S3 as slot 3 (unused) K->>W: security_file_open(file) Note over W: RC = LSM_RET_DEFAULT (0) W->>S0: static_branch active? yes → direct call S0-->>W: 0 (allow) W->>S1: active? yes → direct call S1-->>W: -EACCES (deny) Note over W: first non-zero return short-circuits W-->>K: -EACCES Note over S2,S3: slots 2 and 3 are never reached Note over S3: inactive slot: static key false,<br/>the call is compiled out of the hot path
Dispatch through the static-call array for one hook. What it shows: the wrapper walks slots in registration order, each slot is a patched direct call rather than an indirect one, and the first non-zero return ends the walk. Unused slots are guarded by a static key that is never enabled, so on a kernel with three active LSMs the fourth slot costs a nop, not a branch. The insight: ordering is semantically load-bearing — a module earlier in CONFIG_LSM gets to deny before a later module is ever consulted, so the denial you see in the audit log is the first denial, not necessarily the only one. Two modules can both object and only one will ever be logged.
The mechanics of the LSM_HOOK macro expansion and the call_int_hook / call_void_hook wrappers are covered in full in LSM Hooks and the security_ Call Sites; the point here is structural. There is one more nuance worth stating because it surprises people: because the slot array is __ro_after_init and filled during __init, the active hook set is frozen once boot completes. There is no supported way to add an LSM callback to a running kernel — except through BPF-LSM, whose single registered callback per hook then dispatches to a runtime-loadable set of eBPF programs (BPF-LSM).
The Registration Mechanism — DEFINE_LSM and struct lsm_info
A module advertises itself to the framework with a single static descriptor placed in a special linker section. The macro, from include/linux/lsm_hooks.h at v6.12, is:
#define DEFINE_LSM(lsm) \
static struct lsm_info __lsm_##lsm \
__used __section(".lsm_info.init") \
__aligned(sizeof(unsigned long))Reading it line by line: DEFINE_LSM(selinux) declares a static struct lsm_info named __lsm_selinux; __used tells the compiler not to discard it even though nothing references it by name; __section(".lsm_info.init") forces it into a dedicated ELF section; and __aligned(...) pins its alignment so the framework can walk the section as a plain array. There is a parallel DEFINE_EARLY_LSM(lsm) that targets .early_lsm_info.init for the handful of modules (capability, integrity) that must initialise before the ordered pass. The descriptor itself is:
struct lsm_info {
const char *name; /* Required. */
enum lsm_order order; /* Optional: default is LSM_ORDER_MUTABLE */
unsigned long flags; /* Optional: flags describing LSM */
int *enabled; /* Optional: controlled by CONFIG_LSM */
int (*init)(void); /* Required. */
struct lsm_blob_sizes *blobs; /* Optional: for blob sharing. */
};A concrete registration looks like DEFINE_LSM(selinux) = { .name = "selinux", .flags = LSM_FLAG_LEGACY_MAJOR, .enabled = &selinux_enabled_boot, .blobs = &selinux_blob_sizes, .init = selinux_init };. The linker collects every such descriptor between the symbols __start_lsm_info and __end_lsm_info, and the framework iterates that range at boot — no central registry, no list to keep in sync. Adding a new LSM to the tree is, mechanically, defining one lsm_info, one lsm_id, and a set of callbacks.
flowchart TB subgraph SRC["Per-module source files"] A["security/selinux/hooks.c<br/>DEFINE_LSM(selinux) = {...}"] B["security/apparmor/lsm.c<br/>DEFINE_LSM(apparmor) = {...}"] C["security/landlock/setup.c<br/>DEFINE_LSM(landlock) = {...}"] D["security/commoncap.c<br/>DEFINE_EARLY_LSM(capability) = {...}"] end A --> SEC[".lsm_info.init<br/>ELF section"] B --> SEC C --> SEC D --> ESEC[".early_lsm_info.init"] SEC --> SYM["linker emits<br/>__start_lsm_info / __end_lsm_info"] ESEC --> ESYM["__start_early_lsm_info /<br/>__end_early_lsm_info"] ESYM --> EARLY["early_security_init()<br/>prepare + initialize immediately"] SYM --> PARSE["ordered_lsm_parse()<br/>reorder per CONFIG_LSM / lsm="] EARLY --> PARSE PARSE --> ORD["ordered_lsms[]<br/>the boot order"]
How a module reaches the framework without a registry. What it shows: each module drops one descriptor into a named ELF section; the linker concatenates them and hands the framework a start/end pair; the framework then reorders that array according to a configuration string. The insight: the linker’s layout order is meaningless — it is an unordered bag. The real order is computed at boot from CONFIG_LSM, which is why “which LSM runs first” is a boot-time question, not a build-layout question.
The two flags encode the exclusivity rules. LSM_FLAG_LEGACY_MAJOR (BIT(0)) marks a module that can be selected by the legacy security= boot parameter — historically the “one major LSM” slot. LSM_FLAG_EXCLUSIVE (BIT(1)) marks a module of which at most one may be active, because it manages object labels in a way that conflicted with sharing before the blob redesign — SELinux, AppArmor, Smack, and TOMOYO are the exclusive set. Minor modules (Yama, Landlock, LoadPin, SafeSetID, lockdown, the capability module, BPF-LSM, IPE) carry neither flag and stack freely (Major vs Minor LSMs).
Boot-Time Ordering — ordered_lsm_init()
The order in which modules initialise — and therefore the order their callbacks occupy static-call slots, and therefore the order they run at every hook — is not the order the linker happened to lay them out. It is computed at boot by ordered_lsm_init() in security/security.c from a single ordered string: the CONFIG_LSM build-time default, overridable by the lsm= kernel command line. From the v6.12 security/Kconfig, the fallback default is:
"landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,ipe,bpf"
with four variants that move the chosen default-security module earlier: the AppArmor variant is landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,ipe,bpf, the Smack variant puts smack ahead of selinux, the TOMOYO variant drops the other three majors entirely (landlock,lockdown,yama,loadpin,safesetid,tomoyo,ipe,bpf), and the DAC variant lists no major at all. Note ipe — Integrity Policy Enforcement, a new LSM merged for 6.12 — appears in every v6.12 default list, which is one of the easiest ways to fingerprint a 6.12-era tree against an older one.
flowchart TB START["security_init()"] --> EARLY["early_security_init() already ran:<br/>capability + integrity prepared & initialised"] EARLY --> Q{"lsm= on cmdline?"} Q -->|"yes"| CMD["ordered_lsm_parse(cmdline)<br/>security= is ignored, with a pr_warn"] Q -->|"no"| BUILT["ordered_lsm_parse(CONFIG_LSM)"] CMD --> P1 BUILT --> P1 subgraph PARSE["ordered_lsm_parse()"] P1["1· append every LSM_ORDER_FIRST module<br/>(capability only)"] P1 --> P2["2· if security=X given, disable every OTHER<br/>LSM_FLAG_LEGACY_MAJOR module"] P2 --> P3["3· walk the comma list; append each<br/>LSM_ORDER_MUTABLE match in string order"] P3 --> P4["4· append security=X if not already listed"] P4 --> P5["5· append every LSM_ORDER_LAST module<br/>(integrity only)"] P5 --> P6["6· set_enabled(false) on every LSM<br/>not in the ordered list"] end P6 --> PREP["for each: prepare_lsm()"] subgraph PREPARE["prepare_lsm()"] PR1{"lsm_allowed()?<br/>enabled AND not blocked<br/>by an earlier EXCLUSIVE"} PR1 -->|"no"| PR2["set_enabled(false) — silently skipped"] PR1 -->|"yes"| PR3["claim the exclusive slot if EXCLUSIVE"] PR3 --> PR4["lsm_set_blob_sizes(): accumulate this<br/>module's per-object byte needs"] end PREP --> PREPARE PREPARE --> REPORT["report_lsm_order() → dmesg 'LSM: initializing lsm=...'"] REPORT --> CACHE["kmem_cache_create('lsm_file_cache', lbs_file)<br/>kmem_cache_create('lsm_inode_cache', lbs_inode)<br/>— sizes are only knowable NOW"] CACHE --> EC["lsm_early_cred(current->cred)<br/>lsm_early_task(current)"] EC --> INIT["for each: initialize_lsm() → lsm->init()<br/>SELinux loads initial policy, AppArmor sets up ns, ..."]
The boot-time ordering and sizing pass. What it shows: parsing, exclusivity arbitration, and blob sizing all happen in one pass before any module’s init() runs, and the slab caches for the inode and file blobs cannot be created until every enabled module has declared its storage needs. The insight: ordering and memory layout are the same decision. This is why you cannot enable an LSM after boot — its blob offsets would have to be inserted into already-allocated inodes.
Two details in that walk repeatedly bite people. First, lsm= silently supersedes security=: if both are given, ordered_lsm_init() sets chosen_major_lsm = NULL and emits pr_warn("security=%s is ignored because it is superseded by lsm=%s\n", ...). Second, an exclusivity refusal is not an error. lsm_allowed() returns false for the second exclusive module it meets and the only trace is an init_debug("exclusive disabled: %s\n", ...) line, which is compiled to nothing unless you booted with lsm.debug. Booting lsm=selinux,apparmor therefore gives you SELinux and a completely silent AppArmor. Boot with lsm.debug and read dmesg when an LSM you asked for is not there. The active list and order are then visible at /sys/kernel/security/lsm as a comma-separated list that “reflects the order in which checks are made” (kernel.org LSM admin guide).
The kernel's own admin guide is stale on this point
Documentation/admin-guide/LSM/index.rst— rendered at docs.kernel.org and still the top hit for “Linux Security Module usage” — states that LSMs are “selectable at build-time viaCONFIG_DEFAULT_SECURITYand can be overridden at boot-time via thesecurity=...kernel command line argument,” and that the active list is “the capability module… followed by any ‘minor’ modules… and then the one ‘major’ module.” Read against v6.12’ssecurity/Kconfigandsecurity/security.c, this is out of date in two ways: the selection variable isCONFIG_LSM(an ordered list) withCONFIG_DEFAULT_SECURITY_*only choosing which default string applies, and the modern override islsm=, which supersedessecurity=. The “at most one major” framing also predates the stacking work. Prefer the source andsecurity/Kconfigover this page; it is cited here for the parts that remain accurate (the capability module always being present, and the list reflecting check order).
Per-Object Security Blobs — and the Redesign That Enabled Stacking
A MAC module needs to attach its own state to kernel objects: SELinux needs a security identifier (SID) on every inode, task, file, and socket; AppArmor needs a label pointer on every task and every open file. The framework provides this through security blobs — opaque storage hung off a void *security pointer that the relevant kernel structures carry. At v6.12 the objects with such a pointer include struct cred (process credentials), struct inode, struct file, struct task_struct, struct super_block, struct sock, System V IPC objects, keys, struct msg_msg, struct perf_event, tun devices, InfiniBand objects, and block devices.
The historically important point is how that pointer is managed, because it is the entire reason multiple label-based LSMs could not coexist for years. Originally each object had one void *security pointer and whichever LSM owned the object owned the pointer outright — so only one blob-consuming LSM could be active at a time (the “exclusive” model). The 5.1 kernel introduced infrastructure-managed blobs (LWN: “Stacking” the security modules): instead of each LSM kmalloc()-ing its own pointer, “an LSM will tell the kernel how much space it needs to store its information and the kernel will take care of allocating, managing, and freeing the blob.” Each enabled LSM declares its per-object byte requirements in a struct lsm_blob_sizes, whose v6.12 fields are exactly:
struct lsm_blob_sizes {
int lbs_cred;
int lbs_file;
int lbs_ib; /* InfiniBand */
int lbs_inode;
int lbs_sock;
int lbs_superblock;
int lbs_ipc;
int lbs_key;
int lbs_msg_msg;
int lbs_perf_event;
int lbs_task;
int lbs_xattr_count; /* number of xattr slots in new_xattrs array */
int lbs_tun_dev;
int lbs_bdev;
};The accumulation trick is worth reading closely, because the same field is used first as a request and then as an offset:
static void __init lsm_set_blob_size(int *need, int *lbs)
{
int offset;
if (*need <= 0)
return;
offset = ALIGN(*lbs, sizeof(void *));
*lbs = offset + *need;
*need = offset; /* <-- the module's request is OVERWRITTEN with its offset */
}*need comes in as “this module wants N bytes” and goes out as “this module’s region starts at byte offset.” The module then reads its own slice with pointer arithmetic — AppArmor’s file_ctx() helper is literally return file->f_security + apparmor_blob_sizes.lbs_file;. There is one special case in lsm_set_blob_sizes(): the inode blob reserves sizeof(struct rcu_head) at offset 0 before any module’s region, because inode blobs are freed under RCU and the framework needs somewhere to put the callback head.
packet-beta 0-7: "rcu_head (inode blob only)" 8-11: "SELinux SID" 12-15: "(align to sizeof(void*))" 16-23: "Smack label ptr" 24-31: "AppArmor label ptr" 32-39: "Landlock ruleset ptr"
A composite inode->i_security blob on a kernel with four blob-consuming LSMs enabled. What it shows: one allocation from lsm_inode_cache, carved into per-module regions at boot-computed offsets, each module reading only its own slice; the leading rcu_head is framework overhead present on inode blobs only. The insight: the byte offsets are a function of which modules were enabled at boot, not of any module’s own code — which is why an out-of-tree LSM that miscomputes lsm_blob_sizes silently corrupts its neighbours’ state, and why the offsets are meaningless to crash/gdb without knowing the boot-time module set. (Offsets shown are illustrative of the layout rule, not measured on a specific build.)
During prepare_lsm() the framework sums each field across all enabled LSMs, so the single inode->i_security blob becomes a shared region. The allocation helpers — lsm_cred_alloc(), lsm_inode_alloc() (from lsm_inode_cache), lsm_file_alloc() (from lsm_file_cache), and the early-boot lsm_early_cred() which panic()s on failure because there is no graceful recovery before init — all carve from these pre-sized regions. This blob-sharing is the technical enabler that let AppArmor become composable in 5.5 and Smack/SELinux in 5.8 (LWN 804906); the LSM_FLAG_EXCLUSIVE flag is the residual fence guarding combinations that still are not safe to stack (LSM Stacking and Module Ordering).
Stacking at 6.12 — What Actually Co-Exists
“Stacking” is used loosely in LSM discussion to mean two different things, and conflating them is the most common error in write-ups of this subsystem.
Minor-LSM stacking is done and has been for years. Any number of non-exclusive modules run together, in CONFIG_LSM order, on the same kernel. A stock Ubuntu 24.04 kernel runs capability + Landlock + Yama + AppArmor + BPF-LSM simultaneously; a stock Fedora kernel runs capability + Landlock + Yama + BPF-LSM + SELinux + IMA/EVM. This is not a special mode — it is what ordered_lsm_init() has always produced since the blob rework.
Major-LSM stacking — two exclusive, label-based MACs at once — is still not possible at 6.12. lsm_allowed() refuses the second LSM_FLAG_EXCLUSIVE module it meets, full stop. The blocker is no longer blob storage; it is that a “security context” string (what /proc/self/attr/current returns, what an audit record carries, what a networking peer label means) has no agreed multi-LSM representation. The lsm_ctx structure and LSM_ID_* numbers added in 6.8 are the groundwork for solving exactly that — a context becomes {id, flags, len, ctx_len, ctx[]} rather than a bare string — but the full “stack the majors” work is not in 6.12.
| Module | LSM_ID_* | Flags | Model | Stacks with a major? |
|---|---|---|---|---|
capability | 100 | LSM_ORDER_FIRST, early | POSIX.1e capabilities; the only permissive hooks | Always on, always first |
| SELinux | 101 | LEGACY_MAJOR, EXCLUSIVE | Label-based type enforcement, system-wide | No — exclusive |
| Smack | 102 | LEGACY_MAJOR, EXCLUSIVE | Label-based, simpler; embedded/IoT | No — exclusive |
| TOMOYO | 103 | LEGACY_MAJOR, EXCLUSIVE | Pathname-based, learning-oriented | No — exclusive |
| AppArmor | 104 | LEGACY_MAJOR, EXCLUSIVE | Path-based per-program profiles | No — exclusive |
| Yama | 105 | none | Focused: ptrace scope only | Yes |
| LoadPin | 106 | none | Focused: pin kernel file loads to one filesystem | Yes |
| SafeSetID | 107 | none | Focused: constrain setuid/setgid transitions | Yes |
| lockdown | 108 | none | Focused: security_locked_down() reasons | Yes |
| BPF-LSM | 109 | none | Programmable: eBPF programs on hooks, loadable at runtime | Yes |
| Landlock | 110 | none | Unprivileged self-sandboxing, ABI-versioned | Yes |
| IMA | 111 | LSM_ORDER_LAST | Measure/appraise file contents | Yes |
| EVM | 112 | LSM_ORDER_LAST | Protect file metadata integrity | Yes |
| IPE | 113 | none | Integrity Policy Enforcement — new in 6.12 | Yes |
The in-tree module roster at v6.12, with identities from include/uapi/linux/lsm.h and flag classes from each module’s DEFINE_LSM. What it shows: the exclusivity fence runs between exactly four modules; everything else composes. The insight: the two modules most often missing from older LSM write-ups are the two that changed the model most. Landlock is the first LSM an unprivileged process can use to confine itself — no root, no admin-authored policy, and it stacks with whatever MAC the distro already runs. BPF-LSM is the only LSM whose policy is loaded, replaced, and unloaded at runtime. Neither fits the mental model of “pick one MAC at boot.”
Uncertain
Verify: the exact
LSM_FLAG_*value carried by everyDEFINE_LSMin the table above. Reason: theLSM_ID_*numbers and the fourLSM_FLAG_EXCLUSIVEmodules were read directly from v6.12include/uapi/linux/lsm.hand confirmed againstlsm_allowed()’s exclusivity logic, but the individualDEFINE_LSMinitialisers for the minor modules (LoadPin, SafeSetID, IPE, lockdown) were not each opened during this pass — they are inferred from the absence of any exclusivity conflict in practice. To resolve:grep -rn -A8 'DEFINE_LSM' security/on a v6.12 tree and read each.flagsfield.#uncertain
The Userspace API — /sys/kernel/security/lsm, /proc/*/attr, and the 6.8 Syscalls
Three interfaces expose the framework to userspace, and they arrived in that order.
/sys/kernel/security/lsm is the oldest and simplest: a read-only securityfs file holding the comma-separated lsm_names string, built by lsm_append() as each module registers. It “will always include the capability module” and “reflects the order in which checks are made” (admin guide). It is a string, so parsing it is fragile and it carries no per-module version or capability information.
/proc/<pid>/attr/* is the per-process context interface, and it is the one that does not compose. The files current, exec, fscreate, keycreate, prev, and sockcreate each hold one string, so when two modules both want to report a context there is nowhere to put the second. The partial fix was per-module subdirectories — /proc/self/attr/smack/current, /proc/self/attr/apparmor/current — with the files directly under attr/ kept “as legacy interfaces for modules that provide subdirectories” (admin guide).
The three LSM system calls, merged for 6.8 and implemented in security/lsm_syscalls.c, are the composable replacement. Per the kernel’s userspace-api/lsm documentation (authored by Casey Schaufler, dated July 2023):
long sys_lsm_list_modules(u64 __user *ids, u32 __user *size, u32 flags);
long sys_lsm_get_self_attr(unsigned int attr, struct lsm_ctx __user *ctx,
u32 __user *size, u32 flags);
long sys_lsm_set_self_attr(unsigned int attr, struct lsm_ctx __user *ctx,
u32 size, u32 flags);lsm_list_modules() writes the active LSM_ID_* numbers into ids and returns lsm_active_cnt — the same information as /sys/kernel/security/lsm but as numbers, not a string to parse. lsm_get_self_attr() returns an array of struct lsm_ctx, one per module that has something to say about the requested attribute; LSM_FLAG_SINGLE in flags narrows it to the one module named in the passed ctx. All three follow the same size protocol: pass the buffer size in *size, and on overflow the call returns -E2BIG with *size overwritten by the minimum required — so the correct usage is always call, check for -E2BIG, reallocate, call again.
sequenceDiagram autonumber participant App as Userspace participant K as Kernel (security/lsm_syscalls.c) participant SEL as SELinux participant AA as AppArmor App->>K: lsm_list_modules(ids, &size=0, 0) K-->>App: -E2BIG, *size = lsm_active_cnt * 8 Note over App: allocate ids[lsm_active_cnt] App->>K: lsm_list_modules(ids, &size, 0) K-->>App: n; ids = [100, 110, 105, 101]<br/>(capability, landlock, yama, selinux) App->>K: lsm_get_self_attr(LSM_ATTR_CURRENT, ctx, &size, 0) K->>SEL: security_getselfattr → its context string K->>AA: (not active on this kernel) K-->>App: array of struct lsm_ctx<br/>{id=101, ctx="unconfined_u:unconfined_r:..."}
The 6.8 LSM syscall protocol. What it shows: the two-call size negotiation, and that lsm_get_self_attr returns a tagged array rather than a single string — each element carries the LSM_ID_* of the module that produced it. The insight: this is the ABI shape that a multi-major-LSM world requires. /proc/self/attr/current can hold one answer; struct lsm_ctx[] can hold one per module. The syscalls landing in 6.8 is the clearest signal that “stack the majors” is still an active goal rather than an abandoned one.
The LSM_ATTR_* values and which modules honour them come straight from the documentation: LSM_ATTR_CURRENT is supported by SELinux, Smack, and AppArmor; LSM_ATTR_EXEC and LSM_ATTR_PREV by SELinux and AppArmor; LSM_ATTR_FSCREATE, LSM_ATTR_KEYCREATE, and LSM_ATTR_SOCKCREATE by SELinux alone.
The Framework Enforces Nothing — The Capability Baseline
It bears repeating, because it is the single most misunderstood fact about LSM: the framework makes no access-control decision of its own. With no modules registered, every hook’s static-call slot is inactive and security_*() returns the hook’s LSM_HOOK-declared default (zero — allow, or LSM_RET_VOID for void hooks). The one apparent exception is the capability LSM, implemented in security/commoncap.c, which is compiled in whenever CONFIG_SECURITY is set and is always first. It is not special-cased in the dispatch path; it is an ordinary module registered with DEFINE_EARLY_LSM and LSM_ORDER_FIRST that happens to implement the POSIX.1e capability checks (POSIX Capabilities). Putting capabilities inside an LSM module rather than hard-coding them was Torvalds’ own third requirement, and it keeps the framework uniform — even the baseline privilege model rides the same rails as SELinux. include/linux/lsm_count.h encodes this directly: CAPABILITIES_ENABLED is defined whenever CONFIG_SECURITY is, so MAX_LSM_COUNT is never zero on a kernel with LSM at all.
Failure Modes and Common Misunderstandings
“LSM modules are loadable like kernel modules.” No — and this is the framework’s worst-named concept. Despite the word “module,” LSMs are not runtime-loadable .ko files. The kernel’s own admin guide concedes the point: “The name ‘module’ is a bit of a misnomer since these extensions are not actually loadable kernel modules.” They are compiled in and selected at build time (CONFIG_LSM) and boot time (lsm=); the static-call slots are __ro_after_init and the active set is frozen once ordered_lsm_init() returns. The lone exception is BPF-LSM, where the framework-level callback is registered at boot but individual policies are attached at runtime as eBPF programs (BPF-LSM). Historically this was not always so — LSM originally supported genuinely loadable modules, and that support was removed in 2.6.24 precisely because unloading a security module is unsound (an in-flight hook can be executing when the module vanishes).
“Adding SELinux makes the kernel more permissive somehow.” Impossible for a MAC module by construction. Because the 267 non-capable hooks sit after DAC and can only return a denial, enabling a MAC LSM can only ever remove permissions. The genuine nuance — that security_capable() is a permissive hook — does not undermine this, because only the capability module implements it permissively; SELinux and AppArmor implement capable restrictively.
Exclusivity refusals at boot are silent. Asking for two exclusive modules — lsm=selinux,apparmor — does not error; prepare_lsm() simply refuses to enable the second exclusive module it encounters and logs it via init_debug(), which is a no-op unless you booted with lsm.debug. A “my AppArmor profiles aren’t loading” report often means SELinux won the slot earlier in the order. Diagnose with cat /sys/kernel/security/lsm (who actually won) and dmesg | grep '^LSM:' (the report_lsm_order() line, which prints unconditionally).
Blob-size mismatches are silent memory corruption. Because blob offsets are computed at boot from the set of enabled modules, an out-of-tree LSM that miscalculates its lsm_blob_sizes overwrites neighbouring modules’ state in the shared blob — a class of bug invisible until a specific module combination is enabled, and one that presents as inexplicable misbehaviour in a different module. This is one reason the community is conservative about merging new LSMs, and one reason MAX_LSM_COUNT is a compile-time constant with a panic() behind it rather than a growable list.
“The first non-zero return is the only denial.” The static-call walk short-circuits, so if both AppArmor and SELinux would deny an operation, only the earlier one in CONFIG_LSM order emits an audit record. Chasing a denial that “doesn’t go away when I fix the policy” often means a second module is denying behind the first.
Reading /sys/kernel/security/lsm requires securityfs to be mounted. It usually is (systemd mounts it at /sys/kernel/security), but in a minimal container or initramfs the file simply does not exist, which reads as “no LSMs active” when in fact the kernel is fully confined. Prefer lsm_list_modules() on 6.8+, or check mount | grep securityfs first.
Alternatives and When to Choose Them
The framework is not the only way to enforce policy in Linux, and the alternatives illuminate why LSM is shaped as it is.
| Mechanism | Where it hooks | Sees | Privileged to install? | Can express |
|---|---|---|---|---|
| DAC (Mandatory vs Discretionary Access Control) | inode_permission() | uid/gid vs mode bits | The file’s owner | Owner-discretionary policy only |
| POSIX Capabilities | capable() → security_capable() | The CAP_* bit requested | root / file caps | Splitting root, not confining a subject |
| seccomp-BPF | Syscall entry, before anything resolves | Syscall number + raw register args | No — unprivileged with no_new_privs | “which syscalls,” never “which file” |
| LSM (SELinux/AppArmor/Smack) | 268 points, after DAC, on resolved objects | The actual struct inode/file/task | Admin authors system policy | Full object-level MAC |
| Landlock | LSM hooks, but scoped to a task’s ruleset | Resolved objects, filesystem + some net | No — unprivileged self-sandboxing | Per-process voluntary confinement |
| BPF-LSM | LSM hooks, program per hook | Resolved objects, plus BPF maps/helpers | CAP_BPF+CAP_MAC_ADMIN | Runtime-loadable, dynamically updated policy |
| netfilter / XDP | Packet path | Packet headers | CAP_NET_ADMIN | Network policy only, no process identity |
Where each Linux access-control mechanism attaches. What it shows: seccomp is early and blind to objects; LSM is late and object-aware; Landlock and BPF-LSM are LSM hooks with different installation models rather than different mediation points. The insight: the reason MAC lives in LSM rather than in seccomp is the row marked “Sees” — seccomp cannot reason about which inode an openat() will land on because the name has not been resolved yet, which is exactly the TOCTTOU problem the 2002 paper cited when rejecting syscall-table interposition.
Production Notes
Every mainstream distribution ships an LSM enabled by default: Red Hat–family and Fedora default to SELinux in enforcing mode; Ubuntu, Debian (since Debian 10), and SUSE default to AppArmor. The LSM_FLAG_LEGACY_MAJOR plumbing exists so the historical security=selinux / security=apparmor boot lines keep working, while CONFIG_LSM / lsm= is the modern, stacking-aware way to express the full ordered set — and lsm= silently wins if both are given.
Container runtimes lean on LSM heavily: a hardened container is typically capability-dropped and seccomp-filtered and confined by an SELinux type or an AppArmor profile, three different parts of this same stack (Linux Containers and Isolation MOC). Kubernetes surfaces the last two through securityContext (SecurityContext).
The operational triage sequence for “permission denied with correct file modes” is worth memorising:
# 1. Which modules are even active, and in what order?
cat /sys/kernel/security/lsm # e.g. capability,landlock,yama,apparmor,bpf
dmesg | grep -m1 '^LSM: initializing' # the report_lsm_order() line, always printed
# 2. Did one of them deny? SELinux and AppArmor both log to the audit trail.
ausearch -m AVC,USER_AVC -ts recent # SELinux access-vector denials
journalctl -k | grep 'apparmor="DENIED"'
# 3. Is it a capability rather than a MAC denial?
grep Cap /proc/<pid>/status # CapEff vs what the operation needs
# 4. Nothing in the logs? Some hooks deny without auditing; try lsm.debug at boot
# to see the ordering decisions, and check the module is actually enabled.A subtlety that costs real debugging time: because the dispatch short-circuits on the first non-zero return and each module audits its own denials, a denial logged by AppArmor does not mean SELinux would have allowed it — SELinux was simply never asked. On a stacked kernel, fix the first denial and re-test rather than assuming you have found the only one.
See Also
- LSM Hooks and the security_ Call Sites — the other half:
LSM_HOOKmacro expansion,call_int_hook, and one wrapper traced end to end - LSM Stacking and Module Ordering — the ordering tiers,
CONFIG_LSMvariants, andlsm=/security=interaction in depth - Major vs Minor LSMs — the exclusive label-based MACs vs the stackable minor modules
- Mandatory vs Discretionary Access Control — the “can only restrict” property the framework enforces
- SELinux, AppArmor, Smack, TOMOYO — the four exclusive MAC modules
- Landlock — the unprivileged, stackable, ABI-versioned newcomer
- BPF-LSM — the one LSM whose policy is loaded at runtime as eBPF
- Kernel Lockdown Mode — the
security_locked_down()consumer and its 30 reason codes - Integrity Measurement Architecture / Extended Verification Module — the
LSM_ORDER_LASTintegrity pair - The Yama LSM and ptrace Scope — the canonical example of a focused minor LSM
- POSIX Capabilities — implemented as the always-on
capabilityLSM, and the only permissive hooks - Process Credentials and struct cred — carrier of the per-task security blob
- Seccomp and seccomp-BPF — the syscall-entry alternative, and why it cannot do object-level MAC
- Linux Security MOC — parent map (section C, the LSM framework)