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: %dn", EOF);
// use fputc on a read-only closed stream (should return error, right?)
printf("fputc returned: %dn", 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, "%sn", 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
Answers
C17 7.21.3 (4):
So in your
fputc
call,fp
has an indeterminate value. The standard only defines a behavior forfputc
whenfp
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, soFILE *fp; ...; fclose(fp);
couldn’t change the value of yourfp
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. Thefp
parameter points to aFILE
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 forfputc
to return an error.Now when you
fclose
the file, the buffer andFILE
object are simply deallocated, withfree()
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 callfputc
. 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 callfputc
, the contents of the memory pointed to byfp
still look exactly like a validFILE
object, with a buffer that is not full, sofp
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 theirfputc
likely tests this flag. So if theFILE
object has not been overwritten with other stuff, the value of the flag might still be there in memory, and sofputc
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.Your code exhibits undefined behavior.
From J.2 Undefined behavior:
After you close the file, it’s invalid to use the
FILE
object, and the pointer itself has indeterminate value.