Skip to main content
The Linux Security Module (LSM) framework provides a set of hook points throughout the kernel that allow security subsystems to enforce mandatory access control (MAC) policies without modifying core kernel code. Multiple LSMs can be active simultaneously through LSM stacking.

Framework

Architecture, security_hook_list, and registration

Key Hooks

inode_permission, file_open, task_kill, socket_connect

BPF LSM

bpf_lsm_* hooks for policy without kernel modules

LSM Framework

The LSM framework was introduced to allow multiple security models to coexist in the kernel without requiring separate kernel builds. It works via a static call table (static_calls_table) generated at build time from include/linux/lsm_hook_defs.h. Each hook site in the kernel calls through this table, and each active LSM registers a callback for the hooks it cares about. Boot-time LSM order is controlled by the lsm= kernel command-line parameter (e.g., lsm=lockdown,yama,selinux,bpf). The security/Kconfig option CONFIG_LSM sets the compile-time default order.
/* From security/security.c */
bool lsm_debug __ro_after_init;
unsigned int lsm_active_cnt __ro_after_init;
const struct lsm_id *lsm_idlist[MAX_LSM_COUNT];

Core Data Structures

/* include/linux/lsm_hooks.h */
struct lsm_id {
    const char *name; /* approved LSM name, e.g., "selinux", "apparmor" */
    u64         id;   /* LSM ID from uapi/linux/lsm.h */
};
Each LSM defines exactly one lsm_id. The name must be approved by the LSM maintainers. The id is a stable numeric identifier used by userspace (e.g., for lsm_get_self_attr() syscall).
/* include/linux/lsm_hooks.h */
struct security_hook_list {
    struct lsm_static_call   *scalls; /* static call slot(s) in the table */
    union security_list_options hook; /* the callback function pointer */
    const struct lsm_id      *lsmid; /* owning LSM's identity */
} __randomize_layout;
One security_hook_list entry is created per hook an LSM implements. The LSM_HOOK_INIT() macro fills in the scalls and hook fields:
static struct security_hook_list my_lsm_hooks[] __ro_after_init = {
    LSM_HOOK_INIT(inode_permission, my_inode_permission),
    LSM_HOOK_INIT(file_open,        my_file_open),
    LSM_HOOK_INIT(task_kill,        my_task_kill),
};
/* include/linux/lsm_hooks.h */
struct lsm_blob_sizes {
    unsigned int lbs_cred;         /* bytes in struct cred */
    unsigned int lbs_file;         /* bytes in struct file */
    unsigned int lbs_inode;        /* bytes in struct inode */
    unsigned int lbs_sock;         /* bytes in struct sock */
    unsigned int lbs_superblock;   /* bytes in struct super_block */
    unsigned int lbs_ipc;          /* bytes in IPC structs */
    unsigned int lbs_task;         /* bytes in struct task_struct */
    unsigned int lbs_xattr_count;  /* xattr slots in inode security init */
    unsigned int lbs_bdev;         /* bytes in struct block_device */
    unsigned int lbs_bpf_map;      /* bytes in struct bpf_map */
    unsigned int lbs_bpf_prog;     /* bytes in struct bpf_prog */
    /* ... additional fields ... */
};
LSMs declare the size of their private data blobs here. The LSM framework aggregates all LSM blob sizes and carves out non-overlapping regions within each kernel object’s security pointer. Use the lsm_cred_slot(), lsm_file_slot(), etc. accessors to retrieve your blob.
/* include/linux/lsm_hooks.h */
struct lsm_info {
    const struct lsm_id     *id;      /* LSM identity */
    enum lsm_order           order;   /* LSM_ORDER_FIRST/-MUTABLE/-LAST */
    unsigned long            flags;   /* LSM_FLAG_* */
    struct lsm_blob_sizes   *blobs;   /* blob size requests, or NULL */
    int                     *enabled; /* controlled by CONFIG_LSM */
    int (*init)(void);                /* initialization function */
    /* optional: initcall_early, initcall_core, etc. */
};
LSMs are registered by placing a struct lsm_info in the .lsm_info.init ELF section via DEFINE_LSM(name). The framework discovers and initializes all LSMs in the section at boot.

security_add_hooks()

void security_add_hooks(struct security_hook_list *hooks,
                        int count,
                        const struct lsm_id *lsmid);
Registers count hook entries from the hooks array with the LSM framework. This must be called from the LSM’s init() function. After this call, the kernel will invoke the registered callbacks at each corresponding hook site.
hooks
struct security_hook_list *
required
Array of hook entries initialized with LSM_HOOK_INIT(). Must be __ro_after_init to prevent post-init modification.
count
int
required
Number of entries in hooks. Typically ARRAY_SIZE(my_lsm_hooks).
lsmid
const struct lsm_id *
required
Pointer to the LSM’s lsm_id. The framework uses this to associate each hook with its owning LSM for auditing and stacking.

Key LSM Hooks

Hook signatures are defined in include/linux/lsm_hook_defs.h using the LSM_HOOK(RET, DEFAULT, NAME, ...) macro. Below are the most commonly implemented hooks:

inode_permission

int inode_permission(struct inode *inode, int mask);
Called by inode_permission() in fs/namei.c before granting access to an inode. Invoked for every open(), stat(), access(), and directory lookup that checks inode permissions.
inode
struct inode *
The inode being accessed. The LSM can inspect inode->i_security for its private label.
mask
int
Bitmask of requested access: MAY_READ, MAY_WRITE, MAY_EXEC, MAY_APPEND. May include MAY_NOT_BLOCK when called from a non-blocking context.
Return 0 to allow, negative errno (typically -EACCES or -EPERM) to deny.

file_open

int file_open(struct file *file);
Called during open() after the file has been opened but before it is returned to userspace. The file->f_path and file->f_flags are fully populated. This is the preferred hook for controlling file open access because it fires after path resolution and POSIX DAC checks.
file
struct file *
The newly opened file. Access file->f_inode for the inode, file->f_path for the path, and file->f_flags for open flags (O_RDONLY, O_WRONLY, etc.).
Return 0 to allow, negative errno to deny (the file is closed before returning to userspace).

Writing a Simple LSM Module

1

Define LSM identity and blob sizes

/* my_lsm.c */
#include <linux/lsm_hooks.h>
#include <linux/security.h>

static const struct lsm_id my_lsm_id = {
    .name = "my_lsm",
    .id   = LSM_ID_UNDEF, /* use a real ID from uapi/linux/lsm.h for upstreamed LSMs */
};

/* Request private data on each inode */
static struct lsm_blob_sizes my_blob_sizes __ro_after_init = {
    .lbs_inode = sizeof(struct my_inode_security),
};
2

Implement hook callbacks

static int my_inode_permission(struct inode *inode, int mask)
{
    struct my_inode_security *isec = inode->i_security +
                                     my_blob_sizes.lbs_inode;

    /* Example: deny write to inodes tagged as read-only by this LSM */
    if ((mask & MAY_WRITE) && isec->readonly)
        return -EACCES;

    return 0; /* allow */
}

static int my_file_open(struct file *file)
{
    struct inode *inode = file_inode(file);
    /* Audit the open event */
    pr_debug("my_lsm: file_open ino=%lu flags=0x%x\n",
             inode->i_ino, file->f_flags);
    return 0;
}
3

Declare the hook table

static struct security_hook_list my_lsm_hooks[] __ro_after_init = {
    LSM_HOOK_INIT(inode_permission, my_inode_permission),
    LSM_HOOK_INIT(file_open,        my_file_open),
};
4

Implement the init function

static int __init my_lsm_init(void)
{
    security_add_hooks(my_lsm_hooks, ARRAY_SIZE(my_lsm_hooks), &my_lsm_id);
    pr_info("my_lsm: initialized\n");
    return 0;
}
5

Register with DEFINE_LSM

DEFINE_LSM(my_lsm) = {
    .id    = &my_lsm_id,
    .blobs = &my_blob_sizes,
    .init  = my_lsm_init,
};
The DEFINE_LSM() macro places the lsm_info struct in the .lsm_info.init linker section. The LSM framework scans this section at boot and calls init() for each enabled LSM in the order specified by the lsm= command-line parameter.
6

Enable in Kconfig and build

config SECURITY_MY_LSM
    bool "My LSM"
    depends on SECURITY
    default n
    help
      Enable the My LSM security module.
# Enable and rebuild
echo CONFIG_SECURITY_MY_LSM=y >> .config
make olddefconfig
make -j$(nproc)

# Boot with your LSM enabled
# Add to kernel command line:
lsm=lockdown,capability,my_lsm

LSM Stacking

Multiple LSMs can be active simultaneously. The framework calls each LSM’s hook in the order specified by lsm= and uses the most restrictive result: if any hook returns a non-zero (denial) value, the operation is denied.
lsm=lockdown,yama,selinux,apparmor,bpf
         │       │       │          │    │
         │       │       │          │    └── BPF LSM (last)
         │       │       │          └─────── AppArmor
         │       │       └────────────────── SELinux
         │       └────────────────────────── Yama
         └────────────────────────────────── Lockdown (first)

LSM Ordering

enum lsm_order {
    LSM_ORDER_FIRST   = -1,  /* Capabilities only — runs before all others */
    LSM_ORDER_MUTABLE =  0,  /* Normal LSMs, ordered by lsm= parameter */
    LSM_ORDER_LAST    =  1,  /* Integrity (IMA/EVM) — runs after all others */
};
Set lsm_info.order to control where in the call chain your LSM is placed:
DEFINE_LSM(my_lsm) = {
    .id    = &my_lsm_id,
    .order = LSM_ORDER_MUTABLE,  /* standard position */
    .blobs = &my_blob_sizes,
    .init  = my_lsm_init,
};
capabilities is always LSM_ORDER_FIRST — it runs before any other LSM on every hook, ensuring POSIX capability checks cannot be bypassed. integrity is LSM_ORDER_LAST to finalize integrity measurements after all access control decisions.

LSM Blobs and Private Data

LSMs use blobs to store per-object security labels without modifying core kernel structures. The framework reserves space inside objects based on lsm_blob_sizes and gives each LSM a non-overlapping slice.
/* Getting the inode security blob: */
static inline struct my_inode_security *
my_inode_security(const struct inode *inode)
{
    return inode->i_security + my_blob_sizes.lbs_inode;
    /* lbs_inode holds the byte offset assigned at init time */
}

/* Getting the task security blob: */
static inline struct my_task_security *
my_task_security(const struct task_struct *task)
{
    return task->security + my_blob_sizes.lbs_task;
}

/* Getting the file security blob: */
static inline struct my_file_security *
my_file_security(const struct file *file)
{
    return file->f_security + my_blob_sizes.lbs_file;
}
The lbs_* fields in my_blob_sizes are populated by the framework at init time, not by the LSM author. The LSM author only sets the size (number of bytes needed). The framework resolves the actual offset during boot.

BPF LSM

BPF LSM (CONFIG_BPF_LSM) allows security policies to be implemented as BPF programs attached to LSM hook points, without writing a kernel module. It uses bpf_lsm_* trampoline functions generated for every hook in lsm_hook_defs.h.

Use cases

  • Runtime policy updates without kernel rebuilds
  • Per-container or per-cgroup security policies
  • Rapid prototyping of new access controls
  • Supplementing SELinux or AppArmor with custom rules

Limitations

  • Cannot store persistent state across reboots (use maps)
  • BPF verifier restricts program complexity
  • Cannot return custom error types (limited to standard errno)
  • Requires CAP_BPF + CAP_SYS_ADMIN to load programs

Attaching BPF Programs to LSM Hooks

/* BPF program (kernel side) — compiled with clang and loaded via libbpf */

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

/* Deny write access to /etc/passwd inode */
SEC("lsm/file_open")
int BPF_PROG(my_file_open, struct file *file)
{
    struct inode *inode = BPF_CORE_READ(file, f_inode);
    umode_t mode = BPF_CORE_READ(inode, i_mode);
    __u64 ino = BPF_CORE_READ(inode, i_ino);
    __u32 flags = BPF_CORE_READ(file, f_flags);

    /* Block write opens to inode 1234567 (example: /etc/passwd) */
    if (ino == 1234567 && (flags & O_WRONLY || flags & O_RDWR))
        return -EACCES;

    return 0;
}

char LICENSE[] SEC("license") = "GPL";
/* Loader (userspace) using libbpf */
#include <bpf/libbpf.h>

struct bpf_object *obj = bpf_object__open("my_lsm.bpf.o");
bpf_object__load(obj);

struct bpf_program *prog =
    bpf_object__find_program_by_name(obj, "my_file_open");

struct bpf_link *link = bpf_program__attach_lsm(prog);
/* Policy is now active; unlink to remove it */
bpf_link__destroy(link);

BPF LSM Hook Coverage

All hooks defined in include/linux/lsm_hook_defs.h are available as BPF attachment points. The hook name maps directly to the BPF section name:
LSM HookBPF Section
inode_permissionlsm/inode_permission
file_openlsm/file_open
task_killlsm/task_kill
socket_connectlsm/socket_connect
bpflsm/bpf
sk_alloc_securitylsm/sk_alloc_security
Enable BPF LSM at boot with lsm=...,bpf in the kernel command line. Without this, bpf_program__attach_lsm() will return EINVAL. Also enable CONFIG_BPF_LSM=y and CONFIG_BPF_SYSCALL=y in the kernel config.

SELinux and AppArmor Reference

SELinux and AppArmor are the two most widely deployed LSMs and serve as the canonical reference implementations.
SELinux implements type enforcement (TE) — every process runs in a security domain (type) and every object (file, socket, etc.) has a type. Access is allowed only if a policy rule explicitly permits the {subject_type, object_type, object_class, permission} tuple.SELinux stores its labels in security.selinux xattrs on filesystem objects and in kernel security blobs (inode->i_security, sk->sk_security, etc.).
# Check SELinux status
getenforce

# View process context
ps -Z -p $$

# View file context
ls -Z /etc/passwd

# Generate policy from audit denials
audit2allow -a
AppArmor uses path-based access control — profiles are matched against the absolute file paths being accessed, rather than labels on objects. This makes AppArmor easier to profile for specific applications.Profiles can be in enforce (deny and log violations) or complain (log only) mode.
# List loaded profiles
aa-status

# Put a profile in complain mode
aa-complain /etc/apparmor.d/usr.bin.firefox

# Parse and load a profile
apparmor_parser -r /etc/apparmor.d/usr.bin.firefox
AppArmor profiles for new LSM hooks need the #include <abstractions/base> directive and follow the syntax:
/usr/bin/myapp {
  /etc/myapp.conf r,
  /var/log/myapp/** rw,
  network inet stream,
}
Both SELinux and AppArmor coexist with other LSMs in a stack. For example, a typical desktop Linux system may run lockdown,capability,yama,apparmor,bpf simultaneously. Each LSM independently evaluates its hooks; all must allow an operation for it to proceed.

Build docs developers (and LLMs) love