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,
};
未指定的字段会自动清零,所以:
buf->flags = 0;
另一条 splice_to_pipe() 路径更直接:
buf->flags = 0;
因此当前流程是:
splice文件页进入pipe
↓
flags被清零
↓
文件页成为head-1
↓
write检查CAN_MERGE
↓
检查失败
↓
分配新的匿名pipe页
所以严格来说,Dirty Pipe 不是简单因为“释放时没清 flags”,而是三个条件共同造成:
- 普通 pipe buffer 曾设置
CAN_MERGE。 - slot 释放后旧 flags 仍残留。
- splice 复用 slot 时没有重新初始化 flags。
其中第 3 点是直接的软件缺陷和核心修复点。
0 条评论