This isn’t popping someone else’s proof-of-concept. It’s the quiet, stubborn work of figuring out what really happens when a syscall copies the wrong size from userspace, or a netlink handler forgets to check privileges. Linux kernel exploit development is precision, patience, and a kind of bloody-mindedness you don’t pick up from bug bounty reports. We’ll walk the real workflow—from mapping the attack surface to stabilizing a use-after-free against modern mitigations—with zero marketing fluff.

Why the Kernel Is a Different Beast
Userland exploitation is a playground with boundaries you can see. You’ve got your stack, your heap, your libc. The kernel is a shared, concurrent mess where one bad write panics the box and your target object gets freed under you by a workqueue. You’re not just dodging ASLR and NX. You’re staring down SMAP, SMEP, KPTI, and a growing list of structure-specific hardening tricks. The question stops being “how do I hijack execution” and starts being “how do I massage slab state precisely enough to survive until I win.”
I keep a build environment intentionally behind the latest stable—say, a 5.15 LTS with a known vulnerable driver compiled in. This isn’t about chasing 0-days. It’s about mastering the techniques on a target where you can afford to reboot a thousand times without anyone yelling at you. The workflow always kicks off the same way: static analysis of a driver that handles user-controlled data, usually through ioctl, write, or setsockopt.
Choosing Your First Target
Modern kernels have an absurd attack surface, but not all of it is reachable from an unprivileged namespace. When I’m teaching myself something new, I drift toward out-of-tree drivers or half-forgotten subsystems—think hamradio or android binder backports. The goal is a code path where a length field gets no real check against the destination buffer, or a reference count drops without proper locking. Tools like Syzkaller are fine for fuzzing, but for deliberate exploit development you need to read the code yourself and build a mental model of every allocation and free path.

Heap Grooming and the Slab Allocator
Userland heap exploits often orbit tcache or fastbins. The kernel SLUB allocator is a whole different animal. You’re dealing with dedicated caches for object sizes like kmalloc-192, kmalloc-1024, and structure-specific caches like files_cache. A use-after-free or double-free means you have to reclaim that exact slab slot with a controlled object before the dangling pointer gets dereferenced.
This is where heap spraying shows up, but not the kind you might remember from browser exploits. You can’t just spray ArrayBuffers. Instead, you lean on syscalls that allocate kernel objects of a predictable size: add_key for keyrings, msgget for System V messages, or setsockopt for network-related buffers. The trick is finding an allocation path that lets you control the first few bytes of the object—those bytes often hold function pointers or structural fields like ops vectors or cred pointers.
Crafting a Stable Use-After-Free
Say you’ve found a bug in a driver’s release function: it frees a structure but leaves a file descriptor’s private data pointer dangling. The race window might be tight, so you trigger the free and immediately allocate a new object of the same size. I often use a dedicated thread spinning on userfaultfd or FUSE to pause a copy_from_user midway, stretching the race window artificially. Quiet technique. Doesn’t rely on lucky timing—you choose exactly when the kernel resumes.
Once you’ve reclaimed the slot with a fake object, you need to live through the next few instructions until you can trigger a privilege escalation. That means your fake object’s fields have to satisfy any sanity checks the vulnerable code performs. If you’re overwriting a struct file_operations, you might set the release pointer to a gadget that pivots the stack to a controlled location. But with SMAP, that location can’t be in userspace anymore.
Bypassing Modern Mitigations
SMEP and SMAP stop the kernel from executing or accessing userspace memory directly. KPTI isolates kernel page tables from userspace, so even a leaked kernel address doesn’t hand you a direct map. This forces stack pivoting into the kernel heap or chaining gadgets entirely within kernel space. The ROP chain has to be built from the kernel image itself, which means you need an information leak to beat KASLR.
Information leaks often come from the same bug class you’re exploiting. A heap out-of-bounds read in a syscall might let you leak a nearby object’s slab freelist pointer, which points to another kernel address. From there you can calculate the kernel image base. I’ve also used /proc/kallsyms on older setups, but production systems usually lock that down. Instead, side-channel techniques like prefetch timing or using uninitialized memory in copy_to_user are more practical.
Real-World Example: CVE-2022-1786
A while back I spent time with CVE-2022-1786, a use-after-free in the io_uring subsystem. The bug was in handling IORING_OP_TEE where a pipe buffer could be freed while still referenced. The exploit involved registering a fixed buffer with io_uring_register, triggering the free, and then spraying struct pipe_buffer objects to reclaim the memory. The twist? Modern kernels had randomized slab freelists, so I needed a secondary info leak from an uninitialized io_uring_cqe to locate the reclaimed object. The final payload overwrote the pipe buffer’s ops->release pointer with a gadget that called commit_creds(prepare_kernel_cred(0)).
That gadget, by the way, is a classic. You find it by scanning the kernel’s .text for a call to prepare_kernel_cred followed by a call to commit_creds, usually in run_umount or __sys_setuid code paths. The annoying part is setting up the registers so the result from prepare_kernel_cred (a pointer to a new cred structure) lands as the first argument to commit_creds. Usually that demands a register pivot gadget first.

Tools of the Trade
You can’t do this with just a text editor. My toolkit is minimal but deliberate: a custom QEMU VM with a debug kernel and GDB attached via kgdboc. I use a small Python script that parses System.map and spits out offsets for common structures, and a C program that opens the vulnerable device and triggers the bug with precise timing. For heap visualization, I hacked together a script that parses slabinfo before and after each spray step, so I can see exactly which caches are active.
One undervalued trick is using ftrace to trace the exact function calls leading to the bug. Enable event tracing for kmalloc and kfree on the suspect slab, and you can reconstruct the timeline of allocations and frees from user space. That turns a blind spray into a targeted reclaim, because you know precisely when the vulnerable object gets freed.
Stabilizing the Exploit
A kernel exploit that works once in ten tries is a denial-of-service tool, not a reliable exploit. Stabilization means handling the kernel’s inherent concurrency. You need to account for interrupts, preemption, and other threads that might allocate from the same slab. One approach: set CPU affinity for your exploit process with sched_setaffinity, pinning it to a single core while you do the critical operations. Another: flood the slab with placeholder objects beforehand, so the vulnerable slot is less likely to get snatched by an unrelated allocation.
After the privilege escalation, you need a clean exit. You can’t just call execve("/bin/sh") from kernel context directly, so you usually return to userspace with the new credentials. That means saving the original register state before the ROP chain and restoring it after commit_creds. Mess up the stack frame and you’ll kernel panic right at the finish line—a lesson I’ve learned more times than I’d like to admit.
Defensive Implications
Understanding this craft isn’t just about offense. After spending weeks massaging the slab, you start to see why seemingly innocent code patterns are dangerous. A kfree followed by a goto without clearing the pointer, a copy_from_user without a bounds check on a structure length—these aren’t just bugs. They’re invitations. The hardening features we bypass exist because researchers demonstrated the attack techniques first. Every __randomize_layout annotation in a kernel structure was added because someone, somewhere, managed to overwrite that exact field.
If you’re on the defensive side, pay attention to the slab caches your code uses. Is your structure mixed into a generic cache, or does it have its own dedicated one? A dedicated cache makes heap separation easier for attackers; a generic one forces them to contend with noise. Neither is a silver bullet, but the choice changes how hard a spray-based exploit has to work.
Building Your Own Lab
Start with a kernel that got patched for a known vulnerability, then revert that patch in your local tree. Build it with debug symbols and minimal hardening: nosmep, nosmap, nokaslr on the kernel command line for early stages, then gradually re-enable them as your techniques improve. Write your own vulnerable kernel module—a simple character device that does a bad kfree—and exploit it from first principles. No copy-pasting from exploit-db. The goal is to internalize the state machine of the allocator and the dance of the stack frame.
This path is slow. You’ll read more mm/slub.c than you ever wanted. You’ll stare at register dumps wondering why RAX is zero when it should hold a pointer. But when you finally pop a root shell from a kernel you built and hardened yourself, the quiet satisfaction isn’t about the shell. It’s about knowing exactly why every byte is where it is.
FAQ
- Do I need to know assembly for kernel exploit development? Yes, specifically x86_64 assembly. You’ll need to read disassembly in GDB, understand calling conventions, and manually construct ROP chains. ARM64 is also useful if you’re targeting Android or embedded systems.
- What’s the best way to practice without breaking the law? Build your own vulnerable kernel modules or use deliberately vulnerable virtual machines like those from the pwn.college program. Always work on systems you own or have explicit permission to test.
- How do I handle kernel panics during development? Use a virtual machine with a serial console and configure
kexecorpanic_on_oopsto automatically reboot. Log everything to a host file viavirsh console. Expect hundreds of panics; that’s normal. - Are there any good books on this topic? There are no definitive books, but reading the kernel source itself and studying write-ups from Google Project Zero or the Linux Kernel Exploitation blog posts by various researchers is the most current way to learn. The techniques change faster than any publisher can keep up.