LSM Stacking and Module Ordering
For most of Linux’s history a kernel could run exactly one Linux Security Module at a time: you picked SELinux or AppArmor or Smack or TOMOYO at boot, and the others were inert. Modern kernels have replaced that “one major module” model with LSM stacking — multiple modules register against the same hook points and the framework calls each of them in sequence, so a single boot can run, say, capabilities and Yama and Landlock and lockdown and SELinux and BPF-LSM all at once. The set of active modules and the order they are called in is fixed at build time by the
CONFIG_LSMstring and can be overridden at boot by thelsm=kernel parameter (with the oldersecurity=parameter still honoured for the one “major” module). One important limitation persists as of the 6.12 LTS line: while every minor module stacks freely, only one exclusive, label-based major module (SELinux, AppArmor, or Smack) may be active at a time — you still cannot run SELinux and AppArmor together.
This note is about which modules run and in what order — the registration, ordering, and initialization machinery. The companion split is Major vs Minor LSMs, which explains why the exclusive-major restriction exists (the shared per-object security blob). Read that note for the blob-sharing story; this one traces the boot-time code path that walks the list. Both ride on The Linux Security Module Framework, which describes the hook mechanism itself.
All code and version claims below are pinned to the Linux 6.12 LTS source tree (released 2024-11-17), verified against the raw kernel blobs listed in sources.
Mental Model
Think of the LSM layer not as a single guard but as a pipeline of guards posted at the same gate. When the kernel reaches a security-relevant decision — opening an inode, creating a socket, sending a signal — it calls a security_*() wrapper, and that wrapper invokes every registered module’s callback for that hook in turn. The decision is AND-combined and fail-closed: if any module in the chain returns a denial (a negative errno), the operation is refused; the access proceeds only if all of them agree. The order of the pipeline matters because some modules are cheap pre-filters and the framework wants a deterministic, configurable sequence.
flowchart LR CFG["CONFIG_LSM build string<br/>(default order)"] -->|"lsm= overrides"| ORD["ordered_lsm_parse()"] BOOT["lsm= / security=<br/>boot parameters"] -->|"override"| ORD ORD --> P1["Phase 1:<br/>LSM_ORDER_FIRST<br/>(capability)"] ORD --> P2["Phase 2:<br/>LSM_ORDER_MUTABLE<br/>(parse the order string)"] ORD --> P3["Phase 3:<br/>LSM_ORDER_LAST<br/>(integrity / ima)"] P1 --> LIST["ordered_lsms[]"] P2 --> LIST P3 --> LIST LIST --> PREP["prepare_lsm():<br/>pick exclusive major,<br/>sum blob sizes"] PREP --> INIT["initialize_lsm():<br/>call each lsm->init()"] INIT --> RUN["Hooks now chained<br/>in this order at runtime"]
The build-and-boot pipeline that produces the active LSM list. What it shows: the order is decided in three deterministic phases — capabilities forced first, the user-configurable middle, integrity forced last — and only after the full list is assembled does the framework pick the single exclusive major and allocate the shared blobs. The insight: the lsm= string controls only the mutable middle phase; you cannot reorder capabilities ahead of nothing or push integrity out of last place, because those modules declare a fixed enum lsm_order.
The Three Ordering Tiers
Every module declares where it sits via the order field of its struct lsm_info, drawn from enum lsm_order in include/linux/lsm_hooks.h:
enum lsm_order {
LSM_ORDER_FIRST = -1, /* This is only for capabilities. */
LSM_ORDER_MUTABLE = 0,
LSM_ORDER_LAST = 1, /* This is only for integrity. */
};The comments are the binding contract. LSM_ORDER_FIRST is reserved for the capability module — the classic POSIX-capabilities checks (cap_capable, cap_ptrace_access_check, …) that every other module assumes have already run. In security/commoncap.c the registration is literally:
DEFINE_LSM(capability) = {
.name = "capability",
.order = LSM_ORDER_FIRST,
.init = capability_init,
};Because it is LSM_ORDER_FIRST, capability is always enabled and always called first, regardless of what CONFIG_LSM or lsm= say — it is implicit and cannot be left off the list. The framework’s documentation phrases this as “The Linux capabilities modules will always be included” (Documentation/admin-guide/LSM/index.rst, v6.12). This is why the capability checks form the base of the access-control pipeline diagrammed in Linux Security MOC.
At the other end, LSM_ORDER_LAST is reserved for the integrity subsystem — concretely the IMA module, whose registration in security/integrity/ima/ima_main.c reads:
DEFINE_LSM(ima) = {
.name = "ima",
.init = init_ima_lsm,
.order = LSM_ORDER_LAST,
.blobs = &ima_blob_sizes,
};IMA wants to run last so that measurement and appraisal see the file after the other modules have had their say. Everything in between — Yama, Landlock, lockdown, SafeSetID, LoadPin, BPF-LSM, and the one chosen major (SELinux/AppArmor/Smack/TOMOYO) — is LSM_ORDER_MUTABLE (the default value 0, which is what a DEFINE_LSM block that omits .order gets). The mutable modules are the ones the CONFIG_LSM/lsm= order string actually controls.
The Build-Time Order String: CONFIG_LSM
The default sequence is baked into the kernel by the CONFIG_LSM Kconfig string. In v6.12 (security/Kconfig) the fallback default — when no specific DEFAULT_SECURITY_* is chosen — is:
config LSM
string "Ordered list of enabled LSMs"
default "landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,ipe,bpf" if DEFAULT_SECURITY_SMACK
default "landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,ipe,bpf" if DEFAULT_SECURITY_APPARMOR
default "landlock,lockdown,yama,loadpin,safesetid,tomoyo,ipe,bpf" if DEFAULT_SECURITY_TOMOYO
default "landlock,lockdown,yama,loadpin,safesetid,ipe,bpf" if DEFAULT_SECURITY_DAC
default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,ipe,bpf"
help
A comma-separated list of LSMs, in initialization order.
Any LSMs left off this list, except for those with order
LSM_ORDER_FIRST and LSM_ORDER_LAST, which are always enabled
if selected in the kernel configuration, will be ignored.
This can be controlled at boot with the "lsm=" parameter.
Several things are worth reading carefully here. First, the string lists all four major modules (smack,selinux,tomoyo,apparmor) even though at most one can actually become active — listing a module only permits it; the exclusive-major arbitration happens later (see Major vs Minor LSMs). The DEFAULT_SECURITY_* variants merely move the distro’s preferred major earlier in the string so that, if more than one is compiled in and eligible, the preferred one wins the exclusive slot. Second, the help text states the crucial rule: a module left off the list is ignored — except LSM_ORDER_FIRST (capability) and LSM_ORDER_LAST (integrity/IMA), which run regardless if they were compiled in. Third, note ipe (Integrity Policy Enforcement) appears in the default — IPE was merged in 6.12, so its presence dates this string to that release. The single source-of-truth in the code is one line in security/security.c:
static __initconst const char *const builtin_lsm_order = CONFIG_LSM;i.e. builtin_lsm_order is just the CONFIG_LSM string captured at compile time. The reader can see the runtime result of all of this at /sys/kernel/security/lsm, a read-only file that prints the active modules as a comma-separated list in call order.
Boot-Time Overrides: lsm= and security=
Two boot parameters can override the build default. The modern one is lsm=, parsed in security/security.c:
static int __init choose_lsm_order(char *str)
{
chosen_lsm_order = str;
return 1;
}
__setup("lsm=", choose_lsm_order);lsm=landlock,lockdown,yama,apparmor,bpf on the kernel command line replaces the entire mutable ordering — anything not named (and not FIRST/LAST) is disabled. This is the supported way to, for example, boot a SELinux-default kernel with AppArmor instead, or to strip the list down for debugging.
The older parameter is security=, which selects only the single major module by name:
static int __init choose_major_lsm(char *str)
{
chosen_major_lsm = str;
return 1;
}
__setup("security=", choose_major_lsm);security=selinux means “of the legacy-major modules, enable only SELinux.” It predates stacking and reasons only about majors. When both are given, lsm= wins and security= is discarded with a warning — visible at the top of ordered_lsm_init():
if (chosen_lsm_order) {
if (chosen_major_lsm) {
pr_warn("security=%s is ignored because it is superseded by lsm=%s\n",
chosen_major_lsm, chosen_lsm_order);
chosen_major_lsm = NULL;
}
ordered_lsm_parse(chosen_lsm_order, "cmdline");
} else
ordered_lsm_parse(builtin_lsm_order, "builtin");So the precedence is: lsm= (if present) → otherwise builtin_lsm_order (the CONFIG_LSM string), with security= consulted separately to prune the legacy majors. There is also an lsm.debug boot flag that turns on the init_debug() traces you see referenced throughout the init code (e.g. init_debug("exclusive chosen: %s\n", ...)), useful for diagnosing why a module did or did not load.
The Initialization Walk: ordered_lsm_init()
The heart of the machinery is ordered_lsm_init(). After choosing the order string (above), it runs three loops:
for (lsm = ordered_lsms; *lsm; lsm++)
prepare_lsm(*lsm);
/* ... allocate lsm_file_cache / lsm_inode_cache from accumulated blob sizes ... */
lsm_early_cred((struct cred *) current->cred);
lsm_early_task(current);
for (lsm = ordered_lsms; *lsm; lsm++)
initialize_lsm(*lsm);The ordered_lsms[] array was filled by ordered_lsm_parse(), which implements the three-phase walk: it first appends every LSM_ORDER_FIRST module (capability), then walks the comma-separated order string token by token appending each matching LSM_ORDER_MUTABLE module, then appends every LSM_ORDER_LAST module (integrity), and finally disables any compiled-in LSM that never made it into the list. When security= was supplied, ordered_lsm_parse() additionally walks the raw __start_lsm_info..__end_lsm_info table and disables every LSM_FLAG_LEGACY_MAJOR module whose name does not match the chosen major — that is how security=selinux silences AppArmor even if AppArmor sits in the order string.
The first loop, prepare_lsm(), does two jobs per module — decide enablement and reserve blob space:
static void __init prepare_lsm(struct lsm_info *lsm)
{
int enabled = lsm_allowed(lsm);
set_enabled(lsm, enabled);
if (enabled) {
if ((lsm->flags & LSM_FLAG_EXCLUSIVE) && !exclusive) {
exclusive = lsm;
init_debug("exclusive chosen: %s\n", lsm->name);
}
lsm_set_blob_sizes(lsm->blobs);
}
}This is where the exclusive major is chosen: the first enabled module carrying LSM_FLAG_EXCLUSIVE claims the exclusive slot, and lsm_allowed() then rejects any later exclusive module:
static bool __init lsm_allowed(struct lsm_info *lsm)
{
if (!is_enabled(lsm))
return false;
/* Not allowed if another exclusive LSM already initialized. */
if ((lsm->flags & LSM_FLAG_EXCLUSIVE) && exclusive) {
init_debug("exclusive disabled: %s\n", lsm->name);
return false;
}
return true;
}This is precisely why ordering the majors in CONFIG_LSM matters: whichever exclusive major appears first in the order string wins, and the rest are silently disabled. (TOMOYO is a subtlety — it is LSM_FLAG_LEGACY_MAJOR but not LSM_FLAG_EXCLUSIVE, so it does not actually participate in this exclusive arbitration; see Major vs Minor LSMs.) The lsm_set_blob_sizes() call accumulates each enabled module’s per-object storage requirements into a single blob_sizes struct — the shared-blob mechanism that made minor-LSM stacking possible in the first place, detailed in Major vs Minor LSMs.
Between the two loops the framework allocates slab caches sized to the summed blob requirements (lsm_file_cache, lsm_inode_cache) and seeds the boot task’s credential and task blobs via lsm_early_cred()/lsm_early_task(). Only then does the second loop, initialize_lsm(), actually invoke each module’s registered init():
static void __init initialize_lsm(struct lsm_info *lsm)
{
if (is_enabled(lsm)) {
int ret;
init_debug("initializing %s\n", lsm->name);
ret = lsm->init();
WARN(ret, "%s failed to initialize: %d\n", lsm->name, ret);
}
}There is also an even-earlier path, early_security_init(), which runs DEFINE_EARLY_LSM modules (such as lockdown when configured as integrity-keys-enabled) before the main ordered_lsm_init() — these are placed in a separate .early_lsm_info.init ELF section and must come up before normal memory allocation is fully available. Modules are discovered at all because DEFINE_LSM/DEFINE_EARLY_LSM emit their struct lsm_info into dedicated linker sections (.lsm_info.init / .early_lsm_info.init) that the init code iterates between the __start_*/__end_* boundary symbols — there is no dynamic registration list, just a compile-time array the loader walks.
The Persisting Limitation and the “Stack the Majors” Work
The headline modern fact — multiple minor LSMs plus the exclusive major all stack — leaves one gap: you still cannot run two exclusive label-based majors together. SELinux, AppArmor, and Smack each declare LSM_FLAG_EXCLUSIVE (verified in their DEFINE_LSM blocks in v6.12), so the lsm_allowed() logic above guarantees only one of them runs. The reason is historical and architectural — they each want to own a per-object label, and reconciling two independent labeling schemes on the same object is the hard part. The detailed why is in Major vs Minor LSMs.
The ongoing kernel work to lift this restriction has two visible artifacts in v6.12. First, every module now carries a stable struct lsm_id (name + numeric id) — e.g. { .name = "selinux", .id = LSM_ID_SELINUX } — so userspace can name modules unambiguously even when several are active. Second, three syscalls landed (in Linux 6.8, present in v6.12 at numbers 459–461 in arch/x86/entry/syscalls/syscall_64.tbl) to query and address per-module state:
SYSCALL_DEFINE3(lsm_list_modules, u64 __user *, ids, u32 __user *, size, u32, flags)
SYSCALL_DEFINE4(lsm_get_self_attr, unsigned int, attr, struct lsm_ctx __user *, ctx, u32 __user *, size, u32, flags)
SYSCALL_DEFINE4(lsm_set_self_attr, unsigned int, attr, struct lsm_ctx __user *, ctx, u32, size, u32, flags)lsm_list_modules() returns the active module ids; lsm_get_self_attr()/lsm_set_self_attr() read and write the calling task’s security context for a named attribute, returning per-LSM struct lsm_ctx records (each tagged with its id). The LSM_FLAG_SINGLE flag on the get path lets a caller ask for just one module’s view. These interfaces exist precisely because the old single-blob /proc/self/attr/current cannot represent multiple simultaneous majors’ contexts — they are the userspace ABI that would be needed once two majors can coexist.
Uncertain
Verify: whether full “stack the majors” (running e.g. SELinux and AppArmor simultaneously) is enabled in mainline as of 6.12, versus only the enabling infrastructure (
lsm_id, the syscalls, blob-sharing) being present while the majors still declareLSM_FLAG_EXCLUSIVE. Reason: the v6.12 source confirms SELinux/AppArmor/Smack all setLSM_FLAG_EXCLUSIVE, so they are still mutually exclusive at this release — but the precise upstream status of the remaining stacking patches (and any out-of-tree/Android forks that ship it) was not pinned to a primary changelog during this pass. To resolve: check theDocumentation/admin-guide/LSM/notes and the security tree’s stacking series cover letters for the kernel that first dropsLSM_FLAG_EXCLUSIVEfrom a major. uncertain
Failure Modes and Diagnosis
The most common operational surprise is a major silently not loading. If CONFIG_LSM lists selinux before apparmor and both are compiled in, AppArmor loses the exclusive slot and never initializes — /sys/kernel/security/lsm shows selinux but not apparmor, and the only hint is the exclusive disabled: apparmor line emitted when lsm.debug is on. The fix is to reorder the string (lsm=...,apparmor,... before selinux) or use security=apparmor.
A second pitfall is assuming security=none disables all MAC. It only prunes the legacy majors; minor modules named in CONFIG_LSM (Yama, Landlock, lockdown) still run. To genuinely strip the list you must use lsm= with the desired (possibly empty of majors) set — and even then capability and integrity cannot be removed via the string.
A third: expecting lsm= to add a module that was not compiled in. The order string can only enable/order modules that exist in the kernel’s .lsm_info.init section; naming a module that was never built is a no-op (it simply never matches in ordered_lsm_parse()). Confirm with /sys/kernel/security/lsm after boot rather than assuming the command line took effect.
See Also
- Major vs Minor LSMs — the companion: why minor LSMs stack and exclusive majors do not, via the shared
lsm_blob_sizesblob infrastructure - The Linux Security Module Framework — the hook mechanism these ordered modules register against
- SELinux — an exclusive
LSM_FLAG_LEGACY_MAJOR | LSM_FLAG_EXCLUSIVEmajor - AppArmor — the other dominant exclusive major; cannot run alongside SELinux as of 6.12
- The Yama LSM and ptrace Scope — a canonical stackable minor LSM (no order, no flags)
- Landlock — a stackable minor LSM providing unprivileged sandboxing
- BPF-LSM — a stackable minor LSM whose hooks run eBPF programs
- Linux Security MOC — the parent map (§C, “The LSM Framework”)