Why can I cat /dev?


I can cat /dev, I can ls /dev, I can't less /dev. Why does cat let me cat this directory but no other directories?

Picture of this behaviour in zsh.


Answers:


Historically (up to V7 UNIX, or around 1979) the read system call worked on both files and directories. read on a directory would return a simple data structure which a user program would parse to obtain the directory entries. Indeed, the V7 ls tool did exactly this - read on a directory, parse the resulting data structure, output in a structured list format.

As filesystems got more complex, this "simple" data structure got more complicated, to the point where a readdir library function was added to help programs parse the output of read(directory). Different systems and filesystems might have different on-disk formats, which was getting complicated.

When Sun introduced the Network File System (NFS), they wanted to fully abstract away the on-disk directory structure. Instead of making their read(directory) return a platform-independent representation of the directory, however, they added a new system call - getdirents - and banned read on network-mounted directories. This system call was rapidly adapted to work on all directories in various UNIX flavours, making it the default way to get the contents of the directories. (History abstracted from https://utcc.utoronto.ca/~cks/space/blog/unix/ReaddirHistory)

Because readdir is now the default way to read directories, read(directory) is usually not implemented (returning -EISDIR) on most modern OSes (QNX, for example, is a notable exception which implements readdir as read(directory)). However, with the "virtual filesystem" design in most modern kernels, it's actually up to the individual filesystem whether reading a directory works or not.

And indeed, on macOS, the devfs filesystem underlying the /dev mountpoint really does support reading (https://github.com/apple/darwin-xnu/blob/xnu-4570.1.46/bsd/miscfs/devfs/devfs_vnops.c#L629):

static int
devfs_read(struct vnop_read_args *ap)
{
        devnode_t * dn_p = VTODN(ap->a_vp);

    switch (ap->a_vp->v_type) {
      case VDIR: {
          dn_p->dn_access = 1;

          return VNOP_READDIR(ap->a_vp, ap->a_uio, 0, NULL, NULL, ap->a_context);

This explicitly calls READDIR if you try to read /dev (reading files under /dev is handled by a separate function - devfsspec_read). So, if a program calls the read system call on /dev, it'll succeed and obtain a directory listing!

This is effectively a feature that is a holdover from the very early days of UNIX, and which hasn't been touched in a very long time. Part of me suspects that this is being kept around for some backwards compatibility reason, but it could just as easily be the fact that nobody cares enough to remove the feature since it isn't really hurting anything.