Support File Descriptors in os.readlink()

Hello!

I’d like to propose adding support for file descriptors to os.readlink(). Currently, in os.readlink() can be called with strings, bytes, and (in general) PathLike objects (see the docs). It reads a symlink and returns the path that the symlink is pointing to. However (as far as I’m aware of) if you have a file descriptor (just an integer) for a symlink (not the file targeted by the symlink), there’s no way to check what the symlink refers to.

My proposal is to change that, so that if you pass an integer object to os.readlink(), it interprets that integer as a FD (pointing to a symlink) and tries to read that link and return the path that the symlink is pointing to (consistent with the normal behaviour of os.readlink()). If the integer is not an open FD or the FD does not refer to a symlink, then it should throw (like it does when calling os.readlink() on a path that’s not a symlink.

Regarding implementation: On Linux, you can call the readlinkat syscall with path == "" (see docs). On MacOS, you can call freadlink() (see docs). I’d suggest to leave this feature unimplemented on Windows (raise an error when os.readlink() receives an integer, consistent with current behaviour).

Python os.readlink() already supports calling readlinkat() if you use dir_fd parameter. Example:

$ python3.16
>>> import os
>>> root = os.open("/", os.O_RDONLY)
>>> os.readlink("/bin")
'usr/bin'
>>> os.readlink("bin", dir_fd=root)
'usr/bin'

Problem: I don’t see how you would pass a file descriptor of a file to readlinkat(). The dirfd parameter of readlinkat() is the directory file descriptor.

Passing an empty string to readlinkat() fails with FileNotFoundError:

>>> os.readlink("", dir_fd=root)
FileNotFoundError: [Errno 2] No such file or directory: ''

According to the documentation, using openat with O_PATH|O_NOFOLLOW flags. I do not think this is supported in Python. If we add such feature, it needs a good design.

Seems useful. This is actually already possible, but to understand how, I had to first check the docs for the underlying C API to learn how, and then implement that in Python, which works as follows:

>>> import os
>>> os.readlink(".steam")
'/mnt/sata-ssd/.steam/'
>>> fd = os.open(".steam", os.O_RDONLY|os.O_PATH|os.O_NOFOLLOW)
>>> os.readlink("", dir_fd=fd)
'/mnt/sata-ssd/.steam/'

So your proposal could be handled as “if the path argument is an integer and there is no dir_fd, use path as dir_fd and set path to an empty string”.

Note that the flags to os.open are essential. Without them:

>>> fd = os.open(".steam", os.O_RDONLY)
>>> os.readlink("", dir_fd=fd)
Traceback (most recent call last):
  File "<python-input-5>", line 1, in <module>
    os.readlink("", dir_fd=fd)
    ~~~~~~~~~~~^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: ''

even though such a file descriptor could be used as dir_fd to other APIs.

1 Like

Indeed, getting into the situation where you need this is a bit … peculiar, since getting a FD for the symlink itself is a bit tricky in the first place. But as @Rosuav and @storchaka have pointed out, that works (under Linux) with os.open(..., os.O_RDONLY | os.O_PATH | os.O_NOFOLLOW). On MacOS, you can get one with os.open(..., os.O_RDONLY | os.O_SYMLINK).

I wasn’t aware that os.readlink("", dir_fd=the_fd_for_the_symlink) actually works - seems more of a coincidence than anything else, since Python probably just passes its arguments to the underlying syscall. I guess that more or less solves my issue on Linux (though I’d prefer a cleaner, documented and tested solution). However, on MacOS the same fails with a FileNotFoundError for the file "" even you pass the symlink FD as dir_fd.

The “passes its arguments to the underlying syscall” part is documented under dir_fd:

For POSIX systems, Python will call the variant of the function with an at suffix and possibly prefixed with f (e.g. call faccessat instead of access).

So, your system docs apply; on Linux man 2 readlinkat should say:

Since Linux 2.6.39, path can be an empty string, in which case the
call operates on the symbolic link referred to by dirfd (which
should have been obtained using open(2) with the O_PATH and
O_NOFOLLOW flags).

Unfortunately, O_PATH is Linux-specific and it makes the fd unusable for most other operations. I wouldn’t be comfortable adding os.readlink to os.supports_fd if it only supports OS_PATH fd’s.

+1. If you send a PR, ping me for review :‍)

Perfectly fine; os.supports_fd is already rather short on Windows.

Ping @encukou : I created an MR here: gh-157899: Add support for fds pointing to symlinks in os.readlink(). by OOTS · Pull Request #157915 · python/cpython · GitHub (And after a bit of effort, all the checks are green.)

If you could spare some minutes for a review, I’d appreciate it. The MR is about ~230 lines of changes, including docs, tests, etc., so fairly small.