I work on what can be seen as a C interpreter that detects all undefined behaviors in the program it interprets. While using this interpreter to find bugs in a legacy open-source C application, I was puzzled by the following behavior:
The legacy application expected a 10-byte header that it needed it its entirety in order to do further work. It was correctly calling fread(buffer, 10, 1, f);. Incorrectly, it did not assign the result of the fread call, and started parsing the buffer right away.
When this fread was applied on a file with less than 10 bytes available, what happened was that buffer was partially filed with the available data. The interpreter, as it is designed to, detected that an uninitialized part of the buffer was used later and warned about that and I was able to trace the problem to the result of fread being discarded.
It puzzled me for one minute that the fread written by my colleagues had partially filled the buffer, even though it was going to return 0 eventually, and I wondered if this could be improved. Obviously some implementations do read into the buffer and in the end return the number of read record as return n_bytes / __size;, letting the division round down, in this case to 0. But I wondered if other implementations might only write to buffer if an entire record is available, and leave it entirely uninitialized otherwise.
In practice, on the two Unices that I have at hand, fread behaves the same way as the model implementation written by my colleagues:
~ $ cat t.c
#include <stdio.h>
#include <stdlib.h>
char buffer[11] = "0000000000";
int main(void) {
FILE *f = fopen("aaaa", "r");
if (!f) exit(1);
int r = fread(buffer, 10, 1, f);
printf("%s\n", buffer);
}
~ $ gcc t.c && ./a.out
aaaa000000
~ $ uname -a
Darwin tis-laptop-6.local 16.7.0 Darwin Kernel Version 16.7.0: Sun Jun 2 20:26:31 PDT 2019; root:xnu-3789.73.50~1/RELEASE_X86_64 x86_64
The test program also produces the same result on Linux with Glibc (the file aaaa contains aaaa).
It would be unreasonably costly to analyse what happens in each of the cases “fread partially fills the last record” and “fread leaves the last partial record untouched in the buffer” in the interpreter, but we could make the part of the buffer that should have received the record for which there is only partial data uninitialized, to prevent the interpreted C program to rely on it. But that in turn would be puzzling when fread is passed an already initialized buffer.
So I find myself wondering whether the behavior that the interpreter's fread currently has, and that macOS and Linux/Glibc also have, is guaranteed (in which case everything is fine).
I have found this related question. This comment seems to indicate that the fread behavior I observed in all three implementations is the only possible one, but I would like an explicit confirmation (or not) that fread can be assumed to read all the characters available up to size * nmemb even if the number of characters available is not a multiple of size.