errno is a global variable; anything can change it within a process. errno may also remain unchanged across who knows how much code, until it emerges with some non-zero value. This can be surprising, especially as the abstractions pile up and errno emerges from who knows where.
First up, errno is copied through fork, so might be non-zero due to being set in a parent process:
$ perl -E '$! = 42; fork // die "argh?"; say 0 + $!'
42
42
$ cfu 'errno = 42; fork(); printf("%d\n", errno)'
42
42
It is typical to zero errno before an important call, e.g. before strtol(3) so that ERANGE or whatever is not carried over from who knows where.
char *ep;
long n;
errno = 0;
n = strtol(buf, &ep, 10);
...
In higher level languages errno can leak through unexpectedly; one problem here is to incorrectly use errno without there being an actual failure:
$ cat failrrno
#!/usr/bin/perl
open my $fh, '/etc/passwd';
if ($!) { print "open failed: $!\n"; }
print scalar readline $fh;
$ perl failrrno
open failed: Inappropriate ioctl for device
root:*:0:0:Charlie &:/root:/bin/ksh
$ errno 25
ENOTTY 25 Inappropriate ioctl for device
errno was set to 25 due to some call; the open did not actually fail. Less bad code would run along the lines of the following, which only uses $! (errno) when open actually fails:
my $file = '/etc/passwd';
open my $fh, $file or die "open failed '$file': $!\n";
Where did the 25 come from? ktrace(1) may show this,
$ ktrace perl failrrno
...
$ kdump | grep -3 'errno 25'
89865 perl CALL kbind(0x7c4ce01716f8,24,0xbc5a98c260e13bfc)
89865 perl RET kbind 0
89865 perl CALL fcntl(3,F_ISATTY)
89865 perl RET fcntl -1 errno 25 Inappropriate ioctl for device
89865 perl CALL lseek(3,0,SEEK_CUR)
89865 perl RET lseek 0
89865 perl CALL kbind(0x7c4ce01718a8,24,0xbc5a98c260e13bfc)
--
89865 perl NAMI "/etc/passwd"
89865 perl RET open 3
89865 perl CALL fcntl(3,F_ISATTY)
89865 perl RET fcntl -1 errno 25 Inappropriate ioctl for device
89865 perl CALL lseek(3,0,SEEK_CUR)
89865 perl RET lseek 0
89865 perl CALL fstat(3,0x7c4ce0171800)
so perl is automatically doing a fcntl call on the file descriptor that fails and sets errno ($!) as part of normal operation. Higher level languages tend to have features like this. Here's some other late 1980s dynamic scripting languages:
$ cat failrrno.pie
x = open('/etc/passwd', 'r')
$ ktrace python3 failrrno.pie
$ kdump | grep errno\ 25
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
99601 python3.10 RET fcntl -1 errno 25 Inappropriate ioctl for device
$ cat failrrno.tcl
#!/usr/local/bin/tclsh8.6
set fh [open /etc/passwd]
$ ktrace tclsh8.6 failrrno.tcl
$ kdump | grep errno\ 25
48902 tclsh8.6 RET fcntl -1 errno 25 Inappropriate ioctl for device
48902 tclsh8.6 RET fcntl -1 errno 25 Inappropriate ioctl for device
48902 tclsh8.6 RET fcntl -1 errno 25 Inappropriate ioctl for device
LISP on unix will set errno variously. sbcl at least does not have the fcntl thing like the above languages do.
$ cat open.lisp
(with-open-file (fh "/etc/passwd")
(format t "~a~&" (read-line fh nil)))
$ ktrace /usr/local/bin/sbcl --script open.lisp
root:*:0:0:Charlie &:/root:/bin/ksh
$ kdump | grep errno\ 25
$ kdump | grep "errno "
84999 sbcl RET sigaltstack -1 errno 22 Invalid argument
84999 sbcl RET stat -1 errno 2 No such file or directory
84999 sbcl RET lstat -1 errno 2 No such file or directory
84999 sbcl RET sigaltstack -1 errno 22 Invalid argument
I said that "ktrace may show" errno being set. errno could however be set by anything anywhere, which ktrace may not see. In this case you'll probably need to use a debugger and to scour the code to see where errno is being set wrongly. Process tracing is however a good first step as most often it's some traceable function call that sets errno.
Crouching Tiger, Hidden errno
errno can be masked when a function makes multiple calls that may each set errno. In this case usually only the last error may be available, and despite there being errors the function may return a success. Most any function that does a PATH or LD_LIBRARY_PATH search will have this issue.
$ ktrace sysclean
/usr/local/sbin/sysclean: error: need root privileges
$ kdump | egrep 'RET *execve'
32620 ktrace RET execve -1 errno 2 No such file or directory
32620 ktrace RET execve -1 errno 2 No such file or directory
32620 ktrace RET execve -1 errno 2 No such file or directory
32620 ktrace RET execve -1 errno 2 No such file or directory
32620 ktrace RET execve -1 errno 2 No such file or directory
32620 ktrace RET execve -1 errno 2 No such file or directory
32620 ktrace RET execve -1 errno 2 No such file or directory
32620 ktrace RET execve -1 errno 2 No such file or directory
32620 ktrace RET execve -1 errno 2 No such file or directory
32620 ktrace RET execve -1 errno 2 No such file or directory
32620 perl RET execve JUSTRETURN
The multiple failures here are the PATH search looking for "sysclean" in each directory in PATH; "sysclean" just so happens to be in the last of the PATH directories, so there were a bunch of failures before it was found. All the errors are ignored. One or more of these errors might be important if a library cannot be loaded due to a permissions problem, but then the code finds the wrong version of that library in a subsequent directory: the correct library will be ignored, and the wrong one loaded with success. Process tracing will again help to show what is actually going on.
tags #unix