我在尝试在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
答案 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
中返回。
但是,buffer
和buffersize
都应该有效,因为recv()
或recvfrom()
调用会通过内核net/socket.c:sys_recvfrom()函数来验证buffer
1}}和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中),在这种情况下,内核完成访问检查(在实际尝试读取或写入之前)它成功了。
为了避免使用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)
改为size
,data
就会将(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
指针的情况。