Dirty Pipe — overwriting the unwritable, and putting a stop to it
A kernel pipe bug lets unprivileged users scribble over read-only files — including SUID binaries. Here's how to find it and patch the kernel.
01 The Big PictureFor leadership · no jargon
Okay, so this one is Dirty Pipe, and the tl;dr is: a bug in the Linux kernel — the core program that decides who is allowed to do what — lets a regular, unprivileged user rewrite the contents of files the system considers untouchable. That includes the password file and the programs that run as administrator. The classic analogy is a hotel guest who can't open any of the locked doors, but can slip a forged note under the manager's door that changes the master key.
In business terms, this is a local privilege-escalation bug. Anyone who already has a toehold — a compromised web application account, a shell inside a container, a contractor's low-level login — can turn it into full administrator control of the machine. From there: read every file on disk, steal customer data and credentials, plant ransomware, or use the server as a staging ground to attack the rest of your network. Public exploit code has existed since March 2022, works reliably, and CISA has confirmed real-world exploitation. This is not theoretical.
The urgency rule: if any of your Linux hosts run an unpatched kernel from the 2020–2022 era, treat them as a standing risk — especially anything multi-tenant, internet-facing, or running containers where users share the host kernel. The good news (there is some) is that the fix itself is routine: a standard kernel package update and a reboot per host, minutes of actual work, no data loss. Fleets with livepatch support can close the hole without rebooting at all.
02 Technical BreakdownFor engineers
So what's actually gone wrong? The root cause is an uninitialised flags member in struct pipe_buffer. When data is spliced from a file into a pipe, the kernel creates a pipe buffer that references the page cache of that file instead of copying the data (zero-copy — neat optimisation, right up until it isn't). Since commit 241699cd72a8 (Linux 4.9, 2016), the code path in copy_page_to_iter_pipe() failed to initialise the buffer's flags. That was harmless until commit f6dd975583bd (Linux 5.8, 2020) converted the mergeability check into a per-buffer flag, PIPE_BUF_FLAG_CAN_MERGE. From 5.8 onward, a stale flag could survive on a page-cache reference, letting a subsequent write() to the pipe append directly into the page cache of the spliced file — bypassing every file permission, including read-only mounts, immutable files, and SUID binaries. Oof.
Exploitation is fully deterministic — no races, no luck involved. The attacker creates a pipe, fills and drains it to stamp PIPE_BUF_FLAG_CAN_MERGE onto every ring slot, splices one byte from the target file (opened read-only) just before the target offset, then writes attacker-controlled bytes into the pipe. Those bytes land in the page cache, not the pipe. There are practical constraints: the attacker needs read permission on the target file, the write cannot start on a page boundary, cannot cross a page boundary, and cannot grow the file. That is, unfortunately, more than enough — the public PoCs overwrite entries in /etc/passwd or inject code into a SUID binary like /usr/bin/su, yielding a root shell. One subtlety that matters for forensics: the overwrite does not mark the page dirty, so it may never hit disk — the change is in-memory until something else dirties the file, and can vanish on reboot. Do not assume a clean-looking file on disk means the system was not exploited.
The vulnerable range is Linux 5.8 through 5.16.10, fixed upstream in 5.16.11, 5.15.25, and 5.10.102 by initialising the flags field on every new pipe buffer (Max Kellermann's patch). Distros backported that one-line fix into their own kernel packages. The precondition is local code execution — but note that includes any process inside a container on the host, since containers share the kernel. There is no meaningful configuration mitigation here; the only fix is a patched kernel.
03 Affected
Linux kernel 5.8 through 5.16.10 — i.e., any kernel older than 5.16.11, 5.15.25, or 5.10.102 on those stable branches. That covers Ubuntu 20.04 HWE and 21.10, Debian 11 (bullseye, kernel 5.10), RHEL 9 / Rocky 9 (5.14), Fedora, and any container host on those kernels — container images do not matter, the host kernel is the vulnerable component. Kernels older than 5.8 (Ubuntu 18.04's 4.15, RHEL 8's 4.18, Debian 10's 4.19) carry the latent bug but not the exploitable path, which is a rare stroke of luck. If your distro shipped a patched build, trust the distro package version over the upstream number — they backported.
04 Detection
Right, let's find out where we stand. Start with the obvious:
uname -rRunning kernel version. Vulnerable if it's 5.8–5.16.10 and below your branch's fix (5.16.11 / 5.15.25 / 5.10.102).
dpkg -l 'linux-image-*' | grep ^ii | awk '{print $2, $3}'On Debian/Ubuntu, list installed kernel packages and compare against the patched build for your release (e.g. Ubuntu 20.04: 5.13.0-35.40 or later).
rpm -qa 'kernel*' --qf '%{NAME} %{VERSION}-%{RELEASE}\n' | sort -VThe RHEL/Fedora equivalent; compare the newest entry against your release's kernel advisory.
ansible all -m command -a 'uname -r' -f 50Fan out across the fleet; collect every host's kernel in one pass and flag anything in the vulnerable range.
kubectl get nodes -o wideKubernetes: the IMAGE column shows the node OS and the KERNEL-VERSION column shows the kernel — every node kernel must be patched, since pods share it.
find /tmp /var/tmp /dev/shm -type f -newermt '2022-03-07' 2>/dev/null | headWeak signal hunt for dropped PoC binaries in world-writable dirs. Absence proves nothing, mind — the attack leaves almost no artefacts.
05 Remediation
01 Patch the kernel package
First, install the fixed kernel through the normal package manager so the distro-backported fix lands. The upstream fix is a one-line initialisation, backported into every supported distro kernel — no heroics required.
sudo apt-get update && sudo apt-get install --only-upgrade linux-image-genericDebian/Ubuntu: upgrade the kernel metapackage only, without touching the rest of the system.
sudo dnf upgrade kernel -yRHEL/Fedora/Rocky: pull the patched kernel package.
02 Boot into the new kernel
Here's the bit people trip over: the running kernel is still vulnerable after the package install — the fix is only live once the host boots the new build. On hosts with kernel livepatch (Ubuntu Advantage, kpatch) the fix can be applied without a reboot, but verify the livepatch module actually covers CVE-2022-0847 before trusting it.
sudo rebootBoot the patched kernel. Schedule it; this is the only non-negotiable step.
03 Contain containers until the host is patched
On shared kernel hosts (Kubernetes nodes, CI runners, shared dev boxes), an unpatched host means every container tenant can take over the node. Until the reboot lands, treat pods running untrusted code as the highest-risk surface: restrict scheduling of new untrusted workloads and tighten runAsUser/privilege settings so the blast radius is contained.
kubectl cordon <node> && kubectl drain <node> --ignore-daemonsets --delete-emptydir-dataEvict workloads from a node before rebooting it; repeat per node. Bit of a faff on a big cluster, but so is a breach.
06 Verification
uname -rMust show a kernel at or above the fixed release for your branch (5.16.11 / 5.15.25 / 5.10.102) or the distro-patched build.
cat /etc/passwd | head -3 && ls -l /usr/bin/suSanity check: critical account files are intact and SUID binaries show expected ownership and timestamps.
kubectl get nodes -o wide | awk '{print $1,$3}'On nodes, confirm every host rebooted onto the patched kernel — one straggler node keeps the whole cluster exposed.
sudo apt-get install -y linux-image-generic && apt-cache policy linux-image-genericConfirm the installed candidate matches the distro security advisory version for your release.
rpm -q kernel | sort -V | tail -1RHEL-family equivalent; the newest installed kernel must be the patched build from your advisory.
07 Rollback
If the new kernel misbehaves, you can boot back into the previous one:
ls /boot/vmlinuz-* | sort -VList available kernels; pick the previous entry at the GRUB menu (Advanced options) and reboot into it.
sudo grubby --set-default-index=0Choose the previous kernel with grubby and reboot back if the new kernel misbehaves. Note this re-exposes the host — treat a rollback as an emergency measure and re-patch as soon as the issue is understood.
08 References
- NVD — CVE-2022-0847
- The Dirty Pipe Vulnerability — Max Kellermann's disclosure
- CISA KEV catalog
- Ubuntu USN-5317-1
---
Spotted a mistake or stuck on a step? Corrections and questions are always welcome.
Ship the fix. Prove it’s closed.
If this runbook doesn’t fit your stack, send the CVE — an operator writes a verified path, usually within a business day. Or contribute your own and get co-authored credit.