Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Illegal seek" error when working with socket streams with non-empty read buffers

I'm currently writing a server application on Linux x86_64 using <sys/socket.h>. After accepting a connection via accept(), I use fdopen() to wrap the retrieved socket into a FILE* stream.

Writing to, and reading from, that FILE* stream usually works quite well, but the socket becomes unsusable as soon as I write to it while it has a non-empty read buffer.

For demonstration purposes, I've written some code that listens for a connection, then reads the input, line by line, into a read buffer using fgetc(). If the line is too long to fit into the buffer, it's not completely read, but instead read during the next iteration.

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

FILE* listen_on_port(unsigned short port) {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        struct sockaddr_in name;
        name.sin_family = AF_INET;
        name.sin_port = htons(port);
        name.sin_addr.s_addr = htonl(INADDR_ANY);
        if(bind(sock, (struct sockaddr*) &name, sizeof(name)) < 0)
                perror("bind failed");
        listen(sock, 5);
        int newsock = accept(sock, 0, 0);
        return fdopen(newsock, "r+");
}

int main(int argc, char** argv) {
        int bufsize = 8;
        char buf[9];
        buf[8] = 0; //ensure null termination

        int data;
        int size;

        //listen on the port specified in argv[1]
        FILE* sock = listen_on_port(atoi(argv[1]));
        puts("New connection incoming");

        while(1) {
                //read a single line
                for(size = 0; size < bufsize; size++) {
                        data = fgetc(sock);
                        if(data == EOF)
                                break;
                        if(data == '\n') {
                                buf[size] = 0;
                                break;
                        }
                        buf[size] = (char) data;
                }

                //check if the read failed due to an EOF
                if(data == EOF) {
                        perror("EOF: Connection reset by peer");
                        break;
                } else {
                        printf("Input line: '%s'\n", buf);
                }

                //try to write ack
                if(fputs("ack\n", sock) == EOF)
                        perror("sending 'ack' failed"); 

                //try to flush
                if(fflush(sock) == EOF)
                        perror("fflush failed");        
        }

        puts("Connection closed");
}

The code should compile in gcc without any special parameters. Run it with the port number as argument and use netcat to connect to it locally.

Now, if you try sending strings that are shorter than 8 characters, this will run flawlessly. But if you send a string containing more than 10 characters, the program will fail. This sample input:

ab
cd
abcdefghij

Will create this output:

New connection incoming
Input line: 'ab'
Input line: 'cd'
Input line: 'abcdefgh'
fflush failed: Illegal seek
EOF: Connection reset by peer: Illegal seek
Connection closed

As you see, (rightly) only the first 8 characters of abcdefgh are read, but when the program tries to send the 'ack' string (which the client never receves), and then flush the output buffer, we receive an Illegal seek error, and the next call to fgetc() returns EOF.

If the fflush() part is commented out, the same error still occurs, but the

fflush failed: Illegal seek

line is missing from the server output.

If the fputs(ack) part is commented out, everything seems to work as intended, but a perror() manually called from gdb still reports an 'Illegal seek' error.

If both fputs(ack) and fflush() are commented out, everything does work as intended.

Unfortunately, I've not been able to find any good documentation, nor any Internet discussions on this problem, so your help would be greatly appreciated.

edit

The solution i finally settled for is to not use fdopen() and FILE*, since there seems to be no clean way of converting a socket fd into a FILE* that can reliably used in r+ mode. Instead, I directly worked on the socket fd, writing my own replacement code for fputs and fprintf.

If anyone needs it, here is the code.

like image 903
mic_e Avatar asked Apr 21 '12 02:04

mic_e


1 Answers

Clearly "r+" (read/write) mode does not work on sockets in this implementation, no doubt because the underlying code assumes that it must do a seek to switch between reading and writing. This is the general case with stdio streams (that you must do some kind of synchronizing operation), because back in the Dim Time, actual stdio implementations had only a single counter per stream, and it was either a counter of "number of characters left to read from stream buffer via getc macro" (in read mode) or "number of characters that can safely be written to stream buffer via putc macro (in write mode). To that that single counter re-set, you had to do a seek-type operation.

Seeks are not allowed on pipes and sockets (since "file offset" is not meaningful there).

One solution is not to wrap a socket with stdio at all. Another, probably easier / better for your purposes, is to wrap it with, not one, but two stdio streams:

FILE *in = fdopen(newsock, "r");
FILE *out = fdopen(newsock, "w");

There's another flaw here though, because when you go to fclose one stream, that closes the other's file descriptor. To work around that, you need to dup the socket descriptor once (in either of the two calls above, it does not matter which one).

If you intend to use select or poll or similar on the socket at some point, you should generally go for the "don't wrap with stdio" solution, since there's no nice clean portable way to track stdio buffering. (There are implementation-specific ways).

like image 174
torek Avatar answered Oct 08 '22 15:10

torek