Dirty Pipe 的根因是,环形 slot 被复用时,文件页对应的新 pipe_buffer 没有初始化 flags,错误继承了旧的 PIPE_BUF_FLAG_CAN_MERGE,导致后续 write(pipe) 把数据直接合并进只读文件的 page cache 页。

完整触发过程

1. 先用普通 write 填充 pipe

普通 anon_pipe_write() 创建的 buffer 会设置:

buf->flags = PIPE_BUF_FLAG_CAN_MERGE;

假设某个 slot:

slot N:
page = 匿名pipe页
flags = CAN_MERGE
ops   = anon_pipe_buf_ops

2. 把 pipe 读空

读端消费完后:

pipe_buf_release(pipe, buf);
pipe->tail++;

pipe_buf_release() 只清除:

buf->ops = NULL;

不会清理:

buf->flags
buf->page
buf->offset
buf->len

所以空闲 slot 中可能残留:

slot N:
ops   = NULL
flags = CAN_MERGE   ← 残留

注意:残留本身还不是漏洞,因为该 slot 逻辑上已经无效。

3. splice 把只读文件页放入这个 slot

攻击者对只读文件执行类似:

splice(file_fd, &offset, pipe_write_fd, NULL, 1, 0);

splice() 不复制文件内容,而是让 pipe_buffer.page 直接引用文件的 page cache 页。

漏洞版本初始化了:

buf->page   = file_page;
buf->offset = offset;
buf->len   = len;
buf->ops   = &page_cache_pipe_buf_ops;

但是漏掉了:

buf->flags = 0;

结果变成:

slot N:
page = 只读文件的page cache页
ops   = page_cache_pipe_buf_ops
flags = CAN_MERGE                 ← 错误继承

splice 完成后还会推进:

pipe->head++;

所以这个文件页 buffer 现在正好是:

pipe_buf(pipe, head - 1)

也就是下一次 pipe 写入检查的“最后一个有效 buffer”。

4. 再次 write(pipe)

anon_pipe_write() 执行:

buf = pipe_buf(pipe, head - 1);

if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
  offset + chars <= PAGE_SIZE) {
  copy_page_from_iter(buf->page, offset, chars, from);
}

因为文件页错误带有:

PIPE_BUF_FLAG_CAN_MERGE

检查通过,于是:

copy_page_from_iter()

直接把攻击者数据写入:

只读文件的 page cache 页

这一过程没有走正常的文件写入路径,因此绕过了:

  • 文件是否以可写方式打开;
  • inode 写权限;
  • 文件系统只读状态相关检查;
  • 正常的 write() 权限验证和写时复制逻辑。

流程可以压缩为:

普通write(pipe)
  ↓
slot.flags = CAN_MERGE
  ↓
读空pipe,flags残留
  ↓
splice(只读文件 → pipe)
  ↓
slot.page换成文件page cache
但flags没有初始化
  ↓
文件页错误带有CAN_MERGE
  ↓
write(pipe)
  ↓
向head-1对应的文件页追加
  ↓
只读文件page cache被修改

为什么“合并机制”会修改文件

正常设计假设:

CAN_MERGE → 这是pipe自己拥有的匿名页,可以安全追加

但漏洞状态变成:

CAN_MERGE → 实际上是文件page cache页

也就是 buffer 的:

page/ops:表示文件页
flags:却表示匿名可合并页

这是一个典型的元数据状态不一致漏洞。

当前本地代码如何修复

当前 filemap_splice_read() 使用结构体整体赋值:

*buf = (struct pipe_buffer) {
  .ops   = &page_cache_pipe_buf_ops,
  .page   = page,
  .offset = offset,
  .len   = part,
};

mm/filemap.c (line 2967)

未指定的字段会自动清零,所以:

buf->flags = 0;

另一条 splice_to_pipe() 路径更直接:

buf->flags = 0;

fs/splice.c (line 223)

因此当前流程是:

splice文件页进入pipe
  ↓
flags被清零
  ↓
文件页成为head-1
  ↓
write检查CAN_MERGE
  ↓
检查失败
  ↓
分配新的匿名pipe页

所以严格来说,Dirty Pipe 不是简单因为“释放时没清 flags”,而是三个条件共同造成:

  1. 普通 pipe buffer 曾设置 CAN_MERGE
  2. slot 释放后旧 flags 仍残留。
  3. splice 复用 slot 时没有重新初始化 flags。

其中第 3 点是直接的软件缺陷和核心修复点。

分类: 内核

pareto

未来什么方向不管,先做自己喜欢做的事情。

0 条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注