2016-08-29 5 views
0

Я только что столкнулся с неожиданным переполнением буфера, пытаясь использовать флаг MSG_TRUNC в recv на сокете TCP.Linux TCP recv() с MSG_TRUNC - записывает в буфер?

И, похоже, это происходит только с gcc (не clang) и только при компиляции с оптимизацией.

По этой ссылке: http://man7.org/linux/man-pages/man7/tcp.7.html

Начиная с версии 2.4, Linux поддерживает использование MSG_TRUNC в флагов аргумента RECV (2) (и recvmsg (2)). Этот флаг заставляет принятые байты данных отбрасываться, а не передаваться обратно в буфере, предоставляемом вызывающим абонентом. Начиная с Linux 2.4.4, MSG_PEEK также имеет этот эффект при использовании совместно с MSG_OOB для приема внеполосных данных.

Означает ли это, что поставляемый буфер не будет записан? Я так и ожидал, но был удивлен. Если вы передаете буфер (ненулевой указатель) и размер больше размера буфера, это приводит к переполнению буфера, когда клиент отправляет нечто большее, чем буфер. На самом деле это не означает, что сообщение записывается в буфер, если сообщение мало и помещается в буфер (без переполнения). Видимо, если вы передадите нулевой указатель, проблема исчезнет.

Клиент - это простой netcat, отправляющий сообщение размером более 4 символов.

код сервера основан на: http://www.linuxhowtos.org/data/6/server.c

Измененное чтения, чтобы Recv с MSG_TRUNC, и размер буфера до 4 (bzero до 4, а).

Составлено на Ubuntu 14.04. Эти сборники не работают отлично (без предупреждения):

Gcc -o server.x server.c

лязг -o server.x server.c

лязг -O2 server.x server.c (?)

Это багги сборник, он также дает предупреждение намекая о проблеме:

НКУ -O 2 -o server.x server.c

В любом случае, как я уже говорил, изменение указателя на нуль устраняет проблему, но является ли это известной проблемой? Или я пропустил что-то на странице руководства?

UPDATE:

Переполнение буфера происходит также с помощью GCC -O1. Вот предупреждение компиляции:

В функции «RECV», встраиваемое от «Главного» в server.c: 47: 14: /USR/включать/x86_64-Linux-ГНУ/биты/socket2.h: 42: 2: предупреждение: вызов «__recv_chk_warn» с предупреждением атрибута: recv вызывается с большей длиной, чем размер целевого буфера [включен по умолчанию] return __recv_chk_warn (__fd, __buf, __n, __bos0 (__buf), __flags) ;

Здесь переполнение буфера:

./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 г-хр 00000000 8:01 17732> /tmp/server.x ... больше сообщений здесь Прерванный (ядро сбрасывали)

И НКУ версия:

НКУ (Ubuntu 4.8.4-2ubuntu1 ~ 14.04.3) 4.8.4

Буфер и RECV вызова:

символ буфера [4];

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

И это, кажется, это исправить:

п = ПРИЕМ (newsockfd, NULL, 255, MSG_TRUNC);

Это не будет создавать каких-либо предупреждений или ошибок:

НКУ -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; 
} 

UPDATE: Случается также на Ubuntu 16.04, с GCC версии:

НКУ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.2) 5.4.0 20160609

+0

* Какие * предупреждения вы получаете? Вы пытались включить еще больше предупреждений (например, «-Wall -Wextra -pedantic» или аналогичный)? И, пожалуйста, покажите свой фактический вызов 'recv' вместе с определением буфера. –

+0

Кроме того, поскольку он, очевидно, работает, это, вероятно, не проблема с ядром, но с * компилятором *, поэтому можете ли вы рассказать нам, какую версию GCC вы используете? И вы пробовали с более поздними версиями GCC? Ранее? Попробовал отключить некоторые определенные оптимизации? Протестировано с помощью '-O1'? Протестировано с помощью «-O1», а затем включите один вариант оптимизации после другого, пока не получите проблему (так что вы знаете, какая причина является причиной)? –

+1

Проблема вряд ли будет с компилятором. Это может быть с библиотекой C (что, возможно, слишком тонкое различие), но это скорее с * программой *. Чтобы осмысленно решить этот вопрос, нам нужен код, с помощью которого мы можем воспроизвести проблему. Если вы хотите, чтобы мы действительно смотрели на такой код, то представляйте его в виде [mcve]. –

ответ

1

Я думаю, вы не поняли.

С сокетами дейтаграмм, MSG_TRUNC вариант ведет себя так, как описано в справочной странице man 2 recv (на Linux man pages online для получения наиболее точной и актуальной информации).

С помощью сокетов TCP объяснение в справочной странице man 7 tcp немного слабо сформулировано. Я считал, что это не сбросить флаг, но усекать (или «выбросить все остальное»). Однако реализация (в частности, функция net/ipv4/tcp.c:tcp_recvmsg() в ядре Linux обрабатывает детали для сокетов TCP/IPv4 и TCP/IPv6) указывает иначе.

Существует также отдельный флаг гнезда MSG_TRUNC. Они сохраняются в очереди , связанной с сокетом, и могут быть прочитаны с использованием recvmsg(socketfd, &msg, MSG_ERRQUEUE). Он указывает, что датаграмма, которая была прочитана, была длиннее, чем буфер, поэтому некоторые из них были потеряны (усечены). Это редко используется, потому что это действительно относится только к сокетам дейтаграмм, и есть намного более простые способы определения датаграмм перекрытий.


Datagram розетка:

С датаграммами сокетами, сообщения разделены, и не объединены. При чтении непрочитанная часть каждой принятой дейтаграммы отбрасывается.

Если вы используете

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

это означает, что ядро ​​будет копировать до первых buffersize байт следующей дейтаграммы и отбрасывать оставшуюся часть дейтаграммы, если он больше (как обычно), но nbytes будет отражают истинную длину дейтаграммы.

Другими словами, с MSG_TRUNC, nbytes может превышать buffersize, даже если только до buffersize байт копируются buffer.


TCP сокеты в Linux, ядро ​​2.4 и более поздние версии, отредактировано:

соединения 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 находится в Linux на x86 и x86-64), и в этом случае проверка доступа, выполняемая ядром (перед тем, как на самом деле пытаться ее прочитать или записать) ,

Чтобы избежать/GLibC обертки GCC (так что код составлен, например, с помощью GCC и звоном должны вести себя так же), можно использовать вместо 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; используйте это только в однопоточных тестовых программах.


В целом, с заявленной проблемой в отношении MSG_TRUNC флаг recv() при использовании TCP сокетов, не существует несколько факторов, затрудняющих полную картину:

  • recv(sockfd, data, size, flags) фактически вызывает recvfrom(sockfd, data, size, flags, NULL, NULL) системный вызов (там нет recv syscall в Linux)

  • С сокетом 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 указателя вызывает -EFAULT результата тогда.)

    Причины такого отображения (что, если это разрешено по аппаратным средствам и структурам ядра, существует и не читается или записывается) заключается в том, что при таком сопоставлении любой доступ генерирует сигнал SIGBUS, который может (ловушка для обработки данных или компилятора) может вылавливать и сбрасывать не только стек, но более подробную информацию о точном доступе (адрес, код, который пытался получить доступ, и т. д.).

    Я считаю, что проверка доступа к ядру рассматривает такие отображения, которые можно читать и записывать, потому что для генерируемого сигнала должна быть попытка чтения или записи.

  • Проверка переполнения буфера выполняется как компилятором, так и библиотекой C, поэтому разные компиляторы могут реализовывать проверки и код указателя NULL по-разному.

+0

Я не имел в виду дейтаграммы, мой вопрос носит исключительно на сокетах TCP. Вы говорите, что ядро ​​скопирует до первых байт буферов, но я не думаю, что это правда, потому что: 1) Передача нулевого указателя и размера> 0 не приводит к сбою. 2) Во всех других компиляциях (gcc без оптимизации или clang с/без оптимизации) - нет сбоя, когда буферизация больше фактического размера буфера. И после того, как звонок не будет сохранен, буфер останется без изменений. – Oasys

+0

@Oasys: Отсутствие сбоя не является надежным индикатором, но остается неизменным буфер. Позвольте мне прочитать исходный код ядра (работа завершается в [net/ipv4/tcp.c: tcp_recvmsg()] (https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux .git/tree/net/ipv4/tcp.C# n1581) для сокетов TCP/IPv4 и TCP/IPv6, если вы отслеживаете цепочку вызовов) и верните мои результаты. Вышеприведенный ответ - из памяти; источник даст фактические факты. –

+0

@Oasys: Действительно, ядро ​​не отбрасывает конечные данные (обрезает) для TCP, а просто не копирует данные в предоставленный буфер. Однако он подтверждает, что буфер * существует *. Эта проверка взаимодействует с проверками границ массива компилятора/c-библиотеки и варьируется между компиляторами. Пожалуйста, см. Мой отредактированный ответ, и дайте мне знать, если вы можете наблюдать результаты, противоречащие моему объяснению выше. (Я знаю это, но часто делаю ошибки, так что здесь очень важно.) –