2023-10-22
Unix /dev/fd and dup(2)
I recently read Amber Screen's A tale of /dev/fd (via) which notes an odd behavior of Linux's /dev/fd and in fact of FreeBSD's /dev/fd as well, where if you try to open a /dev/fd/N name for a file descriptor, it does permissions checks and may refuse a process permissions to re-open a file descriptor it already has open. Screen writes:
To the dispassionate hacker (or a reader of Stevens), it's pretty clear that a syscall like
open("/dev/fd/5", O_RDONLY)
should be similar todup(5)
. [...]
'Similar' is an extremely important word here, because you probably don't actually want this to act as if you called dup() on the original file descriptor. But we'll get to that.
A narrow Linux-specific reason that opening /dev/fd/N forces permissions re-checks is probably partly because /dev/fd is a symbolic link to /proc/self/fd, which is a self-referential instance of a general Linux /proc facility that allows you to open what any process's file descriptor points to, provided that you have the necessary permissions. This includes file descriptors for things that are now deleted (I've used this to rescue a file that I accidentally removed but a process still had open). You obviously need to do permissions checks on this in general, so it would take extra code to special case re-opening your own file descriptors.
Somewhat more broadly, you also need some permissions checks no matter what, because the re-open of /dev/fd/N (aka /proc/self/fd/N) could be for a different mode than the file descriptor is currently open for. If your process currently has a file descriptor opened read only and you try to re-open it for write through /dev/fd/N (or /proc/self/fd/N), that had better check if you can actually write to the file. At the same time you probably do want to allow mode changes like this for /dev/fd/N provided that the underlying file permissions (and other situation) allow it, partly because programs may require it for some files they open.
(This isn't just for re-opening files with write permissions when you started with read permission. You might also have file descriptors opened only for write that you now want to read.)
The reason that you don't want /dev/fd/N to do a dup() is that dup()'d file descriptors share more things with each other than separately opened file descriptors on the same file. For one prominent example (noted in both Linux dup(2) and FreeBSD dup(2)), the file offset is shared between all dup()'d file descriptors. If one such descriptor reads, writes, or lseeks, all file descriptors now act on the new file offset. This is fine if a program expects this behavior (or reasonably should), because it obtained the file descriptor through a dup() or dup2(). It's quite possibly not fine if the program gets gifted this behavior simply because it open()'d what it sees as a separate file name; in fact, this behavior would be more or less in contradiction to open()'s normal specification (which promises you an independent file object). So you don't want to implement /dev/fd/N by directly doing a dup() on the given file descriptor if the modes match; you need to do something more complicated.
(To re-iterate Amber Screen's own quote, they aren't advocating for /dev/fd/N literally being a dup() of the file descriptor. As I read it, they are advocating for similar behavior with minimal permissions checking if required because you're changing the mode. Otherwise this /dev/fd/N re-open would make a new and separate copy of what POSIX calls the 'open file description' (see Linux open(2)'s discussion of this).)