【问题标题】:Linux TCP recv() with MSG_TRUNC - writes to buffer?带有 MSG_TRUNC 的 Linux TCP recv() - 写入缓冲区?
【发布时间】:2017-01-05 23:14:37
【问题描述】:

我刚刚在 TCP 套接字上尝试在 recv 中使用标志 MSG_TRUNC 时遇到了令人惊讶的缓冲区溢出。

而且它似乎只发生在 gcc(而不是 clang)并且仅在使用优化编译时发生。

根据此链接:http://man7.org/linux/man-pages/man7/tcp.7.html

从 2.4 版开始,Linux 支持在 recv(2)(和 recvmsg(2))的 flags 参数中使用 MSG_TRUNC。此标志导致接收到的数据字节被丢弃,而不是在调用者提供的缓冲区中传回。从 Linux 2.4.4 开始,MSG_PEEK 与 MSG_OOB 结合使用时也具有此效果,以接收带外数据。

这是否意味着提供的缓冲区不会被写入?我是这么想的,但很惊讶。 如果您传递一个缓冲区(非零指针)并且大小大于缓冲区大小,则当客户端发送大于缓冲区的内容时会导致缓冲区溢出。如果消息很小并且适合缓冲区(没有溢出),它实际上似乎不会将消息写入缓冲区。 显然,如果您传递一个空指针,问题就会消失。

客户端是一个简单的 netcat,发送大于 4 个字符的消息。

服务器代码基于: http://www.linuxhowtos.org/data/6/server.c

使用 MSG_TRUNC 将读取更改为 recv,并将缓冲区大小更改为 4(bzero 也更改为 4)。

在 Ubuntu 14.04 上编译。这些编译工作正常(没有警告):

gcc -o server.x server.c

clang -o server.x server.c

clang -O2 server.x server.c

这是错误 (?) 编译,它还给出了有关问题的警告提示:

gcc -O2 -o server.x server.c

无论如何,就像我提到的将指针更改为 null 可以解决问题,但这是一个已知问题吗?还是我错过了手册页中的某些内容?

更新:

缓冲区溢出也发生在 gcc -O1 中。 这是编译警告:

在函数“recv”中, 从 server.c:47:14 的“main”内联: /usr/include/x86_64-linux-gnu/bits/socket2.h:42:2:警告:调用带有属性警告声明的“__recv_chk_warn”:recv 调用的长度大于目标缓冲区的大小[默认启用] return __recv_chk_warn(__fd, __buf, __n, __bos0 (__buf), __flags);

这里是缓冲区溢出:

./server.x 10003 * 检测到缓冲区溢出 *: ./server.x 终止 ======= 回溯:========= /lib/x86_64-linux-gnu/libc.so.6(+0x7338f)[0x7fcbdc44b38f] /lib/x86_64-linux-gnu/libc.so.6(__fortify_fail+0x5c)[0x7fcbdc4e2c9c] /lib/x86_64-linux-gnu/libc.so.6(+0x109b60)[0x7fcbdc4e1b60] /lib/x86_64-linux-gnu/libc.so.6(+0x10a023)[0x7fcbdc4e2023] ./server.x[0x400a6c] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0x7fcbdc3f9ec5] ./server.x[0x400879] ======= 内存映射:======== 00400000-00401000 r-xp 00000000 08:01 17732 > /tmp/server.x ...更多信息在这里 中止(核心转储)

和gcc版本:

gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4

缓冲区和recv调用:

字符缓冲区[4];

n = recv(newsockfd,buffer,255,MSG_TRUNC);

这似乎解决了它:

n = recv(newsockfd,NULL,255,MSG_TRUNC);

这不会产生任何警告或错误:

gcc -Wall -Wextra -pedantic -o server.x server.c

这是完整的代码:

/* A simple server in the internet domain using TCP
   The port number is passed as an argument */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>

void error(const char *msg)
{
    perror(msg);
    exit(1);
}

int main(int argc, char *argv[])
{
     int sockfd, newsockfd, portno;
     socklen_t clilen;
     char buffer[4];
     struct sockaddr_in serv_addr, cli_addr;
     int n;
     if (argc < 2) {
         fprintf(stderr,"ERROR, no port provided\n");
         exit(1);
     }
     sockfd = socket(AF_INET, SOCK_STREAM, 0);
     if (sockfd < 0) 
        error("ERROR opening socket");
     bzero((char *) &serv_addr, sizeof(serv_addr));
     portno = atoi(argv[1]);
     serv_addr.sin_family = AF_INET;
     serv_addr.sin_addr.s_addr = INADDR_ANY;
     serv_addr.sin_port = htons(portno);
     if (bind(sockfd, (struct sockaddr *) &serv_addr,
              sizeof(serv_addr)) < 0) 
              error("ERROR on binding");
     listen(sockfd,5);
     clilen = sizeof(cli_addr);
     newsockfd = accept(sockfd, 
                 (struct sockaddr *) &cli_addr, 
                 &clilen);
     if (newsockfd < 0) 
          error("ERROR on accept");
     bzero(buffer,4);
     n = recv(newsockfd,buffer,255,MSG_TRUNC);
     if (n < 0) error("ERROR reading from socket");
     printf("Here is the message: %s\n",buffer);
     n = write(newsockfd,"I got your message",18);
     if (n < 0) error("ERROR writing to socket");
     close(newsockfd);
     close(sockfd);
     return 0; 
}

更新: 也发生在 Ubuntu 16.04 上,带有 gcc 版本:

gcc (Ubuntu 5.4.0-6ubuntu1~16.04.2) 5.4.0 20160609

【问题讨论】:

  • 什么你得到警告?您是否尝试过启用更多警告(例如-Wall -Wextra -pedantic 或类似的)?并请显示您实际的recv 调用以及缓冲区的定义。
  • 另外,既然它显然可以工作,它可能不是内核的问题,而是编译器的问题,所以你能告诉我们你使用的是哪个版本的GCC吗?您是否尝试过使用更高版本的 GCC?早点?尝试禁用某些特定优化?用-O1 测试过?使用-O1 进行测试,然后启用一个又一个特定的优化选项,直到您遇到问题(这样您就知道是哪一个问题的原因)?
  • 问题不太可能出在编译器上。它可能与 C 库有关(这可能是一个太细的区别),但更可能与 程序 有关。为了有意义地解决这个问题,我们需要可以重现问题的代码。如果您希望我们实际查看此类代码,请以minimal reproducible example 的形式呈现它。
  • 并告诉我们您是如何确定存在缓冲区溢出的。

标签: c linux tcp overflow recv


【解决方案1】:

我想你误会了。

对于数据报套接字,MSG_TRUNC 选项的行为如man 2 recv 手册页中所述(在Linux man pages online 中获取最准确和最新的信息)。

对于 TCP 套接字,man 7 tcp 手册页中的解释有点措辞不当。我相信这不是一个 discard 标志,而是一个 truncate(或 “扔掉其余部分”)操作。但是,实现(特别是 Linux 内核中的 net/ipv4/tcp.c:tcp_recvmsg() 函数处理 TCP/IPv4 和 TCP/IPv6 套接字的详细信息)表明并非如此。

还有一个单独的MSG_TRUNC 套接字标志。这些存储在与套接字关联的错误队列中,可以使用recvmsg(socketfd, &amp;msg, MSG_ERRQUEUE) 读取。它表示读取的数据报比缓冲区长,因此其中一些数据报丢失(截断)。这很少使用,因为它实际上只与数据报套接字相关,并且有更简单的方法来确定超长数据报。


数据报套接字:

使用数据报套接字,消息是分开的,而不是合并的。读取时,每个接收到的数据报的未读部分都会被丢弃。

如果你使用

    nbytes = recv(socketfd, buffer, buffersize, MSG_TRUNC);

这意味着内核将最多复制下一个数据报的第一个buffersize字节,如果数据报更长(像往常一样)则丢弃其余数据报,但nbytes将反映数据报的真实长度.

换句话说,对于MSG_TRUNCnbytes 可能会超过buffersize,即使只有buffersize 字节被复制到buffer


Linux 中的 TCP 套接字,内核 2.4 及更高版本,已编辑:

TCP 连接是流式的;没有“消息”或“消息边界”,只有一个字节序列流动。 (虽然可能有带外数据,但这在这里不相关)。

如果你使用

    nbytes = recv(socketfd, buffer, buffersize, MSG_TRUNC);

内核将丢弃直到下一个buffersize字节,无论已经缓冲(但将阻塞直到至少一个字节被缓冲,除非套接字处于非阻塞模式或使用MSG_TRUNC | MSG_DONTWAIT代替)。丢弃的字节数在nbytes中返回。

但是,bufferbuffersize 都应该是有效的,因为 recv()recvfrom() 调用通过内核 net/socket.c:sys_recvfrom() 函数验证 bufferbuffersize 是否有效,如果因此,在调用上述 net/ipv4/tcp.c:tcp_recvmsg() 之前填充内部迭代器结构以匹配。

换句话说,带有MSG_TRUNC 标志的recv() 实际上并没有尝试修改buffer。但是,内核会检查bufferbuffersize 是否有效,如果无效,将导致recv() 系统调用失败并显示-EFAULT

当启用缓冲区溢出检查时,GCC 和 glibc recv() 不仅仅返回 -1errno==EFAULT;相反,它会停止程序,产生所示的回溯。其中一些检查包括映射零页面(NULL 指针的目标位于 x86 和 x86-64 上的 Linux 中),在这种情况下,内核完成访问检查(在实际尝试读取或写入之前) ) 成功。

为了避免使用 GCC/glibc 包装器(这样使用 gcc 和 clang 编译的代码应该表现相同),可以改用 real_recv()

#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>

ssize_t real_recv(int fd, void *buf, size_t n, int flags)
{
    long retval = syscall(SYS_recvfrom, fd, buf, n, flags, NULL, NULL);
    if (retval < 0) {
        errno = -retval;
        return -1;
    } else
        return (ssize_t)retval;
}

直接调用系统调用。请注意,这不包括 pthreads 取消逻辑;仅在单线程测试程序中使用它。


总之,在使用 TCP 套接字时,关于 recv()MSG_TRUNC 标志的问题,有几个因素使整个情况复杂化:

  • recv(sockfd, data, size, flags) 实际上调用了recvfrom(sockfd, data, size, flags, NULL, NULL) 系统调用(Linux 中没有recv 系统调用)

  • 如果(char *)data+0(char *)data+size-1 是有效的,则recv(sockfd, data, size, MSG_TRUNC) 使用TCP 套接字,就好像它将最多size 字节读入data;它只是不会将它们复制到data。返回这样跳过的字节数。

  • 内核首先验证data(从(char *)data+0(char *)data+size-1,包括)是可读的。 (我怀疑这个检查是错误的,将来可能会变成可写性检查,所以不要把它当作可读性测试。)

  • 缓冲区溢出检查可以检测到来自内核的-EFAULT 结果,而是通过某种“越界”错误消息(带有堆栈跟踪)暂停程序

  • 缓冲区溢出检查可能使NULL 指针从内核的角度看起来是有效的(因为当前内核测试是用于读取),在这种情况下,内核验证接受NULL 指针是有效的。 (可以通过重新编译而不检查缓冲区溢出来验证是否是这种情况,例如使用上面的real_recv(),然后查看NULL 指针是否会导致-EFAULT 结果。)

    这种映射的原因(如果硬件和内核结构允许,则只存在,并且不可读写)是因为有了这样的映射,任何访问都会产生一个SIGBUS信号,它是一个(库或编译器提供的信号处理程序)不仅可以捕获和转储堆栈跟踪,还可以捕获有关确切访问的更多详细信息(地址、尝试访问的代码等)。

    我确实相信内核访问检查将此类映射视为可读和可写,因为需要进行读取或写入尝试才能生成信号。

  • 缓冲区溢出检查由编译器和 C 库完成,因此不同的编译器可能会以不同的方式实现检查,NULL 指针的情况也不同。

【讨论】:

  • 我指的不是数据报,我的问题纯粹是关于 TCP 套接字的。您说内核将最多复制第一个 buffersize 字节,但我认为这不是真的,因为:1)传递空指针和 size > 0 不会导致任何崩溃。 2)在所有其他编译中(没有优化的 gcc 或有/没有优化的 clang) - 当缓冲区大小大于实际缓冲区大小时不会崩溃。调用后缓冲区保持不变 - 表明根本没有复制。
  • @Oasys:没有崩溃不是一个可靠的指标,但保持不变的缓冲区是。让我去阅读内核源代码(如果您跟踪调用链,工作最终在net/ipv4/tcp.c:tcp_recvmsg() 中完成,用于 TCP/IPv4 和 TCP/IPv6 套接字),然后返回我的结果。以上答案凭记忆;来源将产生实际事实。
  • @Oasys:确实,内核不会丢弃 TCP 的尾随数据(截断),只是不会将数据复制到提供的缓冲区中。然而,它确实验证了缓冲区存在。此检查与编译器/c 库提供的数组边界检查交互,并且在编译器之间有所不同。请查看我编辑的答案,如果您能观察到与我上面的解释相矛盾的结果,请告诉我。 (我知道这些东西,但我确实经常犯错误,所以在这里批评一下很好。)
  • 谢谢,这很有帮助。我仍然想不通——如果检查是由内核完成的,那么为什么我会看到编译标志(调试与优化)或 gcc 和 clang 之间的差异?我希望他们都表现得一样,不是吗?我认为当指定 MSG_TRUNC 时,这个检查对于可读性和可写性都是完全错误的。我现在犹豫是否应该依赖当前的 NULL 指针行为...
  • @Oasys:存在差异,因为有两个单独的检查。一是内核总是做的检查(整个缓冲区,直到指定的大小,是有效的);一个是检查编译器(在 C 库的帮助下)添加到您的代码中(对于数组边界和 -EFAULT 返回,取决于编译器选项)。对于 gcc,您可以使用 gcc [options] -Q --help=warnings 查看 [options] 启用的警告。例如,-Wall 启用数组边界检查 (-Warray-bounds)、针对特定参数的 NULL 指针参数检查 (-Wnonnull) 等等。
猜你喜欢
  • 2019-03-19
  • 2012-04-04
  • 2011-07-02
  • 2010-11-26
  • 1970-01-01
  • 1970-01-01
  • 2016-03-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多