带有MSG_TRUNC的Linux TCP recv() - 写入缓冲区?

时间:2016-08-29 20:13:54

标签: c linux tcp overflow recv

我在尝试在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,__,__bos0(__ buf),__ flag);

这是缓冲区溢出:

  

./ server.x 10003   检测到 *缓冲区溢出* :./ server.x已终止   ======= Backtrace:=========   /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调用:

  

char buffer [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

1 个答案:

答案 0 :(得分:2)

我认为你误解了。

使用数据报套接字,MSG_TRUNC选项的行为与man 2 recv手册页(Linux man pages online中所述)的行为相同,以获取最准确和最新的信息。

使用TCP套接字时,man 7 tcp手册页中的说明措辞有点差。我认为它不是 discard 标志,而是 truncate (或&#34;抛弃其余的&#34; )操作。但是,实现(特别是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_TRUNC时,nbytes可能会超过buffersize,即使最多只有buffersize个字节被复制到buffer

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

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

如果您使用

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

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

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

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

当启用缓冲区溢出检查时,GCC和glibc recv()不仅仅返回-1 errno==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套接字时MSG_TRUNC recv()的{​​{1}}标志所述的问题,有几个因素使整个图片变得复杂:

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

  • 使用TCP套接字时,如果recv(sockfd, data, size, MSG_TRUNC)改为sizedata就会将(char *)data+0字节读取到(char *)data+size-1。有效;它只是不会将它们复制到data。返回如此跳过的字节数。

  • 首先,内核验证data(从(char *)data+0(char *)data+size-1,包括在内)是可读的。 (我怀疑这个检查是错误的,并且可能在将来某个时候变成可写性检查,所以不要依赖于这是一个可读性测试。)

  • 缓冲区溢出检查可以检测内核的-EFAULT结果,而是使用某种&#34;超出界限&#34; 错误消息来暂停程序(带有堆栈跟踪)

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

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

    我确实相信内核访问检查会将这样的映射视为可读写,因为需要对要生成的信号进行读写尝试。

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