Kernel#
The kernel is the resident process at the bottom of every Linux box, the only thing on the host the operator cannot work around. Every syscall, every file open, every packet, every privilege check, every namespace boundary is the kernel’s call. Read what the kernel is doing, what version is loaded, what modules are live, and what the sysctls say, and the operator has read the constraints the rest of the host operates under.
The kernel is what the boot sequence ends at (see Boot). The userland binaries the operator runs (see The Terminal and Processes) are clients of the syscall surface. The filesystem (see Filesystem), the network stack (see Networking), the security model (see Hardening, Permissions), and the memory map (see Memory) are all kernel subsystems. This page is about the kernel as a unit: how to identify it, inspect it, configure it, and read what it is doing right now.
Concept#
The kernel is the privileged software that owns the hardware. On x86 it runs in CPU ring 0 with full access to physical memory, I/O ports, and control registers. Every other process on the machine (the shell, the init system, the operator’s tools, daemons, browsers) runs in ring 3, unprivileged, with no direct path to the hardware. The kernel mediates every request that crosses that boundary.
The mediation happens at one place. The system call. A userspace program asks the kernel to do something (open a file, send a packet, allocate memory, fork a process), the CPU transitions into kernel mode long enough to do it, then returns. The kernel is preemptive. A running process can be interrupted mid-syscall to run something else; the userspace process cannot return the favor.
Three properties follow that are worth committing to memory.
Single shared address space. All kernel code shares one virtual memory layout, separate from any userspace process. A kernel bug in one driver can corrupt another driver; a userspace bug cannot reach kernel memory without a vulnerability.
Cooperative within, preemptive without. Kernel threads run until they yield or block on I/O; userspace processes run only when the scheduler grants them a slice.
Stable userspace ABI. Linus enforces “we don’t break userspace”. A binary built against a 2.6 kernel still runs on a 2026 kernel without recompilation.
The kernel is not an application. It has no main() it can
exit from. The bootloader loads it (see Boot),
it takes ownership of the hardware, starts /sbin/init as PID 1,
and then services syscalls until the machine powers off.
Components#
The kernel ships as a single binary (the vmlinuz image) plus a
collection of loadable modules. Inside that binary, work is split
across the subsystems below. Each has its own maintainers, mailing
list, and merge tree, but they share the kernel address space and
call one another freely.
mindmap
root((Linux<br/>kernel))
Scheduler
CFS (legacy)
EEVDF (6.6+)
SCHED_FIFO / RR
SCHED_DEADLINE
Memory
Buddy allocator
Slab / slub
Swap + OOM killer
THP / KSM
VFS
ext4 / XFS / Btrfs
FUSE / NFS
proc / sys / tmpfs
Network
Sockets + IP
TCP / UDP / QUIC
netfilter / nftables
eBPF / XDP
Security
Capabilities
Namespaces
Cgroups
LSM (SELinux, AppArmor)
seccomp / IMA / Yama
Drivers
Block
Network
Input
Char / V4L2 / ALSA
IPC
Pipes / FIFOs / signals
SysV + POSIX IPC
Futexes
UNIX sockets
Tracing
ftrace / tracepoints
perf / kprobes / uprobes
eBPF (bcc, bpftrace)
Process Scheduler#
Picks which runnable task gets which CPU core at each moment. CFS
(Completely Fair Scheduler) was the historical default; EEVDF
(Earliest Eligible Virtual Deadline First) replaced it in 6.6.
Real-time (SCHED_FIFO, SCHED_RR) and deadline
(SCHED_DEADLINE) classes sit alongside for tasks that need
bounded latency.
Memory Manager#
Owns every byte of physical RAM and the per-process virtual address
spaces that map onto it. Every userspace malloc eventually lands
here, every shared library mapping, every file the kernel caches,
every page the operator sees in /proc/PID/maps.
The subsystem decomposes into seven cooperating pieces. The
operator sees their effects in /proc/meminfo, vmstat,
smem, slabtop, and dmesg when one of them complains.
Component |
Role |
|---|---|
Buddy allocator |
The page-level back end. Hands out physical pages in power-
of-two orders (4 KiB, 8 KiB, 16 KiB, … up to |
Slab / SLUB allocator |
The object-level front end for the kernel itself. Carves
buddy pages into typed caches ( |
Virtual memory (per process) |
The |
Page cache |
The kernel’s read-through cache for file-backed pages.
Every |
Swap |
The pressure-relief valve. Anonymous pages (heap, stack,
|
OOM killer |
The last-resort enforcer when physical RAM plus swap is
exhausted. Picks a process by |
Transparent Huge Pages (THP) + KSM |
Two optional optimisations. THP transparently promotes 2 MiB / 1 GiB pages where the workload is friendly (large contiguous anonymous regions). KSM scans anonymous pages and merges identical ones across processes; valuable on hosts running many similar VMs or containers. |
flowchart LR
APP["User process<br/>malloc / mmap / brk"] -->|page fault| MM["mm subsystem"]
MM --> VMA["per-process VMAs<br/>(/proc/PID/maps)"]
MM --> PC["page cache<br/>(file-backed)"]
MM --> SLAB["slab / SLUB<br/>(kernel objects)"]
SLAB --> BUDDY["buddy allocator<br/>(physical pages)"]
PC --> BUDDY
VMA --> BUDDY
BUDDY --> RAM["Physical RAM<br/>(/proc/meminfo)"]
BUDDY -. pressure .-> RECL["page reclaim<br/>(kswapd / direct)"]
RECL --> SWAP[("swap<br/>+ zswap / zram")]
RECL --> THP["THP / KSM<br/>(compact + dedup)"]
SWAP -. exhausted .-> OOM["OOM killer<br/>(dmesg: Out of memory:)"]
For the operator-facing deep dive (cgroups memory accounting, NUMA, kmem leaks, kernel-pointer-deref triage), see Memory.
VFS and Filesystems#
The Virtual File System layer is the kernel’s “one API for every
storage backend” abstraction. Userspace calls open, read,
write, stat; the VFS dispatches to whichever filesystem
driver owns the path, whether that’s ext4 on local SSD, NFS on the
network, procfs synthesising data on the fly, or FUSE bouncing
into a userland daemon. Every path the operator types eventually
touches this layer.
The subsystem decomposes into five cooperating pieces.
Component |
Role |
|---|---|
VFS core |
The dispatch layer. Defines |
Dentry cache |
Path-component cache. |
Inode cache |
Object-attribute cache for files and directories
(permissions, owner, size, timestamps). Backed by the
slab allocator. |
Filesystem drivers |
The backends. Local disk (ext4, XFS, Btrfs, F2FS, FAT,
NTFS3); network (NFS, CIFS, 9P); special / pseudo
( |
Block / I/O layer |
Below local filesystems. Multi-queue block scheduler
( |
flowchart TB
APP["User process<br/>open / read / write / stat / mmap"] --> SYS["Syscall layer"]
SYS --> VFS["VFS core<br/>(file / inode / dentry vtables)"]
VFS --> DC["Dentry cache<br/>(path resolution)"]
VFS --> IC["Inode cache<br/>(attributes)"]
VFS --> FS{"Filesystem driver<br/>(by mount)"}
FS -->|local disk| EXT["ext4 / XFS / Btrfs / F2FS"]
FS -->|special| PROC["procfs / sysfs / cgroup<br/>tmpfs / devtmpfs / debugfs"]
FS -->|network| NFS["NFS / CIFS / 9P"]
FS -->|userland| FUSE["FUSE → userland daemon"]
EXT --> PC["Page cache"]
PC --> BLK["Block / I/O layer<br/>(mq scheduler, dm, md)"]
BLK --> DRV["Block device driver"]
DRV --> HW["Disk / SSD / NVMe"]
See Filesystem for the operator- facing deep dive on filesystem choice, repair, and forensics.
Network Stack#
One in-kernel pipeline from the NIC driver up to the BSD socket
API. The operator’s ssh, curl, dig, nc, plus every
listening daemon, every container’s overlay traffic, every WireGuard
tunnel, all traverse the same code paths. The stack is per-CPU
locked, RCU-heavy, and increasingly programmable through eBPF /
XDP for high-throughput cases.
Eight layers compose it, top to bottom.
Component |
Role |
|---|---|
Socket API |
|
Protocol handlers (L4) |
TCP, UDP, SCTP, QUIC (kernel and userland variants), DCCP.
Implement reliability, ordering, congestion control
( |
IP layer (L3) |
IPv4 and IPv6. Routing ( |
Netfilter / nftables |
The kernel’s packet-filtering, NAT, and connection-
tracking framework. Hooks at PREROUTING, INPUT, FORWARD,
OUTPUT, POSTROUTING. |
Traffic control (tc) |
Egress and ingress queueing, shaping, policing, and
classification. |
eBPF / XDP |
eXpress Data Path: BPF programs attached at the driver
level, before |
Link layer (L2) |
|
NIC driver + ring buffers |
Vendor driver ( |
flowchart TB
APP["User process<br/>socket / send / recv / epoll / io_uring"] --> SOCK["Socket layer<br/>(/proc/net/tcp, ss)"]
SOCK --> L4["TCP / UDP / QUIC / SCTP<br/>congestion control"]
L4 --> NF1["netfilter OUTPUT / POSTROUTING<br/>conntrack, NAT"]
NF1 --> L3["IP layer (v4 / v6)<br/>routing, neighbour"]
L3 --> TCO["tc egress<br/>fq_codel / HTB / BPF"]
TCO --> L2["L2: bridge / VLAN / bond / veth"]
L2 --> DRV["NIC driver<br/>TX ring, offloads"]
DRV ==> NIC["Hardware NIC"]
NIC ==> DRV2["NIC driver<br/>RX ring, NAPI"]
DRV2 --> XDP["XDP hook<br/>(pre-skb, fastest drop / fwd)"]
XDP --> L2R["L2 ingress"]
L2R --> TCI["tc ingress<br/>(BPF classifier)"]
TCI --> NF2["netfilter PREROUTING / INPUT<br/>conntrack, NAT"]
NF2 --> L3R["IP layer (v4 / v6)"]
L3R --> L4R["TCP / UDP / QUIC / SCTP"]
L4R --> SOCKR["Socket layer"]
SOCKR --> APPR["User process"]
See Networking for the operator-facing deep dive on protocols, tools, and tradecraft.
Security and Isolation#
Capabilities split root into ~40 fine-grained permissions;
namespaces give each process tree its own view of resources;
cgroups cap and account resource use; the LSM framework (SELinux,
AppArmor, Tomoyo, Smack) plugs in mandatory access control; seccomp
filters syscalls; IMA/EVM measure file integrity; Yama gates
ptrace. These compose to build containers and sandboxes.
Device Drivers#
Code that translates between the kernel’s uniform abstractions and the specific quirks of one piece of hardware. The kernel ships ~50,000 drivers; one kernel image runs on every laptop, server, phone, router, and embedded box because the right drivers load on demand for whatever the bus enumeration finds. Drivers are also the largest attack surface in the kernel; most kernel CVEs in any given year are in drivers.
Driver code fits into one of seven framework slots so userspace sees uniform interfaces over wildly different hardware.
Class |
Userspace face |
Examples and notes |
|---|---|---|
Block |
|
SATA, NVMe, SCSI, virtio-blk, MMC. Plugs into the block layer (mq scheduler, request queue) below the page cache. |
Network |
|
|
Char |
|
Serial ( |
Input |
|
Keyboard, mouse, touchpad, joystick, touchscreen. evdev
protocol; |
USB / PCI / I2C / SPI buses |
|
Bus core enumerates devices and matches them to drivers
by vendor/device ID. |
V4L2 / DRM / ALSA |
|
Multimedia frameworks. V4L2 for capture, DRM/KMS for GPU and display, ALSA for audio. |
MTD / virtio / IIO / others |
varies |
Raw flash, paravirtualised devices for guests, industrial I/O sensors, and the long tail. |
flowchart TB
HW["Hardware<br/>NIC, NVMe, USB, GPU, sensor, ..."] --> BUS["Bus core<br/>PCI / USB / I2C / SPI / platform"]
BUS -->|enumerate +<br/>match vendor:device ID| MATCH{"Driver match?"}
MATCH -->|yes| DRV["Driver<br/>(built-in or loadable module)"]
MATCH -->|no| WAIT["Wait for module<br/>(udev / modprobe)"]
WAIT --> DRV
DRV --> FW{"Framework slot"}
FW -->|disk / SSD / NVMe| BLK["Block layer<br/>/dev/sd*, /dev/nvme*"]
FW -->|NIC| NET["Network layer<br/>/sys/class/net/"]
FW -->|keyboard, mouse| INPUT["evdev<br/>/dev/input/event*"]
FW -->|TPM, serial, hwrng| CHAR["Char dev<br/>/dev/<name>"]
FW -->|GPU / display| DRM["DRM/KMS<br/>/dev/dri/*"]
FW -->|camera| V4L["V4L2<br/>/dev/video*"]
FW -->|audio| ALSA["ALSA<br/>/dev/snd/*"]
BLK --> APP["User process<br/>(uniform file API)"]
NET --> APP
INPUT --> APP
CHAR --> APP
DRM --> APP
V4L --> APP
ALSA --> APP
Most drivers ship as loadable modules under
/lib/modules/$(uname -r)/kernel/drivers/; the dispatch table
is auto-built from MODULE_DEVICE_TABLE declarations so udev
can match a freshly-plugged USB ID to the right module.
IPC and Synchronisation#
The kernel-side primitives that let separate processes coordinate
without raw shared memory. Every | in a shell pipeline, every
Ctrl+C, every D-Bus message, every container’s docker.sock,
every mutex inside a Python program, all bottom out in one of the
mechanisms below. The operator who reads them in /proc/PID/fd/
and /proc/PID/status answers “what is this process actually
talking to” without guesswork.
Eight primitives, grouped by communication pattern.
Primitive |
Role |
|---|---|
Pipes / FIFOs |
Unidirectional byte stream between processes. Anonymous
( |
Signals |
Asynchronous notifications: |
System V IPC |
The classic trio. Message queues ( |
POSIX IPC |
Modernised replacements. |
UNIX-domain sockets |
Local-only sockets, file-backed ( |
eventfd / signalfd / timerfd |
File-descriptor versions of events, signals, and timers
so a single |
Futexes |
The kernel-side fast wait/wake primitive that userspace
mutex, condvar, and semaphore libraries ( |
Memory-mapped + atomics |
|
flowchart LR
P1["Process A"] --> PIPE["Pipe / FIFO<br/>byte stream"]
PIPE --> P2["Process B"]
P1 -.signal.-> P2
P1 <--> UDS["UNIX-domain<br/>socket<br/>/run/foo.sock"]
UDS <--> P2
P1 --> SVIPC["SysV IPC<br/>(ipcs)<br/>msgq / sem / shm"]
SVIPC --> P2
P1 --> POSIX["POSIX IPC<br/>/dev/mqueue<br/>/dev/shm"]
POSIX --> P2
P1 --> FUTEX[("Futex<br/>(uaddr in shared page)")]
FUTEX --> P2
P1 <--> SHMEM[("mmap MAP_SHARED<br/>+ atomics<br/>(no syscalls fast path)")]
SHMEM <--> P2
EV["eventfd /<br/>signalfd /<br/>timerfd"] -.-> EP[("epoll / io_uring<br/>(uniform wait)")]
P1 -.-> EP
P2 -.-> EP
Tracing and Profiling#
The instrumentation surface the kernel exposes for “what is this
box doing right now, in detail”. Five primitives plus the eBPF VM
underneath compose every modern observability tool the operator
reaches for: strace, perf, bpftrace, bcc,
Falco, Tetragon, Cilium, Tracee. The mechanisms
are also the operator’s offensive surface; the same hooks that
audit a process can instrument cryptographic libraries or steal
keystrokes.
Six layers, lowest to highest.
Component |
Role |
|---|---|
Tracepoints |
Static, named instrumentation points compiled into the
kernel. Stable contract. |
kprobes / kretprobes |
Dynamic probes that attach to any kernel function on
demand ( |
uprobes / uretprobes |
Same idea, attached to userspace functions
( |
perf events |
Hardware PMU counters (cache misses, branch
mispredictions, IPC) and software events (page faults,
context switches). |
ftrace |
Built-in function tracer. |
eBPF VM |
The unifying runtime. Bytecode programs attached to
tracepoints, kprobes, uprobes, perf events, LSM hooks,
XDP, and tc. Verified for safety before load; data out
via per-CPU maps and ring buffers. Powers |
flowchart LR
subgraph SRC["Event sources"]
TP["Tracepoints<br/>(static, /sys/kernel/tracing)"]
KP["kprobes<br/>(dynamic, any kernel fn)"]
UP["uprobes<br/>(dynamic, any user fn)"]
PE["perf events<br/>(PMU + software)"]
LSM["LSM hooks<br/>(BPF LSM)"]
XDP["XDP / tc<br/>(packet path)"]
end
subgraph VM["eBPF VM<br/>verifier + JIT"]
PROG["BPF program"]
MAP["maps<br/>(hash / array /<br/>perf / ringbuf)"]
end
TP --> PROG
KP --> PROG
UP --> PROG
PE --> PROG
LSM --> PROG
XDP --> PROG
PROG --> MAP
MAP --> US["User-space tools"]
subgraph TOOLS["User-space tools"]
BT["bpftrace"]
BCC["bcc"]
PERF["perf"]
FT["ftrace / trace-cmd"]
FALCO["Falco / Tetragon /<br/>Cilium / Tracee"]
end
Interfaces#
The kernel sits between two worlds: userspace above, hardware below. It also has a third, less obvious boundary: between its own core image and the loadable modules that extend it. Each of these three boundaries is a separate contract, with a separate stability policy, a separate audience, and a separate failure mode when the operator gets it wrong.
Interface |
Faces |
Stability |
Failure mode when broken |
|---|---|---|---|
Syscall ABI |
User space |
Stable by policy (“do not break userspace”) |
Userspace binary stops working; treated as a kernel bug. |
Hardware interface |
Hardware below |
Per-arch + vendor; defined by buses, IRQs, DMA, ACPI / DT |
Device fails to enumerate; |
Module ABI |
Kernel modules above the core |
Unstable by policy; can change every release |
Module fails to load after kernel upgrade; |
The diagram below shows the three together. Operator angle: syscalls are where instrumentation and exploitation hook; the hardware interface is where driver bugs and firmware quirks live; the module ABI is why a kernel upgrade can silently break NVIDIA, OpenZFS, or a vendor backport.
User-Space ABI (Syscalls)#
The kernel’s contract with everything above it, and the single
most enforced stability promise in Linux. About 400 numbered
system calls (read, write, mmap, execve, fork,
ioctl, bpf, …) plus a per-architecture calling
convention define the surface. A 1996 binary still runs on a 2026
kernel. Linus has been quoting “we do not break userspace” for
twenty-plus years and means it.
Five pieces compose this interface.
Component |
Role |
|---|---|
Syscall numbers |
One per call, per arch ( |
Calling convention |
Which registers carry what. x86_64 SysV uses |
Trap instruction |
|
vDSO |
“virtual dynamic shared object”, a tiny ELF mapped into
every process. Serves |
Audit / seccomp gates |
Layered between trap and handler. |
flowchart TB
APP["User process<br/>(ring 3)"] --> CHOICE{"Which path?"}
CHOICE -->|vDSO call<br/>gettimeofday, clock_gettime,<br/>getcpu, time, rt_sigreturn| VDSO["vDSO<br/>(user-mode read of<br/>kernel-maintained page)"]
VDSO --> APP
CHOICE -->|everything else| LIBC["libc / direct syscall<br/>load arg registers"]
LIBC --> TRAP["trap instruction<br/>x86_64 SYSCALL · arm64 SVC<br/>riscv ECALL · power SCV"]
TRAP --> ENTRY["arch syscall entry<br/>(entry_SYSCALL_64 / el0_svc)"]
ENTRY --> NUMTBL["sys_call_table[rax]"]
NUMTBL --> AUDIT["audit / seccomp / LSM<br/>(deny here = EPERM / kill)"]
AUDIT --> HND["sys_<name> handler<br/>vfs_read, do_mmap, ..."]
HND --> RET["SYSRET / ERET<br/>(return value in rax / x0)"]
RET --> APP
Hardware Interface (Drivers)#
The kernel’s contract with the hardware below it. Buses (PCI, USB,
I2C, SPI, MMIO) define how to talk to devices; the device model
under /sys/devices tracks what is plugged in; IRQ lines deliver
interrupts; DMA engines move data without CPU involvement; the
device tree (ARM, RISC-V) or ACPI (x86) tells the kernel what
hardware exists at boot. Drivers fit framework slots (the block
layer, the network layer, the input subsystem, V4L2 for video,
ALSA for audio) so userspace sees uniform abstractions over varied
hardware.
In-Kernel Module ABI#
The kernel’s contract with itself, between the core image and
loadable modules. It is deliberately unstable. Every kernel
release can change in-kernel function signatures, structure
layouts, and locking rules. Distros sign modules against a specific
kernel build; out-of-tree modules (NVIDIA, OpenZFS, vendor drivers)
recompile against /lib/modules/$(uname -r)/build headers. This
is why module-load failures after a kernel upgrade are routine and
why userspace breakage after a kernel upgrade is treated as a bug.
flowchart TB
subgraph US["User space (ring 3)"]
direction TB
APP["Applications<br/>shell, daemons, browsers, agents"]
LIB["Libraries<br/>libc (glibc / musl / Bionic), libssl, ..."]
VDSO["vDSO fast path<br/>gettimeofday, clock_gettime, getcpu<br/>no kernel trap"]
APP --> LIB
APP -.-> VDSO
end
ABI([" 1. Syscall ABI · STABLE<br/>open, read, mmap, execve, bpf, ioctl, ...<br/>'do not break userspace'"])
subgraph KS["Kernel space (ring 0)"]
direction TB
ENTRY["syscall entry<br/>x86_64 SYSCALL · arm64 SVC"]
subgraph CORE["Core subsystems"]
direction LR
SCH["Scheduler<br/>EEVDF / RT"]
MM["Memory<br/>pages, slab, OOM"]
VFS["VFS<br/>ext4, XFS, Btrfs, proc, sys"]
NET["Network<br/>sockets, IP, TCP/UDP, netfilter"]
SEC["Security<br/>LSM, namespaces, cgroups, seccomp"]
end
MABI([" 2. Module ABI · UNSTABLE<br/>in-kernel function signatures<br/>recompile per release"])
DRV["Device drivers<br/>built-in + loadable modules"]
ENTRY --> CORE
CORE --> MABI
MABI --> DRV
end
HWABI([" 3. Hardware interface<br/>PCI · USB · I2C · SPI · MMIO · IRQ · DMA<br/>per-arch, vendor-defined"])
HW["Hardware<br/>CPU · RAM · disk · NIC · GPU"]
LIB ==>|trap| ABI
ABI ==> ENTRY
DRV ==> HWABI
HWABI ==> HW
classDef stable fill:#0d3b22,color:#d6f7df,stroke:#3fb950
classDef unstable fill:#3b1f1f,color:#fad7d7,stroke:#f85149
class ABI stable
class MABI unstable
Userland#
The kernel handles syscalls, scheduling, memory, and drivers. Everything the operator actually interacts with (the C library, init system, shell, coreutils, package manager, daemons, libraries, applications) runs in user space and is collectively the userland. Linus’ tree at kernel.org ships only the kernel; a usable OS is the kernel plus a userland someone else assembled.
What that userland is varies by OS. The kernel is one project, the userland is a separate project (or many), and any given OS picks-and-mixes.
OS |
Kernel |
Userland origin |
|---|---|---|
Most Linux distros |
Linux |
GNU (glibc, bash, coreutils, util-linux) plus freedesktop.org (systemd, dbus). Hence “GNU/Linux”. |
Alpine |
Linux |
musl libc, BusyBox coreutils, OpenRC init. |
Android |
Linux |
AOSP: Bionic libc, Toybox utilities, Android |
ChromeOS |
Linux |
Gentoo-derived GNU userland with Google’s session manager and UI. |
FreeBSD, OpenBSD |
BSD kernel |
Their own BSD-licensed userland (libc, sh, ls, …) maintained in-tree. |
macOS |
XNU (Mach + BSD) |
Apple’s Darwin userland (BSD heritage) plus Cocoa frameworks. |
The practical consequence for the operator. “Linux” by itself is ambiguous. A Debian box and an Android phone share a kernel, but their userlands don’t share a single binary. Cross-compiled tooling, static binaries, and prebuilt wheels all break across libc boundaries (glibc vs musl vs Bionic), and exploit chains that lean on coreutils or shell behavior are tied to the userland, not the kernel.
Identification#
The first move on an unfamiliar box. Pin down which kernel is running, which command line it booted with, and how it identifies itself.
flowchart LR
SH["Shell on<br/>unfamiliar host"] --> UN["uname -srm<br/>release + arch"]
UN --> PV["/proc/version<br/>build banner<br/>(compiler, date, distro)"]
PV --> PCMD["/proc/cmdline<br/>boot args<br/>(root=, init=, lockdown=, ima=)"]
PCMD --> OS["/etc/os-release<br/>distro + version"]
OS --> BOOT["ls /boot/vmlinuz-*<br/>installed kernels"]
BOOT --> CVE["Map distro string<br/>to CVE applicability"]
$ uname -a
$ uname -srm
$ cat /proc/version
$ cat /proc/cmdline
$ cat /etc/os-release # distro, for context
$ ls /boot/vmlinuz-*
uname is the first read; /proc/version is the build banner
(compiler, build host, build time); /proc/cmdline is the kernel
command line passed by the bootloader (root device, init, security
mode toggles, debug flags).
Useful flags:
Flag |
Meaning |
|---|---|
|
kernel name ( |
|
kernel release ( |
|
kernel build banner (date and config tag) |
|
machine hardware ( |
|
processor (often |
|
operating system ( |
|
all of the above |
Other identification surfaces:
hostnamectl(systemd) prints kernel + distro + machine ID together./sys/kernel/versionand/sys/kernel/osreleasemirror whatunamereports./proc/sys/kernel/ostype,osrelease,versionare the same data via the sysctl tree.
Modules#
Most of the kernel’s drivers, filesystems, and protocol handlers ship as loadable modules rather than being linked into the core image. Modules let one kernel binary support every NIC, every filesystem, every USB device the operator might encounter, while loading code only when it’s needed. A few subsystems are always built in (the scheduler, memory manager, VFS core), but the peripheral surface is module-driven.
Modules live under /lib/modules/$(uname -r)/ and load through
udev triggers, systemd-modules-load, or explicit
modprobe.
flowchart LR
HW["Hardware event<br/>(USB plug, PCI add)"] --> UDEV["udev<br/>(systemd-udevd)"]
BOOT["Boot<br/>systemd-modules-load"] --> MOD["modprobe<br/>(kmod)"]
CLI["Operator<br/>modprobe MODULE"] --> MOD
UDEV --> MOD
MOD --> RES["Resolve deps<br/>modules.dep"]
RES --> CONF["Apply modprobe.d<br/>options, alias, blacklist"]
CONF --> SIG["Verify signature<br/>(Secure Boot / lockdown)"]
SIG --> LOAD["init_module()<br/>syscall"]
LOAD --> RUN["Module live<br/>/sys/module/<name>"]
LOAD --> LOG["printk -> dmesg"]
$ lsmod # loaded modules
$ lsmod | grep -E 'nf_|nft_' # netfilter / nftables modules
$ modinfo MODULE # parameters, license, author, deps
$ sudo modprobe MODULE # load
$ sudo modprobe -r MODULE # unload (rmmod is the older form)
$ sudo dmesg -T | tail # confirm load events
Listing what’s loaded:
Module Size Used by
nf_conntrack_netlink 49152 0
nfnetlink 20480 3 nf_conntrack_netlink,...
xfrm_user 45056 1
...
Persistent control:
/etc/modules-load.d/*.conf: list module names to load at boot./etc/modprobe.d/*.conf: pass parameters, set aliases, blacklist.blacklist MODULEin amodprobe.dfile prevents auto-load.
Operator-relevant uses:
Hunt unsigned modules on a host with Secure Boot or kernel lockdown.
cat /proc/keysshows trusted signing keys; tainted modules show(O)or(P)flags inlsmod.Find LKM rootkits. Compare
lsmodagainst the persistent config; check for hidden modules viacat /proc/modulesvsls /sys/module/divergence; checkdmesgfor load events during the suspect window.Disable risky kernel features (e.g.
usb-storage,firewire-core,bluetooth) on a hardened workstation.
Module signing and lockdown live under
/sys/kernel/security/lockdown and /proc/sys/kernel/modules_disabled.
Syscalls#
Every interaction the operator has with the kernel goes through a system call: open, read, write, mmap, fork, execve, ptrace, bpf, ioctl, and a few hundred others. The syscall ABI is the contract between userspace and the kernel; profiling, debugging, and tradecraft all anchor here.
sequenceDiagram
autonumber
participant APP as User process<br/>(ring 3)
participant LIBC as libc<br/>(glibc / musl)
participant K as Kernel entry<br/>(ring 0)
participant CHK as Pre-handler checks
participant SUB as Subsystem<br/>(VFS / net / mm)
APP->>LIBC: read(fd, buf, n)
Note right of LIBC: load registers<br/>rax = 0 (NR_read)<br/>rdi = fd<br/>rsi = buf<br/>rdx = n
LIBC->>K: SYSCALL (trap, ring 3 → 0)
Note over K: entry_SYSCALL_64<br/>save user regs, switch GS,<br/>load kernel stack
K->>CHK: dispatch via sys_call_table[0]
CHK->>CHK: seccomp filter
CHK->>CHK: audit_filter_syscall
CHK->>CHK: LSM hook (SELinux / AppArmor)
CHK->>CHK: tracepoint sys_enter (kprobe / bpf attach)
CHK->>SUB: sys_read → vfs_read → file_op->read_iter
SUB-->>CHK: bytes returned, or -errno
CHK->>CHK: tracepoint sys_exit
CHK-->>K: rax = bytes or -errno
K-->>LIBC: SYSRET (restore ring 3)
LIBC-->>APP: ssize_t return value
Watch what a process is doing:
$ sudo strace -f -p PID # follow forks; live
$ sudo strace -e openat,connect,execve -p PID
$ sudo strace -c CMD # summary count + time per syscall
$ ltrace CMD # library calls (one level above syscalls)
$ sudo perf trace -p PID # lower-overhead alternative
$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s\n", str(args->filename)); }'
Inspect kernel-side surface:
$ ausyscall x86_64 --dump | head -40 # syscall numbers (audit-userspace)
$ ls /sys/kernel/debug/tracing/events/syscalls/ | head
$ cat /proc/PID/syscall # current syscall + args
$ cat /proc/PID/status | grep -i 'cap\|seccomp\|nohash'
The vDSO (virtual dynamic shared object, mapped per process)
implements a few cheap calls (gettimeofday, clock_gettime,
getcpu) without crossing into the kernel; cat /proc/PID/maps |
grep vdso shows the mapping.
/proc and /sys#
The kernel’s two read-write windows into itself.
``/proc`` is the older interface, started as per-process state
(/proc/PID/...) and grew to hold kernel-wide knobs and
read-only views.
``/sys`` is the newer, typed interface for the device model (buses, devices, drivers, kernel objects).
flowchart LR
subgraph PROC["/proc (procfs)"]
direction TB
PPID["/proc/PID/<br/>per-process state"]
PKERN["/proc/version<br/>cmdline / meminfo / loadavg"]
PSYSCTL["/proc/sys/<br/>sysctl tree"]
end
subgraph SYS["/sys (sysfs)"]
direction TB
SBUS["/sys/class/<br/>net, block, input"]
SDEV["/sys/devices/<br/>bus topology"]
SKERN["/sys/kernel/<br/>security, tracing, debug"]
SMOD["/sys/module/<br/>loaded modules"]
end
PS["ps, top"] --> PPID
FREE["free, vmstat"] --> PKERN
SYSCTL["sysctl"] --> PSYSCTL
IP["ip, ethtool"] --> SBUS
LSBLK["lsblk, smartctl"] --> SBUS
LSMOD["lsmod, modinfo"] --> SMOD
SECTL["aa-status, sestatus"] --> SKERN
Together they’re how every well-behaved Linux tool reads the host.
ps reads /proc/PID, ip reads /sys/class/net,
lsblk walks /sys/block, free reads /proc/meminfo.
Selected entries every operator should know cold:
Path |
What it shows |
|---|---|
|
kernel build banner |
|
boot command line |
|
CPU model, flags (vmx, svm, sgx, aes, …) |
|
memory totals, slab, swap |
|
1 / 5 / 15-minute load |
|
uptime + idle time |
|
mounted filesystems (kernel view) |
|
loaded modules (low-level form of |
|
kernel symbol table (often restricted) |
|
keyring contents |
|
the process address space |
|
one-screen process summary |
|
resource limits |
|
per-process namespaces (mnt, pid, net, user, ipc, uts, cgroup, time) |
|
the sysctl tree |
|
network interfaces |
|
block devices |
|
EFI variables (when booted UEFI) |
|
LSM and lockdown state |
|
ftrace control plane |
|
debugfs (root-only on hardened systems) |
sysctl#
Knobs the operator turns to change kernel behavior at runtime. The
sysctl tree is exposed under /proc/sys/ (read or write a file)
and via the sysctl command (which writes the same files with
nicer syntax).
mindmap
root((sysctl))
kernel.
kptr_restrict
dmesg_restrict
unprivileged_bpf_disabled
yama.ptrace_scope
core_pattern
randomize_va_space
net.
ipv4.ip_forward
ipv4.tcp_syncookies
ipv4.conf.all.rp_filter
ipv4.conf.all.accept_redirects
ipv6.conf.all.accept_ra
core.somaxconn
vm.
swappiness
overcommit_memory
max_map_count
drop_caches
min_free_kbytes
fs.
file-max
inotify.max_user_watches
protected_symlinks
protected_hardlinks
suid_dumpable
user.
max_user_namespaces
max_pid_namespaces
dev.
tty.legacy_tiocsti
$ sysctl -a 2>/dev/null | wc -l # how many knobs there are
$ sysctl net.ipv4.ip_forward
$ sudo sysctl -w net.ipv4.ip_forward=1 # live; lost on reboot
$ sysctl --system # reload from /etc/sysctl.d/
Persistence:
/etc/sysctl.conf: legacy, still works./etc/sysctl.d/*.conf: drop-in directory; preferred.Distros ship defaults under
/usr/lib/sysctl.d/.
Hardening defaults the operator typically flips on a fresh box (see Hardening):
kernel.kptr_restrict=2
kernel.dmesg_restrict=1
kernel.unprivileged_bpf_disabled=1
kernel.yama.ptrace_scope=1
net.ipv4.tcp_syncookies=1
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.all.accept_redirects=0
net.ipv6.conf.all.accept_ra=0
fs.protected_symlinks=1
fs.protected_hardlinks=1
Logging#
The kernel writes to a single ring buffer that userland reads with
dmesg (and journalctl -k on systemd boxes, which copies the
ring buffer into the journal).
flowchart LR
SRC["Sources<br/>module load/unload, HW events,<br/>OOM, panic, netfilter LOG,<br/>SELinux / AppArmor denials"] --> PR["printk()"]
PR --> RING[(Ring buffer<br/>kmsg / /dev/kmsg)]
RING --> DM["dmesg<br/>(direct read)"]
RING --> KMSGD["systemd-journald<br/>(reads /dev/kmsg)"]
KMSGD --> JOUR[(journal<br/>/var/log/journal/)]
JOUR --> JCTL["journalctl -k"]
KMSGD --> SYSLOG["rsyslog / syslog-ng<br/>(optional forward)"]
SYSLOG --> REMOTE["Remote syslog<br/>/ SIEM"]
$ dmesg -T # human-readable timestamps
$ dmesg -T --level=err,warn # filter
$ dmesg -w # follow live
$ journalctl -k --since '1 hour ago' # systemd journal view
$ journalctl -k -p err --since today
What lands in the buffer:
Module load and unload events.
Hardware events (USB plug, NIC up, disk error).
OOM killer activity.
Panic / oops messages.
netfilter / iptables
LOGtarget output.Security events (SELinux denials, AppArmor denials, audit if routed via printk).
The ring buffer is finite (/proc/sys/kernel/printk controls
verbosity; /sys/kernel/debug/dynamic_debug/control enables
selective debug prints). Persistent kernel logs are journald’s job.
Versions and Channels#
The kernel project ships in three streams the operator should distinguish.
flowchart LR
L["Linus tree<br/>(mainline)"] -->|~9 wk tag| M["Mainline release<br/>(6.x)"]
M -->|forked| S["Stable series<br/>(6.x.y, ~2 mo)"]
M -.->|selected picks| LTS["Long-term (LTS)<br/>(2-6 yr)"]
S --> RHEL["RHEL / Debian stable<br/>frozen + backports"]
LTS --> UBHWE["Ubuntu LTS / HWE<br/>rolling stable"]
LTS --> ARCH["Arch / Gentoo / Void<br/>mainline-tracking"]
LTS --> FEDORA["Fedora<br/>current stable line"]
Upstream streams (what kernel.org tags).
Stream |
Cadence |
Support window |
Notes |
|---|---|---|---|
Mainline |
new release every ~9 weeks |
until the next mainline tag |
The version Linus tags. No backported fixes; you take the whole new tree or wait. |
Stable |
point releases on the current mainline |
~2 months, until the next mainline |
Each mainline tag becomes a stable series with targeted security and bug fixes. |
Long-term (LTS) |
chosen mainline series, point releases |
2-6 years (recent picks: 2 years) |
The kernel.org LTS page is the authoritative list of which versions get long-term care. |
Downstream patterns (how distros consume upstream).
Pattern |
Distros |
Behaviour |
|---|---|---|
Frozen with backports |
RHEL, Debian stable, SLES |
Major version stays put for the life of the release. Security and selected feature backports come from the distro maintainer’s CVE team. CVE applicability requires checking the distro DB, not upstream. |
HWE / rolling stable |
Ubuntu LTS (Hardware Enablement stack), Fedora, openSUSE Tumbleweed |
The kernel rolls forward with the upstream stable or LTS line every few months. Newer hardware support; same userland. |
Mainline-tracking |
Arch, Gentoo, Void, NixOS unstable |
Distro kernel package follows mainline or LTS directly. Operator gets the newest features and the newest regressions on roughly upstream cadence. |
Why this matters for the operator. CVE applicability depends on the
distro kernel (with backports), not the headline upstream
version. uname -r gives the distro string (6.5.0-15-generic,
5.14.0-427.42.1.el9_4.x86_64); the distro’s CVE database is the
real source of truth on whether a fix is in.
Compartmentalization#
The three primitives the kernel exposes for compartmentalisation. Containers are built on top of them; understanding what each one isolates (and what it does not) is a defender’s, attacker’s, and forensic operator’s standing question.
flowchart TB
subgraph CONT["A container (or sandbox)"]
direction LR
subgraph NS["Namespaces<br/>(what it sees)"]
MNT["mnt"]
PID["pid"]
NET["net"]
USER["user"]
IPC["ipc"]
UTS["uts"]
CGRP["cgroup"]
TIME["time"]
end
subgraph CAPS["Capabilities<br/>(what it can do as root)"]
CAP1["CAP_NET_ADMIN"]
CAP2["CAP_SYS_ADMIN"]
CAP3["CAP_DAC_OVERRIDE"]
CAPN["..."]
end
subgraph CG["Cgroups<br/>(how much it gets)"]
CPU["cpu"]
MEM["memory"]
IO["io"]
PIDS["pids"]
NETIO["net_cls / net_prio"]
end
end
SECCOMP["seccomp filter<br/>(which syscalls it can call)"] --> CONT
LSM["LSM policy<br/>SELinux / AppArmor"] --> CONT
Read together: namespaces decide what the process sees, capabilities decide what it can do if it is root inside, cgroups decide what resources it gets, and seccomp plus the LSM narrow which syscalls and operations it can issue at all. Docker, Podman, systemd-nspawn, Kubernetes, Bubblewrap, firejail, all compose the same primitives differently.
Capabilities split the historical “root or not” privilege check
into ~40 individual capabilities (CAP_NET_ADMIN,
CAP_SYS_ADMIN, CAP_DAC_OVERRIDE, CAP_SETUID, …). A
binary can run as root but with a restricted capability set; an
unprivileged user can be granted a single capability for a single
binary.
$ getpcaps PID # what a process holds
$ cat /proc/PID/status | grep ^Cap # the four sets, hex
$ getcap /usr/bin/ping # file capabilities
$ sudo setcap cap_net_raw+ep /usr/local/bin/myping
$ capsh --print # decode current shell's caps
Namespaces isolate a process’s view of a kernel resource. Eight in current kernels:
Namespace |
Isolates |
|---|---|
|
mount table |
|
process IDs ( |
|
network interfaces, routing, sockets |
|
UID / GID mappings, capabilities |
|
System V IPC, POSIX message queues |
|
hostname and domainname |
|
cgroup root view |
|
boottime / monotonic clocks (newer kernels) |
$ ls -la /proc/PID/ns/
$ readlink /proc/PID/ns/net
$ sudo nsenter -t PID -n -p ip a # enter PID's net + pid ns
$ unshare --user --map-root-user --net bash # roll a quick sandbox
Cgroups control resource use (CPU, memory, IO, PIDs) for a process tree. cgroup v1 is hierarchical per controller; cgroup v2 unifies into a single hierarchy. Modern systemd boxes use v2.
$ mount | grep cgroup
$ cat /sys/fs/cgroup/memory.max # v2 root memory cap
$ systemd-cgls # tree of cgroups
$ systemd-cgtop # live resource use per group
$ cat /proc/PID/cgroup # which group a process is in
A container runtime (Docker, containerd, podman, runc, crun)
combines all three: a new mnt + pid + net + user +
ipc + uts namespace, a per-container cgroup, and a reduced
capability set, with an OCI spec describing the lot.
Security#
The kernel exposes a layered set of security mechanisms; an operator can read which ones are active by inspection without touching the distro’s posture.
flowchart TB
subgraph US["User space"]
APP["Process"]
end
SECCOMP["seccomp BPF filter<br/>(per-process syscall allowlist)"]
APP --> SECCOMP
SECCOMP --> SYS["Syscall ABI"]
SYS --> LSM["LSM hooks<br/>SELinux | AppArmor | Tomoyo | Smack"]
LSM --> CAPS["Capabilities check<br/>(40+ cap bits)"]
CAPS --> YAMA["Yama (ptrace)"]
YAMA --> CORE["Kernel subsystem<br/>(VFS, network, mm, ...)"]
CORE --> IMA["IMA / EVM<br/>(file hash + signature)"]
CORE --> LOCK["Lockdown<br/>none / integrity / confidentiality"]
CORE --> EBPF["eBPF programs<br/>(LSM, XDP, kprobe, tracepoint)<br/>Falco, Tetragon, Cilium, Tracee"]
EBPF -.observe.-> CORE
LSMs plug into kernel security hooks. Major implementations:
SELinux (Red Hat, Fedora): label-based mandatory access control.
getenforce(Enforcing/Permissive/Disabled);ausearch -m AVCfor denials.AppArmor (Debian, Ubuntu, SUSE): path-based MAC.
aa-status;/sys/kernel/security/apparmor/profiles.Tomoyo, Smack: rarely seen in production.
Multiple LSMs can be stacked (lsm= boot parameter). Read which
are active:
$ cat /sys/kernel/security/lsm
$ getenforce 2>/dev/null
$ aa-status 2>/dev/null
Seccomp filters syscalls per process. A profile expressed as
BPF (cBPF historically, eBPF for seccomp_bpf) decides allow,
errno, kill, or trace per syscall. Container runtimes ship default
profiles; docker inspect shows the seccomp profile applied.
$ cat /proc/PID/status | grep Seccomp # 0=disabled 1=strict 2=filter
$ scmp_sys_resolver bind # syscall name lookup
eBPF lets the kernel run sandboxed userspace-supplied programs at attach points (XDP, tc, kprobe, uprobe, tracepoint, LSM). Modern observability and runtime security (Falco, Tracee, Cilium, Tetragon, Aya-based agents) all anchor here.
$ sudo bpftool prog show
$ sudo bpftool map show
$ sudo bpftool perf show
Lockdown (since 5.4) gates kernel introspection and modification when the system was booted with Secure Boot. Three states:
$ cat /sys/kernel/security/lockdown
# none / integrity / confidentiality
In confidentiality mode, /dev/mem, /dev/kmem,
unsigned modules, kexec, hibernation, and most introspection
surfaces are restricted; many kernel-debugging tradecraft moves
fail.
IMA / EVM: Integrity Measurement Architecture and Extended
Verification Module hash and (optionally) sign files at access; used
in regulated environments to detect file tampering. /sys/kernel/
security/ima/ holds the policy and runtime measurements.
Yama: a tiny LSM that gates ptrace to children only by
default. /proc/sys/kernel/yama/ptrace_scope.
Common Tasks#
The most common kernel-related questions an operator answers under time pressure on an unfamiliar host. Each starts with what’s running, not what’s documented.
Pin down the kernel for CVE applicability and exploit selection.
$ uname -srm
$ cat /proc/version
$ cat /proc/cmdline
$ ls -la /boot/vmlinuz-*
Inventory the security posture to know which moves are off the table.
$ cat /sys/kernel/security/lsm
$ getenforce 2>/dev/null
$ aa-status 2>/dev/null
$ cat /sys/kernel/security/lockdown
$ cat /proc/sys/kernel/kptr_restrict
$ cat /proc/sys/kernel/dmesg_restrict
$ cat /proc/sys/kernel/yama/ptrace_scope
$ cat /proc/sys/kernel/unprivileged_bpf_disabled
Audit loaded modules for unsigned modules, oddly-named modules, or known rootkit artifacts.
$ lsmod
$ awk '{print $1}' /proc/modules | sort > /tmp/proc_modules
$ ls /sys/module/ | sort > /tmp/sys_module
$ diff /tmp/proc_modules /tmp/sys_module # divergence is suspicious
$ for m in $(awk '{print $1}' /proc/modules); do modinfo "$m" 2>/dev/null | head -3; done
Check namespace and capability surface of every running process,
which is how you spot containers, sandboxes, and privileged services
without ps cooperating.
$ for pid in $(ls /proc | grep -E '^[0-9]+$'); do
ns=$(readlink /proc/$pid/ns/pid 2>/dev/null)
echo "$pid $ns"
done | sort -k2 -u
$ for pid in $(ls /proc | grep -E '^[0-9]+$'); do
grep -H ^CapEff /proc/$pid/status 2>/dev/null
done | grep -v 0000000000000000 # processes with non-empty effective caps
Read the live ring buffer for boot-time messages, hardware events, OOM kills, and security denials.
$ dmesg -T --level=err,warn
$ dmesg -T | grep -iE 'oom|killed|denied|tainted|secure boot'
$ journalctl -k -p err --since today
Tune a sysctl now and persist it if the engagement allows configuration changes.
$ sudo sysctl -w net.ipv4.ip_forward=1
$ sudo install -m 0644 /dev/stdin /etc/sysctl.d/99-fwd.conf <<'EOF'
net.ipv4.ip_forward=1
EOF
$ sudo sysctl --system
Trace what a process is asking the kernel for when its behavior is opaque from the outside.
$ sudo strace -f -e openat,connect,execve -p PID
$ sudo perf trace -p PID
$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve / pid == TARGET / { printf("%s\n", str(args->filename)); }'
Read kernel logs for tamper indicators during an investigation.
$ sudo journalctl -k --since '24 hours ago' | grep -iE 'taint|secure|module|bpf'
$ sudo dmesg -T | grep -iE 'oom|panic|oops|stack'
$ sudo ausearch -m AVC --start today # SELinux denials
References#
kernel.org: the Linux kernel project homepage; releases, mailing list archives, the news.
Kernel Newbies: per-version changelogs in human-readable form (LinuxChanges).
Linux Documentation: the kernel’s own docs, including admin guide, driver model, ftrace, eBPF, tracing, security, and the sysctl reference.
LWN.net: kernel-development reporting and analysis; the standard secondary source.
Boot: the path that ends with the kernel running.
Hardening: kernel hardening from the operator’s seat.
Devices:
/dev,udev, the device model.eBPF: the eBPF subsystem in depth.
man 7 capabilities,man 7 namespaces,man 7 cgroups,man 7 sysctl.conf,man 8 modprobe,man 5 modprobe.d.