我在尝试在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_TRUNC
,nbytes
可能会超过buffersize
,即使只有多达buffersize
个字节被复制到buffer
。Linux内核2.4及更高版本中的TCP套接字,已编辑:
TCP连接是流式的;没有“消息”或“消息边界”,只有一个字节流序列。(虽然,可能有带外数据,但这与此无关)。
如果你使用
nbytes = recv(socketfd, buffer, buffersize, MSG_TRUNC);
内核将丢弃下一个
buffersize
字节,不管这些字节已经被缓冲(但将阻塞,直到至少一个字节被缓冲,除非套接字处于非阻塞模式或使用MSG_TRUNC | MSG_DONTWAIT
)。丢弃的字节数在nbytes
中返回。但是,
buffer
和buffersize
都应该是有效的,因为recv()
或recvfrom()
调用通过内核net/socket.c:sys_recvfrom()函数,该函数验证buffer
和buffersize
是否有效,如果是,则在调用上述net/ipv4/tcp.c:tcp_recvmsg()
之前填充要匹配的内部迭代器结构。换句话说,带有
recv()
标志的MSG_TRUNC
实际上并不试图修改buffer
。但是,内核会检查buffer
和buffersize
是否有效,如果无效,则会导致recv()
系统调用因-EFAULT
而失败。当启用缓冲区溢出检查时,GCC和glibc
recv()
不只是返回带有-1
的errno==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_TRUNC
的recv()
标志的问题,有几个因素使整个情况复杂化: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
(从data
到data
,包括)是否可读。(我怀疑这个检查是错误的,将来可能会变成一个可写性检查,所以不要指望这是一个可读性测试。)缓冲区溢出检查可以检测到内核的
(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/