2026-07-20
Before 2018, every process's page tables mapped the entire kernel into the upper half of its virtual address space. The pages were marked supervisor-only, so ring 3 couldn't touch them — but they were present. Meltdown showed that speculative execution could bypass the supervisor check and leak kernel memory through cache side channels. The fix was structural, not microcode: unmap the kernel entirely when running in user mode.
The two page tables. Under KPTI (called PTI in Linux), each process now has two sets of page tables:
The cost. Every syscall now does: swap CR3 to the kernel table on entry, swap it back on exit. Writing CR3 is expensive — it historically flushes the TLB. PCID (Process-Context Identifiers) saves us here: Linux assigns two PCIDs per process (one for user, one for kernel), so the flush is avoided, but the CR3 write itself still costs 200–500 cycles on the entry and again on the exit.
Concrete example. Before KPTI, a getpid() syscall on a Skylake box took about 65 ns. After KPTI with PCID enabled, it jumped to roughly 125 ns. Without PCID (older CPUs), it went to 400+ ns because every syscall flushed the TLB. This is why the FSGSBASE, vDSO, and io_uring topics keep coming up — they're all attempts to avoid the syscall boundary now that it's twice as expensive.
Rule of thumb. KPTI adds ~60 ns per syscall round trip on PCID-capable hardware. If your workload does 100k syscalls/sec per core, that's 6 ms/sec of pure overhead — 0.6% of a core, quietly. At 1M syscalls/sec you're burning 6% before you compute anything.
Turning it off. nopti on the kernel command line disables it. You'll see this on air-gapped machines, sealed embedded systems, or CPUs immune to Meltdown (AMD Zen, Intel post-Ice Lake with the fix in silicon). Check /sys/devices/system/cpu/vulnerabilities/meltdown: "Not affected" means KPTI is off by default because the hardware doesn't need it.
The trampoline detail. The user-visible kernel stub is called the entry trampoline, mapped at a fixed address in every process. It switches to the per-CPU kernel stack, writes CR3, then jumps into the real syscall handler. Look for entry_SYSCALL_64_trampoline in the kernel source — it's under 100 instructions and is the only kernel code an unprivileged user process can even see the address of.
