All of stdio.h points at something called error indicator, which is an internal variable supposedly located in an opaque FILE object that the application programmer has no access to (see C17 7.21.1).
The documentation for getchar is found in C17 7.21.7.6:
The getchar function returns the next character from the input stream pointed to by
stdin. If the stream is at end-of-file, the end-of-file indicator for the stream is set and
getchar returns EOF. If a read error occurs, the error indicator for the stream is set and
getchar returns EOF.
So we don't know if getchar returned EOF because it reached the end of the stream, or because there was a read error. In order to know, we'd have to check the error indicator.
This is where ferror(stdin) comes in. It's a mildly useful function, because it only does this (C17 7.21.10.3):
The ferror function returns nonzero if and only if the error indicator is set for
stream.
And that's all there is to it - this is a standardized, portable abstraction layer and we can't really know what's going on underneath the hood. Which is nice, because most of the time we simply don't care.
There will be an OS-specific API underneath these standard C functions, in case of POSIX likely read(), in case of Windows likely ReadFile() etc. These functions in turn can fail for a number of reasons: incorrect file handles, file is taken by another process, no read access to the file given to the user by the OS and so on.
In theory getchar could as well be hooked up to a serial bus on an embedded system, in which case the reasons of it failing would be entirely different ones than on a hosted system. Now we are suddenly talking about wrong baudrate, buffer overruns, framing errors or whatever applies.