Restriction of C standard I/O and why we can't use C standard I/O with sockets

354 views Asked by At

I am reading CSAPP recently. In section 10.9, it said that standard I/O should not be used with socket because of the reasons as follows:

(1) The restrictions of standard I/O

Restriction 1: Input functions following output functions. An input function cannot follow an output function without an intervening call to fflush, fseek, fsetpos, or rewind. The fflush function empties the buffer associated with a stream. The latter three functions use the Unix I/O lseek function to reset the current file position.

Restriction 2: Output functions following input functions. An output function cannot follow an input function without an intervening call to fseek, fsetpos, or rewind, unless the input function encounters an end-of-file.

(2) It is illegal to use the lseek function on a socket.

Question 1: What would happen if I violate the restriction? I wrote a code snippet and it works fine.

Question 2: To walk around restriction 2, one approach is as follows:

File *fpin, *fpout;

fpin = fdopen(sockfd, "r");
fpout = fdopen(sockfd, "w");

/* Some Work Here */

fclose(fpin);
fclose(fpout);

In the text book, it said,

Closing an already closed descriptor in a threaded program is a recipe for disaster.

Why?

1

There are 1 answers

1
R.. GitHub STOP HELPING ICE On BEST ANSWER

Your workaround does not work as written, due to the double-close bug you cited. Double-close is harmless in single-threaded programs as long as there are no intervening operations which could open new file descriptors (the second close will just fail harmlessly with EBADF) but they are critical bugs in multi-threaded programs. Consider this scenario:

  • Thread A calls close(n).
  • Thread B calls open and it returns n which gets stored as int fd1.
  • Thread A calls close(n) again.
  • Thread B calls open again and it returns n again, which gets stored as fd2.
  • Thread B now attempts to write to fd1 and actually writes into the file opened by the second call to open instead of the one first opened.

This can lead to massive file corruption, information leak (imagine writing a password to a socket instead of a local file), etc.

However, the problem is easy to fix. Instead of calling fdopen twice with the same file descriptor, simply use dup to copy it and pass the copy to fdopen. With this simple fix, stdio is perfectly usable with sockets. It's not suitable for asynchronous event loop usage still, but if you're using threads for IO, it works great.

Edit: I think I skipped answering your question 1. What happens if you violate the rules about how to switch between input and output on a stdio stream is undefined behavior. This means testing it and seeing that it "works" is not meaningful; it could mean either:

  1. The C implementation you're using provides a definition (as part of its documentation) for what happens in this case, and it matches the behavior you wanted. In this case, you can use it, but your code will not be portable to other implementations. Doing so is considered very bad practice for this reason. Or,

  2. You just got the result you expected by chance, usually as a side effect of how the relevant functionality is implemented internally on the implementation you're using. In this case, there's no guarantee that it doesn't have corner cases that fail to behave as you expected, or that it will continue to work the same way in future releases, etc.