Dirty Pipe, Copy Fail, Dirty Frag: What Linux Kernel Exploits Keep Teaching Us
A defender's reading of the kernel's recurring privilege-escalation bugs

Contents
Every year or two, a Linux kernel privilege-escalation bug gets a catchy name, a logo, and a flurry of breathless coverage. Dirty COW. Dirty Pipe. DirtyCred. The branding is silly, but the pattern underneath is deadly serious and worth studying — not so you can write exploits, but so you understand why your boxes keep being vulnerable to the same shape of bug, and what actually reduces the blast radius.
This is a defender’s read. No working exploit code here, and none needed. The lessons are in the categories, not the payloads.
The recurring shape: a local user becomes root
The thing these bugs have in common is the outcome: an unprivileged local user — a low-rights service account, a process inside a container, a compromised web app running as www-data — turns into root. That’s “local privilege escalation,” and it’s the second half of nearly every real-world compromise. The attacker rarely lands as root. They land as something small and clamber up. Kernel LPE bugs are the ladder.
Dirty Pipe (CVE-2022-0847) is the cleanest teaching example. Disclosed in early 2022 by Max Kellermann, it was a flaw in the kernel’s pipe handling where, due to an uninitialised flag, data could be written into the page cache backing a file the attacker only had read access to. Read-only files you couldn’t normally touch — including ones owned by root — became writable. The proof-of-concept everyone reached for overwrote a line in a setuid binary or a system file, and from there, root. The bug was elegant precisely because it didn’t need to defeat any memory-corruption defences. It just abused correct-looking logic.
If “writing into a read-only file via a memory-management corner case” sounds familiar, that’s because we’d seen almost exactly this shape six years earlier.
The original sin: Dirty COW
Dirty COW (CVE-2016-5195) was disclosed on 19 October 2016, and it is the grandparent of this whole family. The name is a pun on copy-on-write — the kernel optimisation where a read-only mapping shared between processes is only duplicated at the moment someone writes to it. Dirty COW was a race condition in that breakage: by racing a write against a madvise(MADV_DONTNEED) in a tight loop, an unprivileged user could land their write on the original read-only page rather than the private copy. Same outcome as Dirty Pipe — write to files you should only be able to read, including root-owned binaries — by a completely different mechanism.
The detail that should make every sysadmin uneasy is the lifespan. The race had been in the kernel since around 2007. Linus Torvalds, committing the fix, noted he’d tried to patch the same area eleven years earlier and backed it out because it broke something. So the bug sat in essentially every Linux machine on Earth for nine years before anyone weaponised it publicly. That is the recurring nightmare of kernel security: the flaw isn’t introduced the week before disclosure, it’s discovered then. It was always there, on every box you’ve ever administered.
Why memory-safety mitigations didn’t save us
Here’s the first hard lesson. The kernel has accumulated a serious arsenal of exploit mitigations over the years — KASLR to randomise addresses, SMEP and SMAP to stop the kernel running or reading userspace memory, stack canaries, the lot. They make memory-corruption exploits genuinely harder.
Dirty Pipe sailed past all of it, because it wasn’t a memory-corruption bug. It was a logic flaw. You can’t randomise your way out of a function that does the wrong correct-looking thing. This is why “we have all the mitigations enabled” is necessary but nowhere near sufficient — a whole class of these bugs lives in logic the mitigations never look at.
DirtyCred, presented at Black Hat in 2022 by a team from Northwestern, drove a related point home from the other direction. Rather than corrupting data, it swapped an unprivileged credential structure for a privileged one — exploiting how the kernel manages the cred objects that decide who you are. It generalised a technique so that many otherwise-unrelated bugs could be escalated to root the same way. The lesson: attackers don’t just find bugs, they find reusable exploitation primitives that turn a dozen mediocre bugs into a dozen root shells.
This is why patch-counting misses the point. The security industry loves a tidy “N CVEs fixed this quarter” metric, but a single reusable primitive like DirtyCred quietly upgrades the severity of every heap bug that comes after it. A flaw that the CVSS calculator scores as a moderate, awkward-to-exploit memory issue becomes a reliable root exploit the moment someone wires it into a known primitive. You cannot read severity off the scorecard alone; you have to assume the ecosystem of exploitation tooling around a bug is more capable than the advisory implies, because the people writing that tooling are not constrained by the advisory’s imagination.
And the surface keeps widening. Newer escalation techniques have leaned on subsystems most admins never think about — io_uring, the netfilter layer, the eBPF verifier itself. Each one is a feature someone wanted for performance or flexibility, and each one is more attackable C running with the keys to the kingdom. The defensive read is unglamorous: turn off the kernel features you don’t use. If a host has no business doing asynchronous I/O for untrusted workloads, disabling io_uring via sysctl removes an entire bug class from that machine’s attack surface at zero cost. Reducing what the kernel will even do for an unprivileged caller is the cheapest mitigation there is.
The defensive lessons that actually transfer
So what do you, running real machines, take from all this? Not “panic at every CVE.” These:
Patch latency is the whole game. Dirty Pipe was fixed almost immediately; the people who got burned were running unpatched kernels weeks later. The exposure window is entirely about how fast you reboot into a fixed kernel. A disciplined reboot cadence isn’t hygiene theatre — it’s the single biggest lever you have.
| |
If those two disagree, you have a fixed kernel on disk and a vulnerable one in memory. That gap is where people get owned. Unattended-upgrades will happily install the new kernel package and then sit there for weeks doing nothing useful, because nothing forces the reboot. On machines I can tolerate a brief outage on, I let needrestart or a scheduled maintenance reboot close the gap automatically; on the ones I can’t, the reboot goes on the calendar and actually happens. Livepatch-style technologies (kpatch, Ubuntu Livepatch) can hot-patch some bugs without a reboot, but they don’t cover everything, so treat them as a way to shorten the window, not abolish it.
Assume local code can become root. Every one of these bugs reframes “but they only have a low-privilege shell” as “they have root, give it twenty minutes.” Design as if local access equals total compromise. That means:
- Don’t run untrusted code on hosts that matter. A kernel LPE inside a container is, very often, root on the host. Containers share the host kernel — that’s the bargain, and it’s exactly the surface these bugs attack.
- Reduce who has local access at all. Fewer accounts, fewer running services, fewer setuid binaries lying around to be hijacked.
- Use the kernel’s own seatbelts. seccomp profiles that block the syscalls an exploit needs, and Mandatory Access Control (AppArmor, SELinux) that confines a compromised process, can turn “instant root” into “blocked, and logged.” They don’t fix the bug; they shrink what a single bug buys the attacker.
Detection, because prevention will eventually fail. A defender who can only prevent is a defender who never finds out they were breached. Most of these exploits leave fingerprints if you’re watching the right place. Dirty Pipe writes to files via the page cache; Dirty COW hammers madvise in a loop. The Linux audit subsystem can watch the syscalls and the sensitive files an escalation would touch — I lean on it heavily, as covered in the Linux audit framework guide. A blunt-but-useful starting rule is to alarm on any write to the canonical setuid-clobbering targets:
| |
For the syscall-level view — catching the behaviour of an exploit rather than the file it lands on — eBPF-based tooling like Falco or Tetragon is the modern answer, because it can flag “an unprivileged process just spawned a root shell” in real time regardless of which CVE got it there. That behavioural angle is the one that ages well: it doesn’t care about the exploit’s name.
Defence in depth, because the kernel will fail again. This is the honest one. There will be a Dirty Something in 2026, and 2027. The kernel is twenty-odd million lines of C, and C plus that much surface area guarantees more of these. Planning as though the kernel is a perfect security boundary is planning to be surprised. Planning as though it will eventually be breached — and ensuring that breach is contained, detected, and short-lived — is planning that survives contact with reality.
The container blind spot
There’s one place this lesson is routinely forgotten, and it’s the place it matters most: containers. People reason about a container as if it were a little machine of its own, with its own boundary, and treat “the attacker is only inside the container” as reassuring. It is not. A container is a process with some namespaces and cgroups around it, sharing the same kernel as the host. A kernel LPE inside a container is, very often, root on the host — the container boundary is enforced by the very component the exploit just defeated.
Dirty Pipe was demonstrably exploitable from inside an unprivileged container to write files the host process should have protected. That collapses the comforting mental model entirely. The mitigations that help are the ones that don’t rely on the kernel being perfect: a read-only root filesystem so there’s nothing to overwrite, dropped capabilities so the breakout has fewer levers, user namespaces so “root in the container” maps to an unprivileged UID on the host, and a tight seccomp profile so the syscalls an exploit needs simply aren’t available. None of them fix the kernel bug. All of them shrink what the bug is worth once it fires — which is the entire defensive philosophy of this post in miniature.
The verdict
These exploits aren’t trivia. Each one is a free lesson, paid for by other people’s incident response. The takeaway isn’t to memorise CVE numbers. It’s three habits: patch fast and reboot, treat any local foothold as a probable root compromise, and confine everything so that when the kernel does fail — and it will — the damage is bounded and noisy rather than total and silent.
Who is this for? Anyone running multi-tenant boxes, containers, or anything that executes code they didn’t fully write. Which, again, is everyone. The kernel is doing its best. Your job is to plan for the day its best isn’t enough.




