
LID — Linux Integrity Drift: Bypassing AppArmor via eBPF pathname rewriting. Pre-LSM syscall argument manipulation with zero audit footprint. "Linux is Dying"
██╗ ██╗██████╗
██║ ██║██╔══██╗
██║ ██║██║ ██║
██║ ██║██║ ██║
███████╗██║██████╔╝
╚══════╝╚═╝╚═════╝
— "Linux is Dying" —
Systematic discovery of kernel code paths that bypass LSM security guarantees
The gate was never breached. It was walked around.
The Linux Security Module framework has one core guarantee that has held for 20+ years:
Security modules can only add restrictions. They can never remove them.
This guarantee is correct. LID doesn't break it.
LID finds kernel code paths that bypass LSM hooks entirely — subsystems that perform security-sensitive operations without consulting the LSM framework. The security check is correct. The problem is that the kernel never asks.
Each finding has two distinct dimensions that should not be conflated:
The architectural blind spot. The question is not "can an attacker exploit this?" but:
If a security-sensitive operation occurs and the enforcement layer never even evaluates it, you have a visibility gap — regardless of whether an attacker can practically abuse it today. This matters for compliance, forensics, and defense-in-depth assumptions.
The real-world exploitability question:
These two things are different. A finding can be a critical visibility gap (your monitoring is blind) without being a practical privilege escalation (attacker already needs root). Conversely, a finding can be a direct escalation path with minimal prerequisites.
| Finding | Visibility Gap | Practical Escalation |
|---|---|---|
| LID-001 | Critical — AppArmor sees nothing, audit log empty, zero forensic trace | Limited — requires root or CAP_BPF+CAP_PERFMON (already privileged). Not a privilege escalation. Impact: policy evasion + audit blindness. |
| LID-002 | High — security_file_receive() never fires, fd transfer invisible to all LSMs | High — works from unprivileged userspace via io_uring. Crosses LSM enforcement boundary without any privilege. |
| LID-003 | High — security_sb_mount() bypassed, AppArmor mount policy is dead code | Medium — requires mount namespace access (CAP_SYS_ADMIN in user ns). Available in many container configs. |
| LID-004 | Critical — AppArmor sees nothing, zero BPF hooks (0/9), no audit trace for any BPF token operation | Medium — requires bpffs delegation by host + CAP_BPF in user namespace. Available in container runtimes with BPF delegation (LXD/Incus). |
| LID-005 | Medium — tc egress classifiers on container interface never evaluate AF_XDP traffic | Limited — bypasses tc egress on container eth0 (verified). Cilium NOT bypassed — enforces on node-side veth ingress + source IP verification (tested). Impact limited to plain-Docker setups with tc-only egress filtering. |
Each finding has specific kernel/config/privilege requirements. If your environment doesn't match, the finding will not reproduce.
| Condition | Required | Notes |
|---|---|---|
| Kernel version | 5.x+ | Tested on 5.15, 6.1, 6.6, 6.8 |
CONFIG_BPF_SYSCALL | =y | Default on all major distros |
CONFIG_BPF_KPROBE_OVERRIDE | =y | Ubuntu/Debian enable it, RHEL does not |
CONFIG_SECURITY_APPARMOR | =y | Target LSM must be AppArmor |
| AppArmor profile | Enforcing, deny rule on target path | Works with any path-based deny rule |
| Privileges | root or CAP_BPF + CAP_PERFMON | Cannot run unprivileged |
kernel.lockdown | none or integrity | confidentiality mode blocks kprobe attach |
kernel.unprivileged_bpf_disabled | Irrelevant | Requires CAP_BPF regardless |
fs.protected_hardlinks | 0 for cross-user links | 1 (default) still allows same-user hard links |
| SELinux instead of AppArmor | Does not work | SELinux is inode-based, not pathname-based |
| Condition | Required | Notes |
|---|---|---|
| Kernel version | 6.0+ | IORING_MSG_SEND_FD added in 6.0 |
CONFIG_IO_URING | =y | Default on all major distros |
| Privileges | None | Works from unprivileged userspace |
io_uring_disabled sysctl | 0 (default) | 2 blocks unprivileged, 1 blocks all |
| Target LSM | Any (SELinux, AppArmor, Smack) | security_file_receive() is a generic LSM hook |
kernel.lockdown | Irrelevant | No BPF involved |
| Condition | Required | Notes |
|---|---|---|
| Kernel version | 6.9+ | BPF token introduced in 6.9 |
CONFIG_BPF_SYSCALL | =y | Default on all major distros |
CONFIG_SECURITY_APPARMOR | =y | Ubuntu/Debian default |
| bpffs with delegation | Yes | Host must mount with delegate_* options |
| Privileges | CAP_BPF in user namespace | Trivially available to userns root |
| SELinux instead of AppArmor | Not affected | SELinux implements all 9 BPF hooks |
| Condition | Required | Notes |
|---|---|---|
| Kernel version | 5.2+ | fsopen/fsmount introduced in 5.2 |
CONFIG_SECURITY_APPARMOR | =y | Only AppArmor is affected |
| Privileges | CAP_SYS_ADMIN in user namespace | Available with unshare -m |
| SELinux instead of AppArmor | Does not work | SELinux implements security_sb_kern_mount() |
| Container runtime | Depends on seccomp filter | Docker default seccomp blocks fsopen — Podman/LXC may not |
| Condition | Required | Notes |
|---|---|---|
| Kernel version | 4.18+ | AF_XDP introduced in 4.18 |
CONFIG_XDP_SOCKETS | =y | Default on all major distros |
| Privileges | CAP_NET_RAW only | Default in Docker, Kubernetes pods |
| Container runtime | Docker, K8s, LXC | Default capability set |
| tc-based network policy | Yes | Cilium eBPF, Calico, tc u32/flower |
| Environment | LID-001 | LID-002 | LID-003 | LID-004 | LID-005 |
|---|---|---|---|---|---|
| Ubuntu 22.04+ (AppArmor, default) | Works | Works | Works | Works (6.9+) | Works |
| Debian 12+ (AppArmor) | Works | Works | Works | Works (6.9+) | Works |
| RHEL/Fedora (SELinux) | No | Works | No | No | Works |
lockdown=confidentiality | No | Works | Works | Partially | Works |
| Unprivileged user | No | Works | Depends on user ns | Depends on bpffs delegation | No |
| Container (no CAP_BPF) | No | Depends on io_uring | Depends on seccomp | No | Works |
| Container (CAP_NET_RAW dropped) | No | Depends | Depends | No | No |