写入不可写内存时,read()的不同行为取决于表示文件的文件描述符,匿名管道或套接字的文件描述符

时间:2019-05-01 06:18:08

标签: c++ linux linux-kernel

我有以下代码:

#include <sys/mman.h>
#include <unistd.h>
#include <cstdio>

int main() {
        char *p = (char *)mmap(0, 0x3000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        munmap(p + 0x2000, 0x1000);
        p += 0x800;
        printf("%zd\n", read(0, p, 0x3000));
        return 0;
}

当编译并使用将写入可写内存的输入运行它时,根据输入该输入的方式,我会得到不同的行为:

$ python3 -c 'print("A"*0x3000)' | ./test
4096
$ python3 -c 'print("A"*0x3000)' > input.bin; ./test < input.bin
6144
$ nc.traditional -l -p 1337 -e ./test &
[1] 25855
$ python3 -c 'print("A"*0x3000)' | nc localhost 1337
-1
[1]+  Done                    nc.traditional -l -p 1337 -e ./test

在所有情况下,我都希望read()调用返回相同的结果,但事实并非如此。为什么我会有不同的行为?

1 个答案:

答案 0 :(得分:0)

此命令的strace输出显示,一次写入了全部12288(0x3000)字节:

$ strace python3 -c 'print("A"*0x3000)' | ./test
...
write(1, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 12288) = 12288
...

在管道另一端的./test命令上使用strace显示仅读取4096(0x1000)个字节:

$ python3 -c 'print("A"*0x3000)' | strace ./test
...
read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 12288) = 4096
...

如果./test的标准输入来自input.bin(包含12288个'A',后跟换行符),则strace显示读取了6144(0x1800)个字节:

$ strace ./test < input.bin
...
read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 12288) = 6144
...

我认为只能通过在“ fs / pipe.c”中的pipe_read函数中研究内核代码以从管道读取来找到解释。在内部,管道包含struct pipe_buffer的循环数组,每个循环数组都包含一个指向数据页的指针。 pipe_read函数遍历这些缓冲区,将内容复制到用户,直到读取了所有请求的数据,管道中没有任何内容(在阻塞模式下等待来自写入器的更多数据之后)或错误将数据复制到用户时发生。发生故障时,仅返回4096个字节而不是6144个字节的原因似乎是由于以下代码:

        written = copy_page_to_iter(buf->page, buf->offset, chars, to);
        if (unlikely(written < chars)) {
            if (!ret)
                ret = -EFAULT;
            break;
        }
        ret += chars;
        buf->offset += chars;
        buf->len -= chars;

(以上部分来自Linux内核版本4.19。)

在这里,chars是要从当前管道缓冲区复制的数量,written是实际复制的数量,ret是函数的返回值,通常是读取的字节数。 ret的初始值为0。通常将written设置为chars,除非发生地址错误,在这种情况下它将小于chars

从管道复制第二页时发生错误。成功复制了第一页,因此ret将为4096。复制第二页时,6144-4096 = 2048(0x800)字节后,用户缓冲区中发生地址错误,因此break;语句为在到达ret += chars;语句之前到达循环中断。 ret未设置为-EFAULT,因为它非零。但是,就pipe_buf函数的返回值而言,从管道第二页到用户缓冲区的2048字节的部分副本已被忽略。 (忽略的数据不会从管道中丢弃。管道中的位置仅在成功复制后才继续移动,因此对pipe_read的后续调用将从同一点继续。)

请注意,尽管read仅返回4096,但实际上似乎实际上已将6144字节复制到用户缓冲区。通过p + 4096调用返回后,从read开始检查缓冲区内容,可以确认这一点。

我敢肯定pipe_read函数的相关部分可以重写以正确说明部分复制的部分,但是我不知道核心内核开发人员是否会认为它值得修复。例如,一个修复程序可能会将上面的代码替换为以下(未经测试的)代码:

        written = copy_page_to_iter(buf->page, buf->offset, chars, to);
        ret += written;
        buf->offset += written;
        buf->len -= written;
        if (unlikely(written < chars)) {
            if (!ret)
                ret = -EFAULT;
            break;
        }

我非常确定,通过更改内核代码,您的./test程序将能够从管道读取6144字节。