2016-08-29 89 views
0

我剛剛遇到一個令人驚訝的緩衝區溢出,同時嘗試在TCP套接字的recv上使用標誌MSG_TRUNC。帶有MSG_TRUNC的Linux TCP recv() - 寫入緩衝區?

它似乎只發生在gcc(不是鐺聲),只有在編譯優化時纔會發生。

根據該鏈接:http://man7.org/linux/man-pages/man7/tcp.7.html

由於2.4版本,Linux支持在的recv的flags參數使用MSG_TRUNC的(2)(和recvmsg(2))。該標誌會導致接收到的數據字節被丟棄,而不是通過調用者提供的緩衝區傳回。從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

鐺-o server.x server.c

鐺-02 server.x server.c (?)

這是越野車編制,這也給了一個警告,暗示這個問題:

GCC -O 2 -o server.x server.c

無論如何,就像我提到的將指針改爲null可修復問題,但這是一個已知問題嗎?或者我錯過了手冊頁中的某些內容?

UPDATE:

緩衝區溢出也恰好用gcc -O1。 這裏是編譯警告:

在函數「的recv」, 在server.c從「主」內聯:47:14: 的/ usr /包括/ x86_64的-Linux的GNU /比特/ SOCKET2。h:42:2:警告:調用'__recv_chk_warn'聲明屬性警告:recv調用的長度大於目標緩衝區的大小[缺省情況下啓用] return __recv_chk_warn(__fd,__buf,__n,__bos0(__buf),__flags) ;

這裏是緩衝區溢出:

./server.x 10003 *緩衝區溢出檢測*:./server.x終止 =======回溯:= ======== /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] ======= Memory圖:====== 00400000-00401000 R-XP 00000000 08:01 17732> /tmp/server.x ...更多信息這裏 中止(核心轉儲)

和GCC版本:

GCC(Ubuntu的4.8.4-2ubuntu1〜14.04.3)4.8.4

緩衝區和接收電話:

char buffer [4];

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

這似乎解決它:

N =的recv(newsockfd,NULL,255,MSG_TRUNC);

這不會產生任何警告或錯誤:

的gcc -Wall -pedantic -Wextra -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版本:

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手冊頁中的解釋有點措辭不良。我相信這不是一個丟棄標誌,而是一個截斷(或「扔掉其餘的」)操作。但是,實現(特別是,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中


TCP套接字,內核2.4和以後,編輯:

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()

換句話說,帶有MSG_TRUNC標誌的recv()實際上並未嘗試修改buffer。但是,內核會檢查bufferbuffersize是否有效,如果不是,將導致recv()系統調用失敗,並顯示-EFAULT

當啓用緩衝區溢出檢查時,GCC和glibc recv()不只是返回-1errno==EFAULT;它會停止程序,產生顯示的回溯。其中一些檢查包括映射零頁(指針的目標位於Linux和x86-64上的Linux),在這種情況下,內核完成的訪問檢查(在實際嘗試讀取或寫入之前)成功。

爲了避免GCC/glibc的包裝(因此與例如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取消邏輯;僅在單線程測試程序中使用它。


總之,使用TCP套接字時就MSG_TRUNC標誌recv()了上述問題,有幾個因素的全貌複雜:

  • recv(sockfd, data, size, flags)實際調用recvfrom(sockfd, data, size, flags, NULL, NULL)系統調用(有沒有recv系統調用在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結果然後的情況。)

    的原因這樣的映射(即,如果允許由硬件和內核結構,只存在,並且不可讀或不可寫)是,通過這種映射,任何訪問都會生成一個信號,一個(庫或編譯器提供的信號處理程序)可以捕獲並轉儲,而不僅僅是堆棧跟蹤,但有關確切訪問(地址,嘗試訪問的代碼等)的更多詳細信息。

    我確實認爲內核訪問檢查將這種映射視爲可讀可寫,因爲需要對要生成的信號進行讀取或寫入嘗試。

  • 緩衝區溢出檢查是由編譯器和C庫完成的,所以不同的編譯器可以實現檢查和NULL指針的情況。

+0

我不是指數據報,我的問題純粹是在TCP套接字上。你說內核將複製到第一個緩衝區大小字節,但我不認爲這是真的,因爲:1)傳遞空指針和大小> 0不會導致任何崩潰。 2)在所有其他編譯(gcc沒有優化或clang有/沒有優化) - 當buffersize大於實際緩衝區大小時沒有崩潰。調用後緩衝區保持不變 - 表示完全沒有進行復制。 – Oasys

+0

@Oasys:崩潰不是一個可靠的指標,但緩衝區保持不變。讓我去讀取內核源(工作結束於正在做的[網/支持IPv4/tcp.c:tcp_recvmsg()](https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux git的/樹/網/的IPv4/tcp.c#n1581)爲TCP/IPv4和TCP/IPv6的插座,如果您跟蹤調用鏈),與我的結果回來。以上答案來自內存;來源將產生實際的事實。 –

+0

@Oasys:的確,內核不用於TCP扔掉尾隨數據(截),但根本不會將數據複製到提供的緩衝區。但是,它會確認緩衝區*是否存在*。此檢查與編譯器/ C庫提供的數組邊界檢查交互,並且在編譯器之間有所不同。請看我編輯的答案,並讓我知道你是否可以觀察到與我上面的解釋相矛盾的結果。 (我知道這個東西,但我經常犯錯誤,所以在這裏很重要) –