2
votes

I'm trying to use the c library function fputc from stdio.h

I'm assuming it should work according to the spec at https://linux.die.net/man/3/fputc

Specifically, the parts that are of interest are:

  • fputc() writes the character c, cast to an unsigned char, to stream.
  • fputc(), putc() and putchar() return the character written as an unsigned char cast to an int or EOF on error.

Based on this information, I assume that if fputc successfully writes the character to the stream provided, I should receive a return value equal to the character written, and if it fails to write to the stream, I should get the value of EOF.

Here is my code:

// COMPILE
//    gcc -Wall -Wextra -Werror -O3 -s ./fputc.c -o ./fputc-test
// RUN
//    ./fputc-test

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

void print_error_exit();

int main() {
    FILE * fp;
    
    // ensure file exists
    if((fp = fopen("file", "w")) == NULL) print_error_exit();
    
    // close stream
    if(fclose(fp) == EOF) print_error_exit();
    
    // open file in read-only mode
    if((fp = fopen("file", "r")) == NULL) print_error_exit();
    
    // close stream
    if(fclose(fp) == EOF) print_error_exit();
    
    printf("EOF is: %d\n", EOF);
    // use fputc on a read-only closed stream (should return error, right?)
    printf("fputc returned: %d\n", fputc(97, fp)); // 97 is ascii for 'a'
    // expected:
    //    prints EOF (probably -1)
    // actual:
    //    prints 97 on linux with both gcc and clang (multiple versions tested)
    //    prints -1 on windows with mingw-gcc
    
    return EXIT_SUCCESS;
}

void print_error_exit() {
    fprintf(stderr, "%s\n", strerror(errno));
    exit(EXIT_FAILURE);
}

I have tested the code on Ubuntu 20, Debian 9, Windows 10, using gcc 8.1.0, gcc 8.3.0, gcc 9.3.0, and clang 7.0.1. On windows, I've used mingw.

The only trend I found is that fputc returns what I would expect it to on windows, and does not return what I expect it to on linux.

Which one of the following is correct?

  • There is a bug in my code (if there is, explain why and post fixed code please)
  • I did not understand the spec correctly (if so, please explain it better)
  • There is a bug in both gcc and clang when compiled for linux (where to report this?)
  • There is a bug with linux (some distros or all) (where to report this?)

Please help me understand this. Why does fputc not return an error code (EOF) when I try to use it on a closed stream, let alone, a stream that was opened only for reading?

2
You are calling fputs after closing the file. Isn't that undefined behavior?KamilCuk
@KamilCuk I see no place in the spec that says this is undefined behavior, and my base assumption is that this should be a defined error. Based on what you're saying, my assumption is wrong? In which case, where in the spec can I see this?Aqo
where in the spec Note that the amazing and great linux man-pages project is, well, great and amazing, but it's not a "specification", it's rather "documentation". (and note that linux.die.net are archival pages, prefer man7 pages, they are more up-to-date). The specification is C standard, and it's typical to use free draft of the standard.KamilCuk
I will opt to prefer man7, thank you. Nonetheless, even that one does not state this is undefined behavior, but you linked the relevant part in your answer, so I will upvote that, as it answers exactly what I wanted. Thank you!Aqo

2 Answers

8
votes

Your code exhibits undefined behavior.

From J.2 Undefined behavior:

The behavior is undefined in the following circumstances:

The value of a pointer to a FILE object is used after the associated file is closed (7.21.3).

After you close the file, it's invalid to use the FILE object, and the pointer itself has indeterminate value.

6
votes

C17 7.21.3 (4):

The value of a pointer to a FILE object is indeterminate after the associated file is closed.

So in your fputc call, fp has an indeterminate value. The standard only defines a behavior for fputc when fp points to an output stream, and we cannot say this is the case, so the behavior is undefined.

The text is a little misleading because on a typical system, the value of the pointer does not change when passed to fclose; after all, C passes arguments by value, so FILE *fp; ...; fclose(fp); couldn't change the value of your fp variable even if it wanted to. It still points to the same address as it always did. But the data located at that address can certainly become indeterminate, and need not make sense for the system to interpret as a stream anymore.


Here is what is happening under the hood for Linux. It should go without saying that these are all implementation details and you should not rely on them in any program.

You can see here what Linux's fputc actually does. The fp parameter points to a FILE object which contains a pointer to a buffer and numbers that indicate how much space remains. If there is space in the buffer, the character is written there; there is no way for this to fail. Only if the buffer becomes full and data needs to be written out via the OS is there a possibility for fputc to return an error.

Now when you fclose the file, the buffer and FILE object are simply deallocated, with free() or a similar mechanism. If nobody else has allocated any memory in the meantime, the contents of those objects might still be in memory and unchanged by the time you call fputc. Those objects are not flagged as invalid in any way before doing so, because nobody would ever see the flag unless they were accessing freed memory, and no correct program should ever do that. So when you call fputc, the contents of the memory pointed to by fp still look exactly like a valid FILE object, with a buffer that is not full, so fp writes the character there and returns success - because after all, writing a character into a buffer cannot fail. But in fact you have now written into freed memory, and all sorts of trouble may potentially result.

So it's similar to "use-after-free" bugs with malloc: the system trusts you not to use the resource after you have released it, but does not promise to catch you if you do so anyway.

The other systems you tested most likely have some sort of "invalid" flag in the FILE object which they set before deallocating it, and their fputc likely tests this flag. So if the FILE object has not been overwritten with other stuff, the value of the flag might still be there in memory, and so fputc fails. But it's still wrong, because it had to read freed memory in order to even see the flag. If you do a bunch more work in between, including allocating more memory and writing to it, you may see more unpredictable misbehavior on those systems too, perhaps including spurious returns of success.