CVE-2022-0847: Dirty Pipe In Linux Kernel 5.8

· omacs's blog

#1day

# Contents

  1. Introduction
  2. Background
    1. How file I/O works in linux kernel
    2. Linux pipe
    3. Splice system call
  3. Root Cause Analysis
  4. Exploit
  5. Patch
  6. References

# Introduction

Dirty pipe 취약점은 Max Kalleramnn이 고객사 문의를 해결하는 과정에서 발견한 취약점으로, 리눅스 커널 내부에서 새로운 파이프 버퍼가 할당될 때 적절한 초기화 작업이 수행되지 않아 발생하였다[6, 9].

파이프는 단방향 데이터 채널로, 프로세스 간 데이터 통신에 사용될 수 있다. 리눅스 커널은 이를 파이프 버퍼의 ring (FIFO)로 구현하며, 각 파이프 버퍼는 페이지를 참조하여 데이터를 읽거나 쓸 수 있다. 이때 user space와 kernel space 간 데이터 복사 과정에서 발생하는 overhead를 줄이기 위해 5274f052e7b3 커밋 [8]에서 splice system call이 제안되었다[2, 6, 7].

Splice system call는 페이지의 데이터를 복사하는 것이 아닌, 페이지를 참조하는 포인터를 추가하는 방식으로 데이터를 이동시킨다. 이때 f6dd975583bd 커밋 [10] 이후에는 해당 페이지에 이미 데이터가 존재한다면, write system call은 파이프 버퍼 구조체의 flag 멤버 변수의 값을 확인한다. 그리고 PIPE_BUF_FLAG_CAN_MERGE가 셋트되어 있다면 페이지에 데이터를 추가한다[4, 6].

그런데 데이터가 추가되는 페이지가 파이프가 소유하는 페이지가 아니라면, 파이프가 아닌 다른 파일에 데이터가 복사될 수 있다[6].

# Background

그럼 이제 리눅스 커널에서 파일 입출력이 어떻게 동작하는지, 그리고 파이프와 splice system call이 어떻게 동작하는지 좀 더 세부적으로 알아보겠다.

# How file I/O works in linux kernel

리눅스 커널은 사용자가 read 또는 write system call이 호출하면 일반적으로 (generic) 검사해야 하는 사항들을 확인한다. 그리고 해당 파일 시스템의 read 또는 write 함수를 호출한다. 파일 시스템의 함수는 파일을 읽거나 쓸 때 검사해야 하는 사항들을 검사하고 페이지 캐시에 데이터를 읽거나 쓰기 위한 함수를 호출한다. 이 과정은 다음과 같은 함수 콜 트레이스 예시 (파일 쓰기 연산)로부터 알 수 있다:

 1write()
 2ksys_write()
 3vfs_write()
 4    rw_verfiy_area()
 5    new_sync_write()
 6call_write_iter()
 7file->f_op->write_iter()  /* Here, we think this as ext4_file_write_iter() */
 8                          /* f_op is assigned when open system call */
 9ext4_buffered_write_iter()
10generic_perform_write() /* Write data to page cache and mark as dirty */

이렇게 리눅스 커널은 파일을 읽고 쓸 때 페이지 캐시를 사용하여 상대적으로 비싼 비용을 치러야 하는 디스크 연산을 피한다. 즉, 파일로부터 데이터를 읽을 때는 먼저 디스크로부터 페이지 캐시에 데이터를 넣고 그 이후의 읽기 연산에서는 디스크가 아닌 페이지 캐시로부터 읽어들인다. 이는 파일에 데이터를 쓸 때도 적용되는데, 먼저 페이지 캐시에 데이터를 쓰고 해당 페이지에 dirty 표시를 한 후에 해당 데이터를 다른 프로세스가 참조할 때 디스크에 쓰는 것이다[3].

# Linux pipe and splice system call

리눅스 커널에서 파이프는 파이프 파일 시스템에 속한 파일로 다루어진다. 따라서 파이프 파일을 연 후에 write system call을 호출하면 pipe_write 함수가 호출될 것이다. 그 이유는 파이프 파일 시스템의 file_operations가 다음과 같이 구현되어 있기 때문이다 (path: fs/pipe.c):

 1const struct file_operations pipefifo_fops = {
 2        .open           = fifo_open,
 3        .llseek         = no_llseek,
 4        .read_iter      = pipe_read,
 5        .write_iter     = pipe_write,
 6        .poll           = pipe_poll,
 7        .unlocked_ioctl = pipe_ioctl,
 8        .release        = pipe_release,
 9        .fasync         = pipe_fasync,
10};

이러한 파이프는 내부적으로 파이프 버퍼의 ring (FIFO)로 구현되며, 각각의 버퍼는 데이터를 읽거나 쓰기 위한 페이지를 참조한다 (path: include/linux/pipe_fs_i.h).

    +--> bufs: [0] <-------------head, tail<-+
    |           +---> page            |      |
    |          [1]                    V      |
    |           +---> page            |      |
    |          [2]                    V      |
    |           +---> page            |      |
    |          [...]                  V      |
    |           +---> page            +------+
    |
    +--> tmp_page
    |
    |
pipe_inode_info

위 그림에서 head는 다음에 할당될 파이프 버퍼에 대한 인덱스이고, tail은 다음에 사용될 파이프 버퍼에 대한 인덱스이다. 이러한 파이프 버퍼는 다음과 같이 코드상에서 구현된다:

 1/**
 2 *	struct pipe_buffer - a linux kernel pipe buffer
 3 *	@page: the page containing the data for the pipe buffer
 4 *	@offset: offset of data inside the @page
 5 *	@len: length of data inside the @page
 6 *	@ops: operations associated with this buffer. See @pipe_buf_operations.
 7 *	@flags: pipe buffer flags. See above.
 8 *	@private: private data owned by the ops.
 9 **/
10struct pipe_buffer {
11	struct page *page;
12	unsigned int offset, len;
13	const struct pipe_buf_operations *ops;
14	unsigned int flags;
15	unsigned long private;
16};

지금까지 설명한 것을 종합하면 파이프에 데이터를 쓰는 과정은 다음과 같을 것이라고 생각해볼 수 있다:

1. user calls write system call
2. pipe_write() is called by call_write_iter()
3. data is transferred from write end pipe to read end pipe

그리고 다음 코드의 (1)로부터 PIPE_BUF_FLAG_CAN_MERGE가 셋트되어 있다면 데이터를 기 존재하는 페이지에 추가할 것임을 알 수 있다:

 1static ssize_t
 2pipe_write(struct kiocb *iocb, struct iov_iter *from)
 3{
 4        struct file *filp = iocb->ki_filp;
 5        struct pipe_inode_info *pipe = filp->private_data;
 6        unsigned int head;
 7        ssize_t ret = 0;
 8        size_t total_len = iov_iter_count(from);
 9        ssize_t chars;
10        bool was_empty = false;
11        bool wake_next_writer = false;
12
13        /* Null write succeeds. */
14        if (unlikely(total_len == 0))
15                return 0;
16
17        __pipe_lock(pipe);
18
19        if (!pipe->readers) {
20                send_sig(SIGPIPE, current, 0);
21                ret = -EPIPE;
22                goto out;
23        }
24	
25	/* ... */
26	
27	head = pipe->head;
28        was_empty = pipe_empty(head, pipe->tail);
29        chars = total_len & (PAGE_SIZE-1);
30        if (chars && !was_empty) {
31                unsigned int mask = pipe->ring_size - 1;
32                struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
33                int offset = buf->offset + buf->len;
34
35                if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
36                    offset + chars <= PAGE_SIZE) {
37                        ret = pipe_buf_confirm(pipe, buf);
38                        if (ret)
39                                goto out;
40
41                        ret = copy_page_from_iter(buf->page, offset, chars, from); /* (1) */
42                        if (unlikely(ret < chars)) {
43                                ret = -EFAULT;
44                                goto out;
45                        }
46
47                        buf->len += ret;
48			if (!iov_iter_count(from))
49                                goto out;
50                }
51        }
52	
53	for (;;) {
54                if (!pipe->readers) {
55                        send_sig(SIGPIPE, current, 0);
56                        if (!ret)
57                                ret = -EPIPE;
58                        break;
59                }
60
61                head = pipe->head;
62		if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
63                        unsigned int mask = pipe->ring_size - 1;
64                        struct pipe_buffer *buf = &pipe->bufs[head & mask];
65                        struct page *page = pipe->tmp_page;
66                        int copied;
67			
68			/* ... */
69			
70                        if (is_packetized(filp))
71                                buf->flags = PIPE_BUF_FLAG_PACKET;
72                        else /* (2) */
73                                buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
74				
75			/* ... */
76		}
77		
78		/* ... */
79	}
80	
81	/* ... */
82}

그럼 PIPE_BUF_FLAG_CAN_MERGE는 어떤 상황에서 셋트될까? 위 코드의 (2)를 보면 파이프의 파일 구조체가 packetize되지 않은 경우에 셋트함을 알 수 있다. 그리고 파이프에 대한 manpage를 살펴보면 파이프를 열 때 O_DIRECT를 셋트한 경우에 packet mode로 동작함을 알 수 있다. 따라서 O_DIRECT를 셋트하지 않고 파이프를 연다면 PIPE_BUF_FLAG_CAN_MERGE가 셋트될 것이다[2].

# Splice system call

Splice system call은 파이프를 사용하여 데이터 통신을 수행할 때 user space와 kernel space 간 데이터 복사로 인해 발생하는 overhead를 줄이기 위해 제안되었다. 그러나 파이프 파일 간에만 사용되는 것은 아니며, file-to-pipe, pipe-to-file, pipe-to-pipe에 사용될 수 있다. 이때 overhead를 줄이는 방식은 복사하고자 하는 데이터가 존재하는 페이지에 대한 참조를 추가하는 것 (또는, refcount를 증가시키는 것)이다[4, 8].

splice between file and pipe:
               +-----> [page] <----+
               |                   |
               |                   |
          page cache             pipe buffer
	  
splice between pipe and pipe:
               +-----> [page] <----+
               |                   |
               |                   |
          ipipe buffer        opipe buffer

다음 코드의 (1), (2)는 이를 잘 나타낸다:

 1/*
 2 * Splice contents of ipipe to opipe.
 3 */
 4static int splice_pipe_to_pipe(struct pipe_inode_info *ipipe,
 5			       struct pipe_inode_info *opipe,
 6			       size_t len, unsigned int flags)
 7{
 8	struct pipe_buffer *ibuf, *obuf;
 9	unsigned int i_head, o_head;
10	unsigned int i_tail, o_tail;
11	unsigned int i_mask, o_mask;
12	int ret = 0;
13	bool input_wakeup = false;
14
15retry:
16	ret = ipipe_prep(ipipe, flags);
17	if (ret)
18		return ret;
19
20	ret = opipe_prep(opipe, flags);
21	if (ret)
22		return ret;
23
24	/*
25	 * Potential ABBA deadlock, work around it by ordering lock
26	 * grabbing by pipe info address. Otherwise two different processes
27	 * could deadlock (one doing tee from A -> B, the other from B -> A).
28	 */
29	pipe_double_lock(ipipe, opipe);
30
31	i_tail = ipipe->tail;
32	i_mask = ipipe->ring_size - 1;
33	o_head = opipe->head;
34	o_mask = opipe->ring_size - 1;
35	
36	do {
37	        /* ... */
38		
39		ibuf = &ipipe->bufs[i_tail & i_mask];
40		obuf = &opipe->bufs[o_head & o_mask];
41
42		if (len >= ibuf->len) {
43			/*
44			 * Simply move the whole buffer from ipipe to opipe
45			 */
46			*obuf = *ibuf; /* (1) */
47			
48			/* ... */
49		} else {
50		        /* ... */
51			
52			*obuf = *ibuf;
53			
54			/* ... */
55		}
56		
57		/* ... */
58	} while (len);
59	
60	/* ... */
61}
 1static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
 2			 struct iov_iter *i)
 3{
 4        /* ... */
 5	off = i->iov_offset;
 6	buf = &pipe->bufs[i_head & p_mask];
 7	if (off) { /* (3) */
 8		if (offset == off && buf->page == page) {
 9			/* merge with the last one */
10			buf->len += bytes;
11			i->iov_offset += bytes;
12			goto out;
13		}
14		i_head++;
15		buf = &pipe->bufs[i_head & p_mask];
16	}
17	if (pipe_full(i_head, p_tail, pipe->max_usage))
18		return 0;
19	
20	buf->ops = &page_cache_pipe_buf_ops;
21	get_page(page); /* increases refcount of argument page */
22	buf->page = page; /* (2) */
23	buf->offset = offset;
24	buf->len = bytes;
25	
26	/* ... */
27}

위 코드의 (2)에서 get_page()는 인자로 전달된 페이지의 refcount를 증가시키는 함수임에 주목하라. 이는 copy_page_to_iter_pipe()에 전달된 page와 (2)의 page가 같음을 의미한다. 즉, file-to-pipe에서 파일의 페이지 캐싱을 위해 할당된 페이지를 파이프 버퍼의 page 멤버 변수가 참조한다는 것이다. 그럼 위 코드의 (3)이 가리키는 if 문에서 마지막 것과 병합하는 코드가 그 이후에 실행될 수 있다.

# Root Cause Analysis

다시 splice system call을 살펴보면 file-to-pipe의 경우에 다음과 같은 함수 콜 트레이스를 거쳐서

splice()
do_splice()
do_splice_to()

do_splice_to()가 호출됨을 확인할 수 있다.

 1/*
 2 * Attempt to initiate a splice from a file to a pipe.
 3 */
 4static long do_splice_to(struct file *in, loff_t *ppos,
 5			 struct pipe_inode_info *pipe, size_t len,
 6			 unsigned int flags)
 7{
 8	int ret;
 9
10	if (unlikely(!(in->f_mode & FMODE_READ)))
11		return -EBADF;
12
13	ret = rw_verify_area(READ, in, ppos, len);
14	if (unlikely(ret < 0))
15		return ret;
16
17	if (unlikely(len > MAX_RW_COUNT))
18		len = MAX_RW_COUNT;
19
20	if (in->f_op->splice_read)
21		return in->f_op->splice_read(in, ppos, pipe, len, flags);
22	return default_file_splice_read(in, ppos, pipe, len, flags);
23}

이때 해당 파일이 ext4 파일 시스템에 속하는 파일이라고 가정하면, 다음과 같은 함수 콜 트레이스를 거쳐서

file->f_op->splice_read() /* this is generic_file_splice_read() */
call_read_iter()
file->f_op->read_iter()
ext4_file_read_iter()
generic_file_read_iter()
generic_file_buffered_read()
copy_page_to_iter()
copy_page_to_iter_pipe()

copy_page_to_iter_pipe()가 호출됨을 알 수 있다. 이때 이 함수의 코드는 다음과 같다:

 1static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
 2			 struct iov_iter *i)
 3{
 4	struct pipe_inode_info *pipe = i->pipe;
 5	struct pipe_buffer *buf;
 6	unsigned int p_tail = pipe->tail;
 7	unsigned int p_mask = pipe->ring_size - 1;
 8	unsigned int i_head = i->head;
 9	size_t off;
10
11	if (unlikely(bytes > i->count))
12		bytes = i->count;
13
14	if (unlikely(!bytes))
15		return 0;
16
17	if (!sanity(i))
18		return 0;
19
20	off = i->iov_offset;
21	buf = &pipe->bufs[i_head & p_mask];
22	if (off) {
23		if (offset == off && buf->page == page) {
24			/* merge with the last one */
25			buf->len += bytes;
26			i->iov_offset += bytes;
27			goto out;
28		}
29		i_head++;
30		buf = &pipe->bufs[i_head & p_mask];
31	}
32	if (pipe_full(i_head, p_tail, pipe->max_usage))
33		return 0;
34
35        /****** (1) ******/
36	buf->ops = &page_cache_pipe_buf_ops;
37	get_page(page); /* increases refcount of argument page */
38	buf->page = page;
39	buf->offset = offset;
40	buf->len = bytes;
41
42	pipe->head = i_head + 1;
43	i->iov_offset = offset + bytes;
44	i->head = i_head;
45out:
46	i->count -= bytes;
47	return bytes;
48}

위 코드의 (1) 이하의 코드를 살펴보면 파이프 버퍼 구조체의 flag 멤버 변수에 대한 초기화가 없음을 알 수 있다. 즉, 위 함수를 통해 할당된 파이프 버퍼는 그 이전에 셋트된 flag 값을 (e.g., PIPE_BUF_FLAG_CAN_MERGE) 그대로 따라간다는 것이다.

# PoC: Exploit

# References

  1. Max Kallermann, "lib/iov_iter: initialize flags in new pipe_buffer," 2022. [Online]. Available: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/lib/iov_iter.c?id=9d2231c5d74e13b2a0546fee6737ee4446017903, [Accessed Sep. 07, 2023].
  2. "pipe(2) -- Linux manual page," 2023. [Online]. Available: https://man7.org/linux/man-pages/man2/pipe.2.html, [Accessed Sep. 07, 2023].
  3. "Linux memory management." [Online]. Available: https://www.kernel.org/doc/html/latest/admin-guide/mm/concepts.html#page-cache, [Accessed Sep. 07, 2023].
  4. "splice(2) -- Linux manual page," 2023. [Online]. Available: https://man7.org/linux/man-pages/man2/splice.2.html, [Accessed Sep. 07, 2023].
  5. hyeyoo, "Page Cache: filemap_read", 2022. [Online]. Available: https://hyeyoo.com/161, [Accessed Sep. 07, 2023].
  6. Max Kallermann, "The Dirty Pipe Vulnerability," 2022. [Online]. Available: https://dirtypipe.cm4all.com/, [Accessed Sep. 08, 2023].
  7. Jonathan Corbet, "Rethinking splice()," 2023. [Online]. Available: https://lwn.net/Articles/923237/, [Accessed Jan. 21, 2024].
  8. Jens Axboe,"[PATCH] Introduce sys_splice() system call," 2006. [Online]. Available: https://github.com/torvalds/linux/commit/5274f052e7b3dbd81935772eb551dfd0325dfa9d, [Accessed Jan. 21, 2024].
  9. "CVE-2022-0847 Detail," NIST. [Online]. Available: https://nvd.nist.gov/vuln/detail/cve-2022-0847, [Accessed Jan. 21, 2024].
  10. Christoph Hellwig, "pipe: merge anon_pipe_buf*_ops," 2020. [Online]. Available: https://github.com/torvalds/linux/commit/f6dd975583bd8ce088400648fd9819e4691c8958, [Accessed Jan. 21, 2024].