💾 Archived View for thrig.me › blog › 2023 › 06 › 27 › errno.gmi captured on 2024-07-09 at 01:06:25. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-11-14)

-=-=-=-=-=-=-

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.

https://www.youtube.com/watch?v=n05Dmwy7JX8

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