Skip to content

Commit 4b0e334

Browse files
authored
gh-157899: Add support for fds pointing to symlinks in os.readlink(). (GH-157915)
1 parent 9512cff commit 4b0e334

10 files changed

Lines changed: 227 additions & 24 deletions

File tree

‎Doc/library/os.rst‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2799,14 +2799,23 @@ features:
27992799
may be converted to an absolute pathname using
28002800
``os.path.join(os.path.dirname(path), result)``.
28012801

2802-
If the *path* is a string object (directly or indirectly through a
2803-
:class:`PathLike` interface), the result will also be a string object,
2804-
and the call may raise a UnicodeDecodeError. If the *path* is a bytes
2805-
object (direct or indirectly), the result will be a bytes object.
2806-
28072802
This function can also support :ref:`paths relative to directory descriptors
28082803
<dir_fd>`.
28092804

2805+
On Linux, Android, macOS and iOS, *path* can also be a file descriptor referring
2806+
to a symbolic link. In that case, *dir_fd* must be ``None``.
2807+
(On Linux and Android, such a file descriptor must be obtained through
2808+
:func:`os.open` with ``os.O_RDONLY | os.O_PATH | os.O_NOFOLLOW``.
2809+
On macOS and iOS, :func:`os.open` must be called with
2810+
``os.O_RDONLY | os.O_SYMLINK``.)
2811+
On other operating systems, a ``NotImplementedError`` is raised if *path*
2812+
is an integer.
2813+
2814+
If the *path* is a string object (directly or indirectly through a
2815+
:class:`PathLike` interface) or a file descriptor, the result will be a
2816+
string object, and the call may raise a UnicodeDecodeError. If the *path*
2817+
is a bytes object (direct or indirectly), the result will be a bytes object.
2818+
28102819
When trying to resolve a path that may contain links, use
28112820
:func:`~os.path.realpath` to properly handle recursion and platform
28122821
differences.
@@ -2829,6 +2838,10 @@ features:
28292838
substitution path (which typically includes ``\\?\`` prefix) rather
28302839
than the optional "print name" field that was previously returned.
28312840

2841+
.. versionchanged:: next
2842+
Accepts file descriptors pointing to symbolic links as *path* on
2843+
Linux, Android, macOS and iOS.
2844+
28322845
.. function:: remove(path, *, dir_fd=None)
28332846

28342847
Remove (delete) the file *path*. If *path* is a directory, an

‎Doc/whatsnew/3.16.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,10 @@ os
531531
now raises :exc:`PermissionError` instead of the functions being unavailable.
532532
(Contributed by Md Arif in :gh:`152936`.)
533533

534+
* :func:`os.readlink` now accepts a file descriptor referring to a symlink on
535+
Linux, Android, macOS and iOS.
536+
(Contributed by OOTS in :gh:`157899`.)
537+
534538

535539
pydoc
536540
-----

‎Lib/os.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ def _add(str, fn):
161161
_add("HAVE_FSTATVFS", "statvfs")
162162
if _exists("statx"):
163163
_set.add(statx)
164+
_add("HAVE_FREADLINK", "readlink")
165+
if sys.platform in ["linux", "android"] and "O_PATH" in _globals:
166+
_add("HAVE_READLINKAT", "readlink")
164167
supports_fd = _set
165168

166169
_set = set()

‎Lib/test/test_os/test_posix.py‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1881,6 +1881,74 @@ def test_readlink_dir_fd(self):
18811881
self.addCleanup(posix.unlink, fullname)
18821882
self.assertEqual(posix.readlink(name, dir_fd=dir_fd), 'symlink')
18831883

1884+
1885+
_support_readlink_with_fd = (
1886+
hasattr(os, 'readlink')
1887+
and os.readlink in os.supports_fd
1888+
)
1889+
1890+
def _open_symlink_as_fd(self, path):
1891+
open_flags = os.O_RDONLY
1892+
if hasattr(os, "O_SYMLINK"): # macOS/iOS
1893+
open_flags |= os.O_SYMLINK
1894+
elif hasattr(os, "O_NOFOLLOW") and hasattr(os, "O_PATH"): # Linux/Android
1895+
open_flags |= os.O_NOFOLLOW | os.O_PATH
1896+
else:
1897+
self.fail("lacking open flags for this test")
1898+
return os.open(path, open_flags)
1899+
1900+
@unittest.skipUnless(_support_readlink_with_fd,
1901+
"feature not supported on this platform")
1902+
def test_readlink_with_fd(self):
1903+
with self.prepare() as (dir_fd, name, fullname):
1904+
os.symlink("symlink", fullname)
1905+
self.addCleanup(posix.unlink, fullname)
1906+
fd = self._open_symlink_as_fd(fullname)
1907+
self.addCleanup(os.close, fd)
1908+
self.assertEqual(os.readlink(fd), "symlink")
1909+
1910+
@unittest.skipUnless(_support_readlink_with_fd,
1911+
"feature not supported on this platform")
1912+
def test_readlink_with_fd_not_referring_to_symlink_throws(self):
1913+
with self.prepare_file() as (dir_fd, name, fullname):
1914+
fd = os.open(fullname, os.O_RDONLY)
1915+
self.addCleanup(os.close, fd)
1916+
# on Linux/Android, readlinkat("", fd, ...) fails with ENOENT, which
1917+
# Python translates to a FileNotFoundError, a subclass of OSError.
1918+
# On macOS, freadlink(fd, ...) fails with EINVAL, which gets raised
1919+
# as a OSError.
1920+
# So catching OSError here covers both cases.
1921+
with self.assertRaises(OSError):
1922+
os.readlink(fd)
1923+
1924+
@unittest.skipUnless(_support_readlink_with_fd,
1925+
"feature not supported on this platform")
1926+
def test_readlink_with_fd_throws_if_both_fd_and_dir_fd_are_given(self):
1927+
with self.prepare() as (dir_fd, name, fullname):
1928+
os.symlink("symlink", fullname)
1929+
self.addCleanup(posix.unlink, fullname)
1930+
fd = self._open_symlink_as_fd(fullname)
1931+
self.addCleanup(os.close, fd)
1932+
with self.assertRaises(ValueError):
1933+
os.readlink(fd, dir_fd=dir_fd)
1934+
1935+
@unittest.skipIf(_support_readlink_with_fd,
1936+
"feature is supported on this platform")
1937+
def test_readlink_with_fd_throws_not_implemented_error(self):
1938+
# on unsupported platforms, we may not even be able to get a
1939+
# file descriptor for a symlink, so use a fd for an ordinary file
1940+
os_helper.create_empty_file(os_helper.TESTFN)
1941+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
1942+
fd = os.open(os_helper.TESTFN, os.O_RDONLY)
1943+
self.addCleanup(os.close, fd)
1944+
with self.assertRaises(NotImplementedError):
1945+
os.readlink(fd)
1946+
1947+
@unittest.skipUnless(sys.platform in ["linux", "android", "darwin", "ios"],
1948+
"feature not supported on this platform")
1949+
def test_readlink_is_marked_as_supports_fd(self):
1950+
self.assertIn(os.readlink, os.supports_fd)
1951+
18841952
@unittest.skipUnless(os.rename in os.supports_dir_fd, "test needs dir_fd support in os.rename()")
18851953
def test_rename_dir_fd(self):
18861954
with self.prepare_file() as (dir_fd, name, fullname), \
@@ -2620,6 +2688,17 @@ def test_readlink(self):
26202688
with self.assertRaisesRegex(NotImplementedError, "dir_fd unavailable"):
26212689
os.readlink("path", dir_fd=0)
26222690

2691+
def test_freadlink(self):
2692+
self._verify_available("HAVE_FREADLINK")
2693+
if self.mac_ver >= (13, 0):
2694+
self.assertIn("HAVE_FREADLINK", posix._have_functions)
2695+
2696+
else:
2697+
self.assertNotIn("HAVE_FREADLINK", posix._have_functions)
2698+
errmsg = "readlink cannot read file descriptors on this platform"
2699+
with self.assertRaisesRegex(NotImplementedError, errmsg):
2700+
os.readlink(0)
2701+
26232702
def test_symlink(self):
26242703
self._verify_available("HAVE_SYMLINKAT")
26252704
if self.mac_ver >= (10, 10):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`os.readlink` now accepts a file descriptor referring to a
2+
symlink on Linux, Android, macOS and iOS.

‎Modules/clinic/posixmodule.c.h‎

Lines changed: 6 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Modules/posixmodule.c‎

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ static const unsigned int _Py_STATX_KNOWN = (STATX_BASIC_STATS | STATX_BTIME
505505
# define HAVE_UNLINKAT_RUNTIME __builtin_available(macOS 10.10, iOS 8.0, *)
506506
# define HAVE_OPENAT_RUNTIME __builtin_available(macOS 10.10, iOS 8.0, *)
507507
# define HAVE_READLINKAT_RUNTIME __builtin_available(macOS 10.10, iOS 8.0, *)
508+
# define HAVE_FREADLINK_RUNTIME __builtin_available(macOS 13.0, iOS 16.0, *)
508509
# define HAVE_SYMLINKAT_RUNTIME __builtin_available(macOS 10.10, iOS 8.0, *)
509510
# define HAVE_FUTIMENS_RUNTIME __builtin_available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
510511
# define HAVE_UTIMENSAT_RUNTIME __builtin_available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
@@ -571,6 +572,10 @@ static const unsigned int _Py_STATX_KNOWN = (STATX_BASIC_STATS | STATX_BTIME
571572
# define HAVE_READLINKAT_RUNTIME (readlinkat != NULL)
572573
# endif
573574

575+
# ifdef _Py_HAVE_FREADLINK
576+
# define HAVE_FREADLINK_RUNTIME (freadlink != NULL)
577+
# endif
578+
574579
# ifdef HAVE_SYMLINKAT
575580
# define HAVE_SYMLINKAT_RUNTIME (symlinkat != NULL)
576581
# endif
@@ -10979,11 +10984,19 @@ os_unshare_impl(PyObject *module, int flags)
1097910984
#endif
1098010985

1098110986

10987+
1098210988
#if defined(HAVE_READLINK) || defined(MS_WINDOWS)
10989+
10990+
#if (defined(__linux__) || defined(__ANDROID__)) && defined(O_PATH)
10991+
// readlinkat(fd, "", ...) reads the symlink that fd refers to.
10992+
// supported since Linux 2.6.39 (same version that O_PATH was introduced).
10993+
#define _Py_READLINKAT_SUPPORTS_EMPTY_PATH
10994+
#endif
10995+
1098310996
/*[clinic input]
1098410997
os.readlink
1098510998

10986-
path: path_t
10999+
path: path_t(allow_fd=True)
1098711000
*
1098811001
dir_fd: dir_fd(requires='readlinkat') = None
1098911002

@@ -10995,48 +11008,74 @@ that directory.
1099511008

1099611009
dir_fd may not be implemented on your platform. If it is unavailable,
1099711010
using it will raise a NotImplementedError.
11011+
11012+
On Linux, Android, macOS and iOS, path may be a file descriptor
11013+
referring to a symlink. If it is, dir_fd must be None.
1099811014
[clinic start generated code]*/
1099911015

1100011016
static PyObject *
1100111017
os_readlink_impl(PyObject *module, path_t *path, int dir_fd)
11002-
/*[clinic end generated code: output=d21b732a2e814030 input=03d10130870dbca8]*/
11018+
/*[clinic end generated code: output=d21b732a2e814030 input=d90bddfeee86f673]*/
1100311019
{
1100411020
#if defined(HAVE_READLINK)
1100511021
char buffer[MAXPATHLEN+1];
1100611022
ssize_t length;
11007-
#ifdef HAVE_READLINKAT
11008-
int readlinkat_unavailable = 0;
11009-
#endif
1101011023

11011-
Py_BEGIN_ALLOW_THREADS
11024+
if (path_and_dir_fd_invalid("readlink", path, dir_fd)) {
11025+
return NULL;
11026+
}
11027+
11028+
if (path->is_fd) {
11029+
#if defined(_Py_HAVE_FREADLINK)
11030+
if (HAVE_FREADLINK_RUNTIME) {
11031+
Py_BEGIN_ALLOW_THREADS
11032+
length = freadlink(path->fd, buffer, MAXPATHLEN);
11033+
Py_END_ALLOW_THREADS
11034+
} else
11035+
#elif defined(HAVE_READLINKAT) && defined(_Py_READLINKAT_SUPPORTS_EMPTY_PATH)
11036+
// linux/android:
11037+
// readlinkat(fd, "", ...) reads the link that fd refers to.
11038+
if (HAVE_READLINKAT_RUNTIME) {
11039+
Py_BEGIN_ALLOW_THREADS
11040+
length = readlinkat(path->fd, "", buffer, MAXPATHLEN);
11041+
Py_END_ALLOW_THREADS
11042+
} else
11043+
#endif
11044+
{
11045+
PyErr_SetString(PyExc_NotImplementedError,
11046+
"readlink cannot read file descriptors on this platform");
11047+
return NULL;
11048+
}
11049+
} else
1101211050
#ifdef HAVE_READLINKAT
1101311051
if (dir_fd != DEFAULT_DIR_FD) {
1101411052
if (HAVE_READLINKAT_RUNTIME) {
11053+
Py_BEGIN_ALLOW_THREADS
1101511054
length = readlinkat(dir_fd, path->narrow, buffer, MAXPATHLEN);
11055+
Py_END_ALLOW_THREADS
1101611056
} else {
11017-
readlinkat_unavailable = 1;
11057+
argument_unavailable_error(NULL, "dir_fd");
11058+
return NULL;
1101811059
}
1101911060
} else
1102011061
#endif
11062+
{
11063+
Py_BEGIN_ALLOW_THREADS
1102111064
length = readlink(path->narrow, buffer, MAXPATHLEN);
11022-
Py_END_ALLOW_THREADS
11023-
11024-
#ifdef HAVE_READLINKAT
11025-
if (readlinkat_unavailable) {
11026-
argument_unavailable_error(NULL, "dir_fd");
11027-
return NULL;
11065+
Py_END_ALLOW_THREADS
1102811066
}
11029-
#endif
1103011067

1103111068
if (length < 0) {
1103211069
return path_error(path);
1103311070
}
1103411071
buffer[length] = '\0';
1103511072

11036-
if (PyUnicode_Check(path->object))
11073+
if (path->is_fd || PyUnicode_Check(path->object)) {
1103711074
return PyUnicode_DecodeFSDefaultAndSize(buffer, length);
11038-
else
11075+
}
11076+
else {
1103911077
return PyBytes_FromStringAndSize(buffer, length);
11078+
}
1104011079
#elif defined(MS_WINDOWS)
1104111080
DWORD n_bytes_returned;
1104211081
DWORD io_result = 0;
@@ -11045,6 +11084,12 @@ os_readlink_impl(PyObject *module, path_t *path, int dir_fd)
1104511084
_Py_REPARSE_DATA_BUFFER *rdb = (_Py_REPARSE_DATA_BUFFER *)target_buffer;
1104611085
PyObject *result = NULL;
1104711086

11087+
if (path->is_fd) {
11088+
PyErr_SetString(PyExc_NotImplementedError,
11089+
"readlink cannot read file descriptors on this platform");
11090+
return NULL;
11091+
}
11092+
1104811093
/* First get a handle to the reparse point */
1104911094
Py_BEGIN_ALLOW_THREADS
1105011095
reparse_point_handle = CreateFileW(
@@ -18878,6 +18923,10 @@ PROBE(probe_openat, HAVE_OPENAT_RUNTIME)
1887818923
PROBE(probe_readlinkat, HAVE_READLINKAT_RUNTIME)
1887918924
#endif
1888018925

18926+
#ifdef _Py_HAVE_FREADLINK
18927+
PROBE(probe_freadlink, HAVE_FREADLINK_RUNTIME)
18928+
#endif
18929+
1888118930
#ifdef HAVE_SYMLINKAT
1888218931
PROBE(probe_symlinkat, HAVE_SYMLINKAT_RUNTIME)
1888318932
#endif
@@ -18945,6 +18994,10 @@ static const struct have_function {
1894518994
{ "HAVE_FPATHCONF", NULL },
1894618995
#endif
1894718996

18997+
#ifdef _Py_HAVE_FREADLINK
18998+
{ "HAVE_FREADLINK", probe_freadlink },
18999+
#endif
19000+
1894819001
#ifdef HAVE_FSTATAT
1894919002
{ "HAVE_FSTATAT", probe_fstatat },
1895019003
#endif

‎configure‎

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)