我在尝试在TCP套接字上的recv中使用MSG_TRUNC标志时,遇到了令人惊讶的缓冲区溢出。
而且它似乎只发生在gcc(而不是clang)上,并且只在使用优化编译时发生。
根据此链接:http://man7.org/linux/man-pages/man7/tcp.7.html
从版本2.4开始,Linux支持在recv(2)(和recv MSG(2))的flags参数中使用MSG_TRUNC。此标志导致丢弃接收到的数据字节,而不是在调用方提供的缓冲区中传回。自从Linux 2.4.4以来,MSG_PEEK在与MSG_OOB一起使用以接收带外数据时也具有这种效果。
这是否意味着提供的缓冲区将不会写入?我早料到,但很惊讶。
如果传递缓冲区(非零指针)且大小大于缓冲区大小,则当客户端发送大于缓冲区的内容时,将导致缓冲区溢出。如果消息很小并且适合缓冲区(没有溢出),它实际上似乎不会将消息写入缓冲区。
显然,如果传递空指针,问题就消失了。
客户端是一个简单的netcat,它发送的消息大于4个字符。
服务器代码基于:
http://www.linuxhowtos.org/data/6/server.c
将read更改为recv with MSG_TRUNC,将buffer size更改为4(也将bzero更改为4)。
在Ubuntu 14.04上编译。这些编译工作正常(没有警告):
gcc-o服务器.x服务器.c
clang-o服务器.x服务器.c
clang-O2服务器.x服务器.c
这是马车(?)编译时,它还对问题给出了警告提示:
gcc-O2-o服务器.x服务器.c
不管怎样,就像我提到的,将指针改为空可以解决这个问题,但是这是一个已知的问题吗?或者我错过了手册中的一些内容?
更新:
gcc-O1也会发生缓冲区溢出。
以下是编译警告:
在函数“recv”中,
从服务器上的“main”内联。c:47:14:
/usr/include/x86_64-linux-gnu/bits/socket2.h:42:2:警告:调用用属性warning:recv called,其长度大于目标缓冲区的大小[默认启用]
return u recv_chk_warn(u fd,u buf,u n,u bos0(u buf),u标志);
以下是缓冲区溢出:
/服务器x 10003
*检测到缓冲区溢出*:。/server.x已终止
====回溯:=========
/lib/x86_64-linux-gnu/libc.so.6(+0x7338f)[0x7fcbdc44b38f]
/lib/x86_64-linux-gnu/libc.so.6(uu 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(uu libc_start_main+0xf5)[0x7fcbdc3f9ec5]
./server.x[0x400879]
====内存映射:========
00400000-00401000 r-xp 00000000 08:01 17732>/tmp/server.x
... 此处有更多消息
中止(核心转储)
以及gcc版本:
海合会(Ubuntu 4.8.4-2ubuntu1~14.04.3)4.8.4
缓冲区和recv调用:
字符缓冲区[4];
n=recv(newsockfd,缓冲区,255,MSG_TRUNC);
这似乎解决了这个问题:
n=recv(newsockfd,空,255,MSG_TRUNC);
这不会生成任何警告或错误:
gcc-Wall-Wextra-pedantic-o server.x服务器.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;
}

更新:
也出现在Ubuntu16.04上,gcc版本:
海合会(Ubuntu 5.4.0-6ubuntu1~16.04.2)5.4.0 20160609

最佳答案

我想你误解了。
对于数据报套接字,MSG_TRUNC选项的行为如man 2 recv手册页中所述(对于最准确和最新的信息,在Linux man pages online处)。
对于TCP套接字,man 7 tcp手册页中的解释有点措词不当。我相信这不是一个丢弃标志,而是一个截断(或“扔掉其余的”)操作。但是,实现(特别是Linux内核中的net/ipv4/tcp.c:tcp_recvmsg()函数处理TCP/IPv4和TCP/IPv6套接字的详细信息)表明了另一种情况。
还有一个单独的MSG_TRUNC套接字标志。它们存储在与套接字关联的错误队列中,可以使用recvmsg(socketfd, &msg, MSG_ERRQUEUE)读取。它表示读取的数据报比缓冲区长,因此有些数据报丢失(被截断)。这很少使用,因为它实际上只与数据报套接字相关,而且有更容易确定超长数据报的方法。
数据报套接字:
对于数据报套接字,消息是分开的,而不是合并的。读取时,丢弃每个接收数据报的未读部分。
如果你使用

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

这意味着内核将复制到下一个数据报的第一个buffersize字节,如果数据报较长(通常),则丢弃其余的数据报,但nbytes将反映数据报的真实长度。
换句话说,使用MSG_TRUNCnbytes可能会超过buffersize,即使只有多达buffersize个字节被复制到buffer
Linux内核2.4及更高版本中的TCP套接字,已编辑:
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()之前填充要匹配的内部迭代器结构。
换句话说,带有recv()标志的MSG_TRUNC实际上并不试图修改buffer。但是,内核会检查bufferbuffersize是否有效,如果无效,则会导致recv()系统调用因-EFAULT而失败。
当启用缓冲区溢出检查时,GCC和glibcrecv()不只是返回带有-1errno==EFAULT;而是停止程序,生成所示的回溯。其中一些检查包括映射零页(其中NULL指针的目标位于x86和x86-64上的Linux中),在这种情况下,内核执行的访问检查(在实际尝试读取或写入它之前)成功。
为了避免g cc/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套接字时,关于MSG_TRUNCrecv()标志的问题,有几个因素使整个情况复杂化:
recv(sockfd, data, size, flags)实际上调用了系统调用(Linux中没有系统调用)
如果recvfrom(sockfd, data, size, flags, NULL, NULL)recv是有效的,那么对于TCP套接字,recv(sockfd, data, size, MSG_TRUNC)的作用就好像它要将size字节读入data中一样;它只是不将它们复制到(char *)data+0中。返回跳过的字节数。
内核首先验证(char *)data+size-1(从datadata,包括)是否可读。(我怀疑这个检查是错误的,将来可能会变成一个可写性检查,所以不要指望这是一个可读性测试。)
缓冲区溢出检查可以检测到内核的(char *)data+0结果,而是用某种“越界”错误消息(带有堆栈跟踪)停止程序
缓冲区溢出检查可能会使(char *)data+size-1指针从内核的角度看似乎是有效的(因为内核测试当前是用于读取的),在这种情况下,内核验证接受-EFAULT指针为有效的。(可以通过重新编译而不进行缓冲区溢出检查(例如,使用上面的NULL,然后查看NULL指针是否导致real_recv()结果)来验证是否存在这种情况。)
这种映射的原因(如果硬件和内核结构允许的话,仅存在,并且是不可读或可写的)是这样的映射,任何访问生成一个NULL信号,其中一个(库或编译器提供的信号处理程序)可以捕获,并且不只是堆栈跟踪转储,而是关于确切访问的更多细节(地址,尝试访问的代码,等等)。
我相信内核访问检查处理这种映射是可读和可写的,因为要生成信号,需要进行读或写尝试。
缓冲区溢出检查是由编译器和C库共同完成的,因此不同的编译器可能会以不同的方式实现检查和指针大小写。

关于c - 带有MSG_TRUNC的Linux TCP recv()-写入缓冲区?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/39214445/

10-13 07:23