参考: GitHub - chenaotian/CVE-2022-0847: CVE-2022-0847 POC and Docker and Analysis write up

    pipe 是内核提供的一个通信管道,通过pipe/pipe2 函数创建,返回两个文件描述符,一个用于发送数据,另一个用于接受数据,类似管道的两段。

    pipe 原理和pipe_write - 图1

    简单说一下在内核中的实现,通常pipe 缓存空间总长度65536 字节用页的形式进行管理,总共16页(一页4096字节),页面之间并不连续,而是通过数组进行管理,形成一个环形链表。维护两个链表指针,一个用来写(pipe->head),一个用来读(pipe->tail),这里主要分析一下函数:
    1. static ssize_t
    2. pipe_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. ··· ···
    14. head = pipe->head;
    15. was_empty = pipe_empty(head, pipe->tail);
    16. chars = total_len & (PAGE_SIZE-1);
    17. if (chars && !was_empty) {
    18. //[1]pipe 缓存不为空,则尝试是否能从当前最后一页"接着"写
    19. unsigned int mask = pipe->ring_size - 1;
    20. struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
    21. int offset = buf->offset + buf->len;
    22. if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
    23. offset + chars <= PAGE_SIZE) {
    24. /*[2]关键,如果PIPE_BUF_FLAG_CAN_MERGE 标志位存在,代表该页允许接着写
    25. *如果写入长度不会跨页,则接着写,否则直接另起一页 */
    26. ret = pipe_buf_confirm(pipe, buf);
    27. ···
    28. ret = copy_page_from_iter(buf->page, offset, chars, from);
    29. ···
    30. }
    31. buf->len += ret;
    32. ···
    33. }
    34. }
    35. for (;;) {//[3]如果上一页没法接着写,则重新起一页
    36. ··· ···
    37. head = pipe->head;
    38. if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
    39. unsigned int mask = pipe->ring_size - 1;
    40. struct pipe_buffer *buf = &pipe->bufs[head & mask];
    41. struct page *page = pipe->tmp_page;
    42. int copied;
    43. if (!page) {//[4]重新申请一个新页
    44. page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
    45. if (unlikely(!page)) {
    46. ret = ret ? : -ENOMEM;
    47. break;
    48. }
    49. pipe->tmp_page = page;
    50. }
    51. spin_lock_irq(&pipe->rd_wait.lock);
    52. head = pipe->head;
    53. ··· ···
    54. pipe->head = head + 1;
    55. spin_unlock_irq(&pipe->rd_wait.lock);
    56. /* Insert it into the buffer array */
    57. buf = &pipe->bufs[head & mask];
    58. buf->page = page;//[5]将新申请的页放到页数组中
    59. buf->ops = &anon_pipe_buf_ops;
    60. buf->offset = 0;
    61. buf->len = 0;
    62. if (is_packetized(filp))
    63. buf->flags = PIPE_BUF_FLAG_PACKET;
    64. else
    65. buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
    66. //[6]设置flag,默认PIPE_BUF_FLAG_CAN_MERGE
    67. pipe->tmp_page = NULL;
    68. copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
    69. //[7]拷贝操作
    70. ··· ···
    71. ret += copied;
    72. buf->offset = 0;
    73. buf->len = copied;
    74. ··· ···
    75. }
    76. ··· ···
    77. }
    78. ··· ···
    79. return ret;
    80. }
    1. 如果当前管道(pipe)中不为空(head==tail判定为空管道),则说明现在管道中有未被读取的数据,则获取head 指针,也就是指向最新的用来写的页,查看该页的lenoffset(为了找到数据结尾)。接下来尝试在当前页面续写
    2. 判断 当前页面是否带有** PIPE_BUF_FLAG_CAN_MERGE flag标记,如果不存在则不允许在当前页面续写**。或当前写入的数据拼接在之前的数据后面长度超过一页(即写入操作跨页),如果跨页,则无法续写。
    3. 如果无法在上一页续写,则另起一页
    4. alloc_page 申请一个新的页
    5. 将新的页放在数组最前面(可能会替换掉原有页面),初始化值。
    6. buf->flag 默认初始化为 PIPE_BUF_FLAG_CAN_MERGE ,因为默认状态是允许页可以续写的。
    7. 拷贝写入的数据,没拷贝完重复上述操作。