R0KernelPath
module 031 / 262

Privilege Levels, Protection Rings & CPU Modes

The CPU itself decides who to trust — privilege rings are not an OS policy but a silicon guarantee, and every system call is a tightly controlled leap across that boundary.

~12h focused study11 concepts

Overview

Every program you've ever written runs in a carefully constructed illusion. Your code thinks it owns the machine — it thinks it can read any memory address, talk to any hardware device, halt the CPU whenever it wants. But the CPU itself is quietly keeping score, enforcing a set of rules your program doesn't even know exist. These rules are called privilege levels, and they are one of the most important inventions in the history of computing.

Before privilege levels existed, every program ran with the full trust of the hardware. A bug in a spreadsheet could overwrite the kernel. A malicious game could reprogram the hard drive controller. A crashing app would take the whole system down with it. Modern computing — with its billions of processes, sandboxed tabs, and multi-user servers — is only possible because CPUs gained the ability to distrust software and enforce boundaries in hardware.

This module is about that mechanism. You'll learn how modern CPUs implement a hierarchy of trust — called rings on x86, exception levels on ARM — and exactly what happens at the hardware level when your program asks the kernel to do something on its behalf. You'll understand why a system call is not just a function call with a different name, but a carefully controlled leap across a trust boundary, mediated by the CPU itself. You'll see why "kernel mode" and "user mode" are not software constructs but actual CPU states stored in silicon.

By the end, the phrase "ring 0" will mean something precise and physical to you, and when you read Linux kernel code you'll understand why it works the way it does — not just how.

why this matters for the kernel

Privilege levels are the bedrock on which all kernel contribution rests. Every piece of code in the Linux kernel runs in Ring 0 (x86) or EL1 (ARM), meaning a single null pointer dereference, a single off-by-one in a driver, can corrupt the entire system — there is no safety net below the kernel. When you write or review kernel code, you must carry a mental model of privilege at all times: which ring is this code running in? What can it touch? What happens if it faults?

Beyond defensive awareness, understanding the ring transition is essential for anything involving system calls, virtual machines, eBPF, seccomp filters, kernel modules, and device drivers. A kernel contributor who does not deeply understand what happens during a syscall instruction — how the CPU switches stacks, saves context, validates the call number, and dispatches — cannot reason about performance, security, or correctness at the boundaries that matter most. This is not background knowledge; it is load-bearing infrastructure for everything that follows.

interactive visualization
Ring 0
Ring 3 — User apps

Your browser, editor, games. No direct hardware access; must ask the kernel via syscalls.

Linux only uses Ring 0 (kernel) and Ring 3 (user). The rings are the CPU's hardware-enforced privilege walls.

x86-64 System Call: Ring 3 to Ring 0 and Back

rendering diagram…

The full lifecycle of a Linux system call on x86-64, showing every CPU-level step during the Ring 3 to Ring 0 transition via SYSCALL and the return via SYSRET. Each 'Note over CPU' step is enforced entirely in hardware.

The concepts

01The Problem Privilege Solves: Why CPUs Cannot Trust Software

Imagine a multi-tenant apartment building where every door uses the same master key. Any tenant can walk into any room, rearrange the furniture, or cut the power. This was the state of early personal computers: every program ran with full access to every CPU instruction and every memory address.

The consequences were severe. On early MS-DOS systems, a buggy program could accidentally write to the wrong memory address and corrupt the OS. A program could execute the HLT (halt) instruction and freeze the machine. Any program could directly program hardware by writing to I/O ports, meaning two programs trying to talk to the same device at the same time would produce garbage.

The insight that fixed this: hardware should have opinions about what software is allowed to do. Not all code deserves equal trust. The operating system, which is carefully written and reviewed, needs to configure hardware, manage memory, and respond to interrupts. User programs — arbitrary, potentially hostile — should only be allowed to do what the OS explicitly permits.

This separation of trust is the entire motivation for privilege levels. The CPU itself, not software, enforces these boundaries. A user program literally cannot execute certain instructions — not because the OS blocks them after the fact, but because the CPU will refuse and raise an exception before the instruction completes.

02x86 Protection Rings: Ring 0 Through Ring 3

The x86 architecture (from the 80286 onward) formalizes privilege into four concentric rings, numbered 0 through 3. Ring 0 is the innermost ring — the most trusted, the most powerful. Ring 3 is the outermost ring — the least trusted, the most restricted. Think of it like a medieval castle: the king (Ring 0) lives in the keep at the center and can go anywhere; peasants (Ring 3) must stay outside the walls.

The rings differ along two axes:

Instruction access. Certain CPU instructions are privileged — they can only execute in Ring 0. The most important examples on x86:

  • HLT — halts the CPU (only the OS should be able to do this)
  • LGDT / LIDT — loads the Global Descriptor Table and Interrupt Descriptor Table registers, which control how memory segments and interrupts are mapped
  • MOV CRn — moves data to/from control registers (CR0 controls paging on/off, CR3 holds the page table root)
  • IN / OUT — reads/writes hardware I/O ports (with IOPL-based nuance)
  • WRMSR — writes Model Specific Registers (controls CPU features, power, etc.)

If Ring 3 code tries to execute any of these, the CPU immediately raises a General Protection Fault (#GP, interrupt vector 13), handing control to the kernel's fault handler — not to the user program.

Memory access. Each page table entry has a U/S bit (User/Supervisor). Pages marked Supervisor can only be accessed by Ring 0 code. Ring 3 code trying to access a supervisor page gets a Page Fault (#PF, vector 14). This is what prevents any user process from reading kernel memory.

In practice, mainstream operating systems use only two of the four rings:

  • Ring 0: the kernel, device drivers (monolithic kernels like Linux), the hypervisor on bare metal
  • Ring 3: all user-space programs

Rings 1 and 2 were intended for OS services and device drivers at intermediate trust levels, but this model never caught on. Hardware virtualization (VMX/SVM) effectively created a new "Ring -1" for hypervisors, and Ring 1/2 became vestigial.

/* In kernel code (Ring 0), this is legal: */
static inline void write_cr3(unsigned long val)
{
    asm volatile("mov %0, %%cr3" : : "r"(val) : "memory");
}

/* In user code (Ring 3), this would cause #GP: */
/* GCC inline asm: mov rax, cr3  -> SIGILL / SIGSEGV */

03The Current Privilege Level (CPL) and How It's Stored

At any given clock cycle, the CPU needs to know what privilege level it is currently operating at. On x86-64, this information lives in the bottom two bits of the CS (Code Segment) register, called the CPL (Current Privilege Level).

The CS register holds a segment selector — a 16-bit value that indexes into the Global Descriptor Table (GDT). Bits 1:0 of the selector are the RPL (Requested Privilege Level), and when a selector is loaded into CS, the CPL is taken from those bits.

In 64-bit long mode (the mode modern x86-64 systems run in), segmentation is mostly flat and vestigial — the hardware still checks CPL from CS bits 1:0, but the GDT entries for code and data don't do meaningful address translation. The ring concept is fully preserved, but through page table bits rather than segment limits.

You can observe this from user space:

$ cat /proc/self/maps

The kernel pages mapped at the top of the address space (before KPTI/Meltdown patches) had the supervisor bit set. Even though they appeared in the process's address space, any attempt to access them from Ring 3 would fault.

An important nuance: the CPL is checked by the CPU on every memory access and on every privileged instruction execution — there is no way to "disable" this checking from software. It is a purely hardware mechanism.

/* Read the CS register to see your CPL (bottom 2 bits) */
#include <stdio.h>
int main(void) {
    unsigned short cs;
    asm("mov %%cs, %0" : "=r"(cs));
    printf("CS = 0x%04x, CPL = %u\n", cs, cs & 3);
    /* In user space you'll see CPL=3 (e.g., CS=0x0033) */
    return 0;
}
/* Compile: gcc -o showcs showcs.c && ./showcs */

04Mode Switches: How the CPU Transitions Between Rings

Rings are only useful if crossing between them is tightly controlled. The CPU provides exactly one sanctioned path from Ring 3 to Ring 0: a controlled gate. There is no instruction a user program can execute that says "now I am in Ring 0" — the CPU would refuse. Instead, ring transitions happen through specific, hardware-enforced mechanisms.

Ring 3 → Ring 0 (privilege escalation): The primary mechanism in 64-bit x86 is the SYSCALL instruction (there is also INT 0x80 on 32-bit, and older SYSENTER). When SYSCALL executes:

  1. The CPU reads the target kernel entry address from the LSTAR MSR (set by the kernel at boot)
  2. The CPU atomically switches CS to the kernel code selector (Ring 0 CPL)
  3. The CPU saves the user-space return address and RFLAGS, switches to the kernel stack (stored in the TSS — Task State Segment)
  4. Jumps to the kernel's syscall entry point

At no point does the CPU execute any user-supplied code in Ring 0. The destination of the jump is a kernel address loaded from a register only Ring 0 code could have written.

Ring 0 → Ring 3 (privilege drop): The kernel returns to user space via SYSRET (or IRET for interrupt returns). These instructions:

  1. Restore the user-space CS (Ring 3 CPL)
  2. Restore RFLAGS and the return address
  3. Switch back to the user-space stack

Exceptions and Interrupts: When a hardware interrupt fires or a fault occurs, the CPU automatically transitions to Ring 0 (using the Interrupt Descriptor Table, IDT), runs the kernel handler, and uses IRET to return. This is the same Ring 3 → 0 escalation, just triggered by hardware rather than software.

The key insight: privilege escalation always goes to a kernel-chosen address, never a user-chosen address. The user can choose which system call to make (via the system call number in RAX), but the entry point itself is fixed by the kernel.

/* The Linux x86-64 syscall convention:
   RAX = syscall number
   RDI, RSI, RDX, R10, R8, R9 = arguments
   SYSCALL instruction transitions to Ring 0 */

/* A raw write(1, "hello\n", 6) with no libc */
.section .text
.global _start
_start:
    mov  $1,  %rax      /* syscall number: write */
    mov  $1,  %rdi      /* fd: stdout */
    lea  msg(%rip), %rsi /* buf */
    mov  $6,  %rdx      /* count */
    syscall             /* -> Ring 0 kernel entry */
    /* CPU is now back in Ring 3 after SYSRET */
    mov  $60, %rax      /* syscall: exit */
    xor  %rdi, %rdi
    syscall
msg: .ascii "hello\n"

05The Kernel Stack Switch and TSS

Here's a subtle but critical detail: when the CPU transitions to Ring 0, it must use a different stack from the user program. Why? Because if the CPU continued using the user-space stack pointer (RSP), Ring 0 kernel code would be pushing return addresses and local variables onto a buffer that the user program can read and modify — a devastating security hole.

The x86 CPU solves this with the Task State Segment (TSS), a special kernel-managed data structure. Among other things, the TSS contains RSP0 — the stack pointer to use when entering Ring 0 from Ring 3. The kernel sets this up per-thread when scheduling.

The sequence during a SYSCALL:

  1. CPU reads RSP0 from the TSS and switches RSP to that value
  2. The old user RSP is saved into RCX (for SYSRET to restore) — note that SYSCALL does NOT automatically push user RSP onto the new stack; the kernel's entry stub does this
  3. RFLAGS is saved in R11
  4. Kernel entry code (entry_SYSCALL_64 in Linux) then manually saves all general-purpose registers onto the kernel stack (building a pt_regs struct)

This stack switch is one of the reasons system calls are more expensive than regular function calls — even beyond the privilege check, you have a stack switch, full register save, and TLB effects on some architectures.

You can observe the kernel stack in /proc/<pid>/status — look at VmStk — but the kernel stack itself is not in this region; it lives in the kernel address space, invisible to user tools except through kernel debugging interfaces.

/* Simplified view of what Linux's entry_SYSCALL_64 does
   (arch/x86/entry/entry_64.S) — pseudocode */

entry_SYSCALL_64:
    /* CPU already switched RSP to TSS.RSP0 */
    swapgs                  /* swap GS base: user GS <-> kernel GS */
    mov  %rsp, %gs:cpu_tss_rw+TSS_sp2  /* save user RSP */
    /* ... push all registers to build struct pt_regs ... */
    call do_syscall_64      /* C function: dispatch on RAX */
    /* ... restore registers ... */
    swapgs
    sysretq                 /* back to Ring 3 */

06Memory Protection: The U/S Bit and Kernel Address Space

Privilege rings control which instructions can execute. But isolation also requires controlling which memory each privilege level can read and write. This is done through the page table hierarchy.

In x86-64, each page table entry (at every level: PML4, PDPT, PD, PT) has a User/Supervisor (U/S) bit:

  • U/S = 1: the page is accessible from both Ring 3 and Ring 0
  • U/S = 0: the page is only accessible from Ring 0 (Supervisor)

All kernel code and data — the kernel text segment, kernel heap (slab allocator), per-CPU data, interrupt stacks — are mapped with U/S = 0. Every process maps the kernel in the top portion of its virtual address space (above 0xffff800000000000 on x86-64), but those mappings are supervisor-only. Ring 3 code that tries to dereference a kernel pointer gets a Page Fault (#PF).

Meltdown and KPTI: The Meltdown vulnerability (CVE-2017-5754) revealed that even though user-space cannot architecturally access supervisor pages, the CPU's speculative execution engine would sometimes speculatively read them before the permission check completed — leaking data through cache side-channels. The fix, KPTI (Kernel Page Table Isolation), makes the kernel invisible even from the page tables: in user mode, only a tiny stub of kernel code is mapped (enough to handle the syscall entry), and the full kernel mappings are only present in a separate page table that's swapped in on entry to Ring 0. This comes at a measurable performance cost (extra CR3 writes = TLB flushes on every syscall).

You can check if your kernel has KPTI enabled:

grep 'pti' /proc/cpuinfo   # 'pti' flag present = KPTI active
dmesg | grep -i kpti
/* The kernel maps itself at a fixed virtual address.
   On x86-64 Linux, kernel virtual addresses start at:
   0xffffffff80000000 (kernel text)
   0xffff888000000000 (direct physical map)

   A user process cannot access these — #PF if tried.
   The following in user space will SIGSEGV: */
#include <stdint.h>
int main(void) {
    volatile uint8_t *kaddr = (volatile uint8_t *)0xffffffff80000000UL;
    return *kaddr;  /* Page Fault -> SIGSEGV */
}

07ARM Exception Levels (EL0–EL3): A Cleaner Generalization

ARM's AArch64 architecture (used in every modern phone, Apple Silicon, and many servers) takes the same concept and redesigns it from scratch with a cleaner naming scheme. Instead of "rings," ARM uses Exception Levels (EL), numbered 0 through 3. Higher numbers mean higher privilege.

  • EL0 — user space. Applications, sandboxed code. Equivalent to x86 Ring 3.
  • EL1 — the OS kernel. Linux kernel code runs here. Equivalent to x86 Ring 0.
  • EL2 — the hypervisor. When running a VM, the VMM (e.g., KVM) runs at EL2 and can control EL1 guests. There is no direct x86 ring equivalent (it's closer to VMX root mode / "Ring -1" informally).
  • EL3 — secure monitor / firmware (ARM TrustZone). Manages the boundary between a Normal World (EL0/1/2) and a Secure World (TEE). Has no x86 equivalent in the architecture (though Intel TXT/SGX are vaguely analogous).

Exception Levels and SPSR: The ARM CPU saves the current EL in the SPSR_ELn (Saved Program Status Register) when an exception occurs. On entry to EL1 from EL0, the CPU stores the user-mode status (including the EL) in SPSR_EL1 and jumps to the EL1 exception vector. On ERET, it restores SPSR and returns to the saved EL.

System calls on ARM: The SVC instruction (Supervisor Call) is ARM's equivalent of SYSCALL. It raises a synchronous exception, always transitioning to EL1 (the kernel). The kernel examines the SVC immediate operand or register arguments to determine the system call number.

ARM's model is cleaner than x86's in a key way: the transition rules between ELs are rigidly defined by the ISA. EL3 can set SCR_EL3.HCE to enable/disable EL2. EL2 can set HCR_EL2.TGE to route EL0 exceptions to EL2. This is a properly layered security architecture.

/* ARM AArch64 system call: write(1, "hi\n", 3) */
.section .text
.global _start
_start:
    mov  x8, #64        /* syscall number: write (ARM64) */
    mov  x0, #1         /* fd: stdout */
    adr  x1, msg        /* buf */
    mov  x2, #3         /* count */
    svc  #0             /* -> exception to EL1 kernel */
    /* returns here after ERET from EL1 */
    mov  x8, #93        /* syscall: exit */
    mov  x0, #0
    svc  #0
msg: .ascii "hi\n"

08RISC-V Privilege Modes: Machine, Supervisor, User

RISC-V, the open-source ISA gaining traction in embedded systems and increasingly in servers, uses a different but conceptually similar three-level privilege hierarchy:

  • U-mode (User mode) — user applications. Least privileged. Equivalent to Ring 3 / EL0.
  • S-mode (Supervisor mode) — the OS kernel. Linux runs here in the RISC-V port. Equivalent to Ring 0 / EL1.
  • M-mode (Machine mode) — firmware / bootloader (e.g., OpenSBI). Has direct access to all hardware, including configuration of which traps are delegated to S-mode. Equivalent to EL3 (roughly).

On RISC-V, the current privilege level is stored in the mstatus CSR (Control and Status Register), specifically the MPP field (for M-mode) and SPP field (for S-mode), recording what mode to return to on MRET/SRET.

System calls on RISC-V: The ECALL instruction (Environment Call) triggers an exception upward. From U-mode, ECALL jumps to the S-mode trap handler (the Linux kernel's handle_exception). From S-mode, ECALL goes to M-mode (the firmware/SBI). The system call number is passed in a7.

RISC-V's approach is elegant: the ISA defines Privileged Specifications separately from the User ISA, and the trap delegation mechanism (medeleg, mideleg CSRs) lets M-mode hand off specific exception types to S-mode, so the kernel doesn't need to involve firmware for every system call.

/* RISC-V U-mode system call: exit(0)
   a7 = syscall number, a0-a5 = arguments */
.section .text
.global _start
_start:
    li   a7, 93         /* __NR_exit = 93 on Linux/RISC-V */
    li   a0, 0          /* exit code */
    ecall               /* -> trap to S-mode kernel */
    /* never reached */

09The System Call Boundary in the Linux Kernel

A system call is the only legitimate way for a user-space program to request kernel services. Understanding what happens at this boundary — at the kernel source level — makes Linux kernel code readable.

On x86-64, the entry point is entry_SYSCALL_64 in arch/x86/entry/entry_64.S. Here is the path a write(2) call takes from glibc to return:

  1. glibc wrapper sets RAX=1 (SYS_write), RDI=fd, RSI=buf, RDX=count, executes SYSCALL
  2. CPU switches to Ring 0, RSP to kernel stack (from TSS.RSP0), saves RIP/RFLAGS in RCX/R11
  3. entry_SYSCALL_64 calls swapgs (switches GS to point at per-CPU kernel data), saves registers as struct pt_regs on the kernel stack
  4. do_syscall_64() (in arch/x86/entry/common.c) indexes into sys_call_table[] using RAX, calls sys_write()
  5. sys_write → ksys_write() → vfs_write() → file-specific write operation (e.g., ext4_file_write_iter())
  6. Return value placed in pt_regs->ax, registers restored, SYSRET issued
  7. CPU switches CS back to Ring 3 CPL, restores RSP/RIP/RFLAGS from saved values
  8. glibc checks for error (negative return = errno), returns to caller

You can trace this in real-time with strace, which uses ptrace() to intercept system calls. You can also see system call counts with perf stat -e syscalls:sys_enter_write ./yourprogram.

A crucial Linux detail: system calls can return -EINTR if interrupted by a signal, and the kernel may restart them (SA_RESTART). This is entirely handled in the Ring 0 return path — another reason the boundary is complex.

/* You can see the syscall table entry in kernel source:
   arch/x86/entry/syscall_64.c (generated from syscall_64.tbl)

   SYSCALL_DEFINE3(write, unsigned int, fd,
                   const char __user *, buf, size_t, count)
   {
       return ksys_write(fd, buf, count);
   }

   The __user annotation is crucial: it marks pointers
   that come from Ring 3 and must be validated before
   the kernel dereferences them (copy_from_user / get_user). */

/* Observe system calls from user space: */
/* $ strace -e write echo hello */
/* write(1, "hello\n", 6) = 6       */

10Hardware Virtualization: Ring -1 and the Hypervisor Layer

When you run a virtual machine, you have an operating system (the guest) that believes it is in Ring 0. But it is actually running inside another OS (the host). How does this work — if the guest kernel tries to execute a privileged instruction, what happens?

Early virtualization (before hardware support) used binary translation: a VMM would scan guest code before execution, find privileged instructions, and replace them with traps into the VMM. This was slow and complex.

Modern CPUs added hardware virtualization extensions that create a new privilege level logically below Ring 0:

  • Intel VT-x introduces VMX (Virtual Machine Extensions). The hypervisor runs in VMX root mode (full Ring 0 + VMX privileges). Guest OSes run in VMX non-root mode (their Ring 0 is hardware-constrained). Privileged instructions in non-root mode cause VM exits — traps to the VMX root hypervisor — instead of executing.
  • AMD SVM (Secure Virtual Machine) is the AMD equivalent; guests run in Guest Mode.
  • ARM EL2 natively supports this: guest kernels run at EL1, the hypervisor at EL2 traps on EL1 attempts to access hardware resources.

KVM (Kernel-based Virtual Machine) in Linux uses VT-x/SVM to implement Type-1.5 virtualization. The Linux kernel at EL1/Ring 0 acts as a hypervisor for guest VMs, with the KVM module managing VM exits and VMCS (Virtual Machine Control Structure) configuration.

A VM exit is conceptually similar to a system call: the guest OS is performing an operation that requires the "OS above it" (the hypervisor) to handle, just as a user process asks the OS. This creates a beautiful recursive structure — rings within rings.

/* Check if your CPU supports VT-x from user space: */
/* $ grep -m1 vmx /proc/cpuinfo   # Intel VT-x */
/* $ grep -m1 svm /proc/cpuinfo   # AMD SVM    */

/* Check if KVM is loaded: */
/* $ lsmod | grep kvm */
/* kvm_intel   or  kvm_amd  should appear */

/* The VMCS (Virtual Machine Control Structure) is a
   per-VM data structure the CPU uses to save/restore
   guest state on VM exits. The hypervisor sets VMCS
   fields to control which guest operations cause exits. */

11Security Implications: Privilege Escalation Vulnerabilities

Understanding privilege levels is inseparable from understanding the class of bugs that violate them — privilege escalation vulnerabilities. These are bugs where Ring 3 code ends up executing as Ring 0, or where one process gains the memory access of another.

Kernel exploits (local privilege escalation): A bug in a kernel system call, ioctl handler, or driver that can be triggered from Ring 3 to achieve unintended Ring 0 behavior. Classic patterns:

  • Stack overflow in kernel: overwrite a return address on the kernel stack to redirect execution to attacker-controlled code (now mitigated by kernel stack canaries, SMEP/SMAP)
  • Use-after-free: a freed kernel object is reused with controlled content, leading to controlled function pointer calls
  • Type confusion: a kernel structure is cast to the wrong type, allowing field writes to corrupt sensitive kernel data

SMEP and SMAP are hardware defenses:

  • SMEP (Supervisor Mode Execution Prevention): Ring 0 cannot execute pages marked User (U/S=1). Prevents the classic "ret2usr" technique where attacker shellcode sits in user memory and the kernel is redirected to execute it.
  • SMAP (Supervisor Mode Access Prevention): Ring 0 cannot read or write user pages unless it explicitly uses stac/clac instructions (which copy_from_user does). Prevents the kernel from being tricked into treating user pointers as trusted kernel pointers.

These are set in CR4 (bit 20 = SMEP, bit 21 = SMAP). The Linux kernel enables both at boot if the CPU supports them.

Studying these attacks is one of the best ways to deeply understand what privilege levels actually protect — the attacker's job is to find the cracks in the ring boundaries.

/* Check SMEP/SMAP status: */
/* $ grep -E 'smep|smap' /proc/cpuinfo */

/* In kernel source, CR4 manipulation:
   arch/x86/kernel/cpu/common.c:

   cr4_set_bits(X86_CR4_SMEP);
   cr4_set_bits(X86_CR4_SMAP);

   These are called during CPU initialization.
   A kernel exploit that gains arbitrary write
   might try to clear these bits via CR4 before
   attempting ret2usr — modern kernels pin CR4
   via CR4 pinning (since ~5.3) to prevent this. */

Key insights

  • ✦Privilege levels are enforced by the CPU in hardware — not by the OS in software. The OS cannot grant itself extra trust; it relies on being the first code the CPU trusts.
  • ✦The Current Privilege Level (CPL) on x86 is stored in the bottom 2 bits of the CS register. It is checked on every single memory access and before every privileged instruction.
  • ✦Ring transitions are one-way gates with kernel-chosen destinations. A user program can request a system call (choosing which one), but the entry point it jumps to is fixed by the kernel at boot — the user cannot redirect it.
  • ✦Mainstream OSes only use Ring 0 (kernel) and Ring 3 (user). Rings 1 and 2 were designed but never used. Hardware virtualization effectively created Ring -1.
  • ✦ARM's Exception Levels (EL0-EL3) and RISC-V's privilege modes (U/S/M) are the same concept under different names — all exist because hardware must not trust software unconditionally.
  • ✦The Meltdown vulnerability showed that architectural privilege guarantees are not the same as microarchitectural isolation — speculative execution can leak data across ring boundaries through cache side-channels.
  • ✦The `__user` annotation in Linux kernel source is not just documentation — tools like sparse use it to enforce that kernel code does not accidentally dereference ring-3 pointers without validation.
  • ✦Every security mechanism above this layer — containers, sandboxes, VMs, seccomp filters — ultimately rests on the CPU's willingness to enforce ring boundaries. If this breaks, everything above it breaks.

Common pitfalls

  • ⚠Assuming 'kernel mode' is a software state that software can set. It is a hardware CPU state. Only the CPU changes it, only through defined mechanisms (SYSCALL/SVC/ECALL/interrupt).
  • ⚠Confusing the four defined rings with what OSes actually use. Linux and Windows use only Ring 0 and Ring 3. Rings 1 and 2 exist in x86 documentation but are absent from all modern mainstream OS designs.
  • ⚠Thinking system calls are expensive 'just because of the kernel work'. The ring transition itself — TSS stack switch, register saves, TLB effects from KPTI — is a significant fixed cost even for trivial syscalls.
  • ⚠Forgetting that ARM EL2 (hypervisor) and EL3 (secure monitor) have no direct x86 ring equivalent. The x86 equivalent of EL2 is VMX root mode, which is orthogonal to the ring numbering.
  • ⚠Assuming a kernel address in a user process's page table is accessible. The U/S bit in the page table makes it architecturally inaccessible, even though the virtual address is mapped. Meltdown was the exception that proved the rule.
  • ⚠Thinking SMEP/SMAP are software policies. They are CR4 bits that the CPU enforces in hardware on every load/store/instruction fetch, with no per-page override.

Practice

  1. 01
    easy

    Write a minimal x86-64 assembly program (no libc) that uses the raw SYSCALL instruction to call write() and then exit(). Confirm it works with strace to verify the system call numbers and argument passing. Then modify it to attempt mov rax, cr3 and observe the SIGSEGV.

  2. 02
    easy

    Write a C program that reads its own CS register using inline assembly and prints the CPL (bottom 2 bits). Then use /proc/self/maps to find the kernel-mapped region (highest address range) and try to read one byte from it — catch the SIGSEGV with a signal handler and print a message explaining why it faulted.

  3. 03
    medium

    Use strace and perf to measure system call overhead. Write a tight loop that calls getpid() one million times. Measure wall time. Then write the same loop but cache the result (no syscall). Compare times. Calculate the per-syscall overhead in nanoseconds. Bonus: rerun on a kernel with KPTI enabled vs disabled (boot with kpti=0 if your CPU is not affected by Meltdown) and observe the difference.

  4. 04
    hard

    Build and boot a minimal Linux kernel (using buildroot or just defconfig) in QEMU. Add a printk to the syscall entry path in arch/x86/entry/common.c (inside do_syscall_64, before the dispatch) that prints the syscall number. Boot your kernel and run 'ls'; observe the flood of syscall numbers in dmesg. Identify which system calls ls uses by cross-referencing with /usr/include/asm/unistd_64.h.

  5. 05
    hard

    Write a Linux kernel module that reads CR4 from within the module (Ring 0) and prints whether SMEP and SMAP are enabled. Then write a user-space program that tries to set a bit in CR4 using inline assembly, and confirm via dmesg that the kernel's General Protection Fault handler fires.

Resources

📄Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3A (System Programming Guide)
docsfree

The primary source for x86 protection rings, CPL, TSS, SMEP/SMAP, and SYSCALL/SYSRET semantics. Chapter 5 covers protection; Chapter 20 covers VMX. Dense but authoritative.

📄ARM Architecture Reference Manual for A-profile architecture (AArch64)
docsfree

The canonical ARM reference. Exception levels, SPSR, SVC instruction, and the EL2 hypervisor model are covered in the 'AArch64 System Architecture' section.

📄RISC-V Privileged Architecture Specification
docsfree

Defines U/S/M privilege modes, CSRs, ECALL, trap delegation — the cleanest privilege ISA to study because it was designed without 40 years of legacy.

📄Linux Kernel Source: arch/x86/entry/entry_64.S
docsfree

The real system call entry point. Read entry_SYSCALL_64 top-to-bottom alongside this module. Bootlin's Elixir cross-referencer makes navigation effortless.

📕Operating Systems: Three Easy Pieces (OSTEP) — Chapters on Limited Direct Execution
bookfree

Chapter 6 'Direct Execution' is the best introductory treatment of why privilege levels exist and how system calls work. Free PDF and web version.

✎LWN.net: KAISER: hiding the kernel from user space
articlefree

The original LWN article on KPTI (then called KAISER). Essential reading for understanding why Meltdown forced a fundamental change to ring-boundary memory isolation.

🔧Compiler Explorer (godbolt.org)
toolfree

Use it to compile small C snippets to x86-64 or AArch64 assembly and see exactly what instructions the compiler generates around syscall boundaries.

📕Computer Organization and Design: ARM Edition
bookpaidPatterson & Hennessy, Morgan Kaufmann. Covers ARM exception levels in the context of the full pipeline; excellent companion for the architecture fundamentals underlying privilege modes.
📕Linux Device Drivers, 3rd Edition — Chapter 1: An Introduction to Device Drivers
bookfree

Free from LWN. Though dated (2.6 kernel), the conceptual treatment of kernel vs user space, and why drivers run in Ring 0, remains accurate and readable.

✎Kernelnewbies: System Calls
articlefree

Concise overview of the Linux system call interface with pointers to relevant kernel source, good as a quick-reference alongside the deeper material.

Checkpoints — you can now…

  • ✓You can explain to someone else why a user-space program cannot execute HLT or MOV CR3, and what hardware mechanism prevents it.
  • ✓You can read the CS register in a C program and identify the CPL from its bottom 2 bits, and predict that a running user program will always show CPL=3.
  • ✓You can trace the exact sequence of CPU and kernel events that occur between a user program executing SYSCALL and the kernel's do_syscall_64 dispatching on the system call number.
  • ✓You can explain the difference between x86 Ring 0/3, ARM EL0/EL1/EL2/EL3, and RISC-V U/S/M modes, and map each layer to its equivalent OS role.
  • ✓You can explain why Meltdown required KPTI and what performance cost KPTI imposes, at the level of 'an extra CR3 write causes a TLB flush on every syscall'.
  • ✓You can identify the __user annotation in Linux kernel source and explain why a kernel function must call copy_from_user rather than directly dereferencing a user-provided pointer.
  • ✓You can articulate why SMEP and SMAP exist as hardware features and what specific attack class each one defeats.