Why can I cat /dev?

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.


Less is a text file viewer, cat is a tool for copying arbitrary data. So less performs its own checking to make sure you're not opening something that will have massive amounts of data or behave very strangely. On the other hand, cat has no such checking at all – if the kernel lets you open something (even if it's a pipe or a device or something worse), cat will read it.

So why does the OS allow cat to open directories? Traditionally in BSD-style systems all directories could be read as files, and that was how programs would list a directory in the first place: by just interpreting dirent structures stored on disk.

Later on, those on-disk structures began to diverge from the dirent used by the kernel: where previously a directory was a linear list, later filesystems began using hashtables, B-trees, and so on. So reading directories directly wasn't straightforward anymore – the kernel grew dedicated functions for this. (I'm not sure if that was the main reason, or if they were primarily added for other reasons such as caching.)

Some BSD systems continue to let you open all directories for reading; I don't know whether they give you the raw data from disk, or whether they return an emulated dirent list instead, or whether they let the filesystem driver decide.

So perhaps macOS is one of those operating systems where the kernel allows it as long as the filesystem provides the data. And the difference is that /dev is on a devfs filesystem that was written to allow this in the early days, while / is on an APFS filesystem that omitted this feature as unnecessary in modern times.

Disclaimer: I haven't actually done any research on BSDs or macOS. I'm just winging it.

Tags:

Macos

Cat