[ Team LiB ] Previous Section Next Section

16.6 Nonblocking accept

We stated in Chapter 6 that a listening socket is returned as readable by select when a completed connection is ready to be accepted. Therefore, if we are using select to wait for incoming connections, we should not need to set the listening socket to nonblocking because if select tells us that the connection is ready, accept should not block.

Unfortunately, there is a timing problem that can trip us up here [Gierth 1996]. To see this problem, we modify our TCP echo client (Figure 5.4) to establish the connection and then send an RST to the server. Figure 16.21 shows this new version.

Figure 16.21 TCP echo client that creates connection and sends an RST.


 1 #include     "unp.h"

 2 int
 3 main(int argc, char **argv)
 4 {
 5     int     sockfd;
 6     struct linger ling;
 7     struct sockaddr_in servaddr;

 8     if (argc != 2)
 9         err_quit("usage: tcpcli <IPaddress>");

10     sockfd = Socket(AF_INET, SOCK_STREAM, 0);

11     bzero(&servaddr, sizeof(servaddr));
12     servaddr.sin_family = AF_INET;
13     servaddr.sin_port = htons(SERV_PORT);
14     Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

15     Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

16     ling.l_onoff = 1;          /* cause RST to be sent on close() */
17     ling.l_linger = 0;
18     Setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
19     Close(sockfd);

20     exit(0);
21 }

Set SO_LINGER socket option

1619 Once the connection is established, we set the SO_LINGER socket option, setting the l_onoff flag to 1 and the l_linger time to 0. As stated in Section 7.5, this causes an RST to be sent on a TCP socket when the connection is closed. We then close the socket.

Next, we modify our TCP server from Figures 6.21 and 6.22 to pause after select returns that the listening socket is readable, but before calling accept. In the following code from the beginning of Figure 6.22, the two lines preceded by a plus sign are new:

     if (FD_ISSET(listenfd, &rset)) {    /* new client connection */
+        printf("listening socket readable\n");
+        sleep(5);
         clilen = sizeof(cliaddr);
         connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);

What we are simulating here is a busy server that cannot call accept as soon as select returns that the listening socket is readable. Normally, this slowness on the part of the server is not a problem (indeed, this is why a queue of completed connections is maintained), but when combined with the RST from the client, after the connection is established, we can have a problem.

In Section 5.11, we noted that when the client aborts the connection before the server calls accept, Berkeley-derived implementations do not return the aborted connection to the server, while other implementations should return ECONNABORTED but often return EPROTO instead. Consider the following example of a Berkeley-derived implementation:

  • The client establishes the connection and then aborts it as in Figure 16.21.

  • select returns readable to the server process, but it takes the server a short time to call accept.

  • Between the server's return from select and its calling accept, the RST is received from the client.

  • The completed connection is removed from the queue and we assume that no other completed connections exist.

  • The server calls accept, but since there are no completed connections, it blocks.

The server will remain blocked in the call to accept until some other client establishes a connection. But in the meantime, assuming a server like Figure 6.22, the server is blocked in the call to accept and will not handle any other ready descriptors.

This problem is somewhat similar to the denial-of-service attack described in Section 6.8, but with this new bug, the server breaks out of the blocked accept as soon as another client establishes a connection.

The fix for this problem is as follows:

  1. Always set a listening socket to nonblocking when you use select to indicate when a connection is ready to be accepted.

  2. Ignore the following errors on the subsequent call to accept: EWOULDBLOCK (for Berkeley-derived implementations, when the client aborts the connection), ECONNABORTED (for POSIX implementations, when the client aborts the connection), EPROTO (for SVR4 implementations, when the client aborts the connection), and EINTR (if signals are being caught).

    [ Team LiB ] Previous Section Next Section