Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't call `ioutil.ReadDir` on `/dev/fd` on MacOS

Tags:

macos

go

I tried to run the following Go code:

package main

import (
    "fmt"
    "io/ioutil"
)

func main() {
    items, err := ioutil.ReadDir("/dev/fd")
    if err != nil {
        panic(err)
    }
    fmt.Println(items)
}

I just get this error:

panic: lstat /dev/fd/4: bad file descriptor

goroutine 1 [running]:
main.main()
    /Users/andy/Desktop/demo.go:11 +0xe8
exit status 2

The /dev/fd folder definitely exists, and there is a /dev/fd/4 inside there when I ls it.

$ ls -Al /dev/fd
total 9
crw--w----  1 andy  tty     16,   4 Jan 25 00:16 0
crw--w----  1 andy  tty     16,   4 Jan 25 00:16 1
crw--w----  1 andy  tty     16,   4 Jan 25 00:16 2
dr--r--r--  3 root  wheel      4419 Jan 23 20:42 3/
dr--r--r--  1 root  wheel         0 Jan 23 20:42 4/

What's going on? Why can't I read this directory? I'm trying to port the ls command to Go here, so I would like to be able to read this directory in order to produce similar output to ls.

EDIT: I am running everything as non-root user. The executable bit on /dev/fd is set.

$ ls -al /dev | grep fd
dr-xr-xr-x   1 root  wheel                 0 Jan 23 20:42 fd/

$ stat /dev/fd/4 # same result with -L flag
stat: /dev/fd/4: stat: Bad file descriptor
like image 800
Andy Carlson Avatar asked Jan 29 '23 19:01

Andy Carlson


1 Answers

First, let's remember that /dev/fd is special. Its contents are different for every process (since they reflect the file descriptors of that process), so it doesn't really mean anything that ls can list it because its contents will be different for ls and your program.

Anyway, here's a slightly updated version of your program where instead of letting ioutil do things behind our back, we do it ourselves to see what's going on:

package main

import (
        "fmt"
        "time"
        "os"
        "log"
)

func main() {
        d, err := os.Open("/dev/fd")
        if err != nil {
                log.Fatal(err)
        }
        names, err := d.Readdirnames(0)
        if err != nil {
                log.Fatal(err)
        }
        for _, n := range names {
                n = "/dev/fd/" + n
                fmt.Printf("file: %s\n", n)
                _, err := os.Lstat(n)
                if err != nil {
                        fmt.Printf("lstat error: %s - %v\n", n, err)
                }
        }

        time.Sleep(time.Second * 200)
}

And then when run it gives me:

file: /dev/fd/0
file: /dev/fd/1
file: /dev/fd/2
file: /dev/fd/3
file: /dev/fd/4
lstat error: /dev/fd/4 - lstat /dev/fd/4: bad file descriptor

So this is indeed the same problem. I added the sleep at the end so that the process doesn't die so that we can debug which file descriptors it has. In another terminal:

$ lsof -p 7861
COMMAND  PID USER   FD     TYPE     DEVICE SIZE/OFF       NODE NAME
foo     7861  art  cwd      DIR        1,4     2272     731702 /Users/art/src/go/src
foo     7861  art  txt      REG        1,4  1450576 8591078117 /private/var/folders/m7/d614cd9x61s0l3thb7cf3rkh0000gn/T/go-build268777304/command-line-arguments/_obj/exe/foo
foo     7861  art  txt      REG        1,4   837248 8590944844 /usr/lib/dyld
foo     7861  art    0u     CHR       16,4   0t8129        645 /dev/ttys004
foo     7861  art    1u     CHR       16,4   0t8129        645 /dev/ttys004
foo     7861  art    2u     CHR       16,4   0t8129        645 /dev/ttys004
foo     7861  art    3r     DIR 37,7153808        0        316 /dev/fd
foo     7861  art    4u  KQUEUE                                count=0, state=0x8

We can see that fd 4 is a KQUEUE. Kqueue file descriptors are used for managing events on OSX, similar to epoll on Linux if you know that mechanism.

It appears that OSX does not allow stat on kqueue file descriptors in /dev/fd which we can verify with:

$ cat > foo.c
#include <sys/types.h>
#include <sys/event.h>
#include <sys/stat.h>
#include <stdio.h>
#include <err.h>

int
main(int argc, char **argv)
{
    int fd = kqueue();
    char path[16];
    struct stat st;

    snprintf(path, sizeof(path), "/dev/fd/%d", fd);
    if (lstat(path, &st) == -1)
        err(1, "lstat");
    return 0;
}
$ cc -o foo foo.c && ./foo
foo: lstat: Bad file descriptor
$

Go programs on OSX need a kqueue to handle various events (not exactly sure which, but probably signals, timers and various file events).

You have four options here: don't stat, ignore the error, don't touch /dev/fd because it's weird, convince Apple that this is a bug.

like image 125
Art Avatar answered Jan 31 '23 08:01

Art