Why does successfully reading from a /dev source of data set errno?

184 views Asked by At

Simple test program in C called get1:

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

int main(void) {
    errno = 0;
    int ch = fgetc(stdin);
    printf("ch = %d\n", ch);
    if (errno)
        printf("errno = %d: %s\n", errno, strerror(errno));
    return 0;
}

It just prints the first byte read in decimal, and then shows errno and the associated error message if errno is not zero.

Some results (foo is a text file, empty is a file of length zero):

% ./get1 < foo
ch = 104
% ./get1 < empty
ch = -1

Ok, as expected. However:

% ./get1 < /dev/zero 
ch = 0
errno = 25: Inappropriate ioctl for device
% ./get1 < /dev/null 
ch = -1
errno = 25: Inappropriate ioctl for device
% ./get1 < /dev/random 
ch = 196
errno = 25: Inappropriate ioctl for device

The read is working fine, but when I read from any of these devices it is setting errno. Why?

That was on macOS (Darwin). I get the same thing on Linux (Debian) and NetBSD, except a different error just for /dev/random (the errors on the other devices were the same as on macOS):

% ./get1 < /dev/random 
ch = 170
errno = 22: Invalid argument

It was my understanding that if you don't get EOF, then there is no error. If you do get EOF, then you check errno to see if there was an error. In all of these cases there does not appear to be an error in getting a byte from the device, nor should there be. Yet errno was set.

(Removing the printf doesn't change anything, in case anyone is wondering.)

As it turns out, errno is not set when reading from such a device on AIX, SunOS, or OpenSUSE.

So what's the deal here? Is this a bug, albeit widespread? Is this acceptable behavior? Is this expected behavior? Why is it setting errno?

2

There are 2 answers

7
rici On BEST ANSWER

I wouldn't have said it was expected behaviour (and indeed, I cannot reproduce it on the systems I have handy), but it is permitted by both the C standard and by Posix.

Both the C standard and Posix generally prohibit library functions from setting errno to 0, so they cannot clear errno to indicate success. However, C permits errno to be modified by a library function "whether or not there is an error, provided the use of errno is not documented in the description of the function" (§7.5 para 3). The C standard does not document the use of errno by fgetc.

Posix is even more flexible: it specifies that

The setting of errno after a successful call to a function is unspecified unless the description of that function specifies that errno shall not be modified.

It does document the use of errno by fgetc, but that documentation does not indicate that errno shall not be modified by a successful call, only that it should be modified by a read error.

Specifically, what Posix says is

If a read error occurs, the error indicator for the stream shall be set, fgetc() shall return EOF, and shall set errno to indicate the error.

In other words, EOF might indicate a successful call (because end of file is not an error) or it might indicate an error. Only in the latter case is errno set to a meaningful value. Consequently, before checking errno you need to verify that ferror(stdin) reports an error.

So, as I said, the behaviour is allowed by the standards. Why does the fgetc implementation do that? My guess is that the libc implementations on your systems are doing some sort of lazy initialization on the first call.

7
Zbynek Vyskovsky - kvr000 On

The errno is not result of the specific operation but rather result of supporting operation when building high level FILE *stdin stdio structure.

Libc usually calls stat(), few others and lseek() system call which fails because the specific files in /dev/ are not regular files. It may depend on specific Libc implementation, therefore the differences you saw on various systems.

This doesn't mean that your operation failed though, you should check errno only in case error code is returned.

On Linux you can use command strace to get the list of all system calls your program is calling and the exact one causing the errno:

strace ./my program

There are similar tools on the other systems too.