https://github.com/python/cpython/commit/5a57248b22ad3b9aafcaaadae2c304a1923daeca
commit: 5a57248b22ad3b9aafcaaadae2c304a1923daeca
branch: main
author: Serhiy Storchaka <storch...@gmail.com>
committer: serhiy-storchaka <storch...@gmail.com>
date: 2025-05-04T17:24:10+03:00
summary:

gh-81793: Always call linkat() from os.link(), if available (GH-132517)

This fixes os.link() on platforms (like Linux and OpenIndiana) where the
system link() function does not follow symlinks.

* On Linux, it now follows symlinks by default and if
  follow_symlinks=True is specified.
* On Windows, it now raises error if follow_symlinks=True is passed.
* On macOS, it now raises error if follow_symlinks=False is passed and
  the system linkat() function is not available at runtime.
* On other platforms, it now raises error if follow_symlinks is passed
  with a value that does not match the system link() function behavior
  if if the behavior is not known.

Co-authored-by: Joachim Henke <37883863+jo...@users.noreply.github.com>
Co-authored-by: Thomas Kluyver <tak...@gmail.com>

files:
A Misc/NEWS.d/next/Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst
M Doc/library/os.rst
M Lib/test/test_inspect/test_inspect.py
M Lib/test/test_posix.py
M Modules/clinic/posixmodule.c.h
M Modules/posixmodule.c

diff --git a/Doc/library/os.rst b/Doc/library/os.rst
index 54a5d3b98e8662..1e54cfec609bd2 100644
--- a/Doc/library/os.rst
+++ b/Doc/library/os.rst
@@ -2338,6 +2338,7 @@ features:
    This function can support specifying *src_dir_fd* and/or *dst_dir_fd* to
    supply :ref:`paths relative to directory descriptors <dir_fd>`, and 
:ref:`not
    following symlinks <follow_symlinks>`.
+   The default value of *follow_symlinks* is ``False`` on Windows.
 
    .. audit-event:: os.link src,dst,src_dir_fd,dst_dir_fd os.link
 
diff --git a/Lib/test/test_inspect/test_inspect.py 
b/Lib/test/test_inspect/test_inspect.py
index 4950af42cfe5a4..c9b37fcd8f6327 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -5844,7 +5844,7 @@ def test_operator_module_has_signatures(self):
         self._test_module_has_signatures(operator)
 
     def test_os_module_has_signatures(self):
-        unsupported_signature = {'chmod', 'utime'}
+        unsupported_signature = {'chmod', 'link', 'utime'}
         unsupported_signature |= {name for name in
             ['get_terminal_size', 'posix_spawn', 'posix_spawnp',
              'register_at_fork', 'startfile']
diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py
index c9cbe1541e733e..bb18e49c9e3f82 100644
--- a/Lib/test/test_posix.py
+++ b/Lib/test/test_posix.py
@@ -1521,6 +1521,50 @@ def test_pidfd_open(self):
         self.assertEqual(cm.exception.errno, errno.EINVAL)
         os.close(os.pidfd_open(os.getpid(), 0))
 
+    @unittest.skipUnless(hasattr(os, "link"), "test needs os.link()")
+    def test_link_follow_symlinks(self):
+        default_follow = sys.platform.startswith(
+            ('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly', 'sunos5'))
+        default_no_follow = sys.platform.startswith(('win32', 'linux'))
+        orig = os_helper.TESTFN
+        symlink = orig + 'symlink'
+        posix.symlink(orig, symlink)
+        self.addCleanup(os_helper.unlink, symlink)
+
+        with self.subTest('no follow_symlinks'):
+            # no follow_symlinks -> platform depending
+            link = orig + 'link'
+            posix.link(symlink, link)
+            self.addCleanup(os_helper.unlink, link)
+            if os.link in os.supports_follow_symlinks or default_follow:
+                self.assertEqual(posix.lstat(link), posix.lstat(orig))
+            elif default_no_follow:
+                self.assertEqual(posix.lstat(link), posix.lstat(symlink))
+
+        with self.subTest('follow_symlinks=False'):
+            # follow_symlinks=False -> duplicate the symlink itself
+            link = orig + 'link_nofollow'
+            try:
+                posix.link(symlink, link, follow_symlinks=False)
+            except NotImplementedError:
+                if os.link in os.supports_follow_symlinks or default_no_follow:
+                    raise
+            else:
+                self.addCleanup(os_helper.unlink, link)
+                self.assertEqual(posix.lstat(link), posix.lstat(symlink))
+
+        with self.subTest('follow_symlinks=True'):
+            # follow_symlinks=True -> duplicate the target file
+            link = orig + 'link_following'
+            try:
+                posix.link(symlink, link, follow_symlinks=True)
+            except NotImplementedError:
+                if os.link in os.supports_follow_symlinks or default_follow:
+                    raise
+            else:
+                self.addCleanup(os_helper.unlink, link)
+                self.assertEqual(posix.lstat(link), posix.lstat(orig))
+
 
 # tests for the posix *at functions follow
 class TestPosixDirFd(unittest.TestCase):
diff --git 
a/Misc/NEWS.d/next/Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst 
b/Misc/NEWS.d/next/Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst
new file mode 100644
index 00000000000000..842e973b821744
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst
@@ -0,0 +1,7 @@
+Fix :func:`os.link` on platforms (like Linux) where the
+system :c:func:`!link` function does not follow symlinks. On Linux,
+it now follows symlinks by default or if
+``follow_symlinks=True`` is specified. On Windows, it now raises an error if
+``follow_symlinks=True`` is passed. On macOS, it now raises an error if
+``follow_symlinks=False`` is passed and the system :c:func:`!linkat`
+function is not available at runtime.
diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h
index 0125e247ee44d3..6b8cc3d07ab01c 100644
--- a/Modules/clinic/posixmodule.c.h
+++ b/Modules/clinic/posixmodule.c.h
@@ -1471,7 +1471,7 @@ os_getcwdb(PyObject *module, PyObject *Py_UNUSED(ignored))
 
 PyDoc_STRVAR(os_link__doc__,
 "link($module, /, src, dst, *, src_dir_fd=None, dst_dir_fd=None,\n"
-"     follow_symlinks=True)\n"
+"     follow_symlinks=(os.name != \'nt\'))\n"
 "--\n"
 "\n"
 "Create a hard link to a file.\n"
@@ -1530,7 +1530,7 @@ os_link(PyObject *module, PyObject *const *args, 
Py_ssize_t nargs, PyObject *kwn
     path_t dst = PATH_T_INITIALIZE_P("link", "dst", 0, 0, 0, 0);
     int src_dir_fd = DEFAULT_DIR_FD;
     int dst_dir_fd = DEFAULT_DIR_FD;
-    int follow_symlinks = 1;
+    int follow_symlinks = -1;
 
     args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
             /*minpos*/ 2, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@@ -13398,4 +13398,4 @@ os__emscripten_debugger(PyObject *module, PyObject 
*Py_UNUSED(ignored))
 #ifndef OS__EMSCRIPTEN_DEBUGGER_METHODDEF
     #define OS__EMSCRIPTEN_DEBUGGER_METHODDEF
 #endif /* !defined(OS__EMSCRIPTEN_DEBUGGER_METHODDEF) */
-/*[clinic end generated code: output=a5ca2541f2af5462 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=f7b5635e0b948be4 input=a9049054013a1b77]*/
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index 04c3b9e987aef8..964e988843110e 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -573,7 +573,11 @@ extern char *ctermid_r(char *);
 #  define HAVE_FACCESSAT_RUNTIME 1
 #  define HAVE_FCHMODAT_RUNTIME 1
 #  define HAVE_FCHOWNAT_RUNTIME 1
+#ifdef __wasi__
+#  define HAVE_LINKAT_RUNTIME 0
+# else
 #  define HAVE_LINKAT_RUNTIME 1
+# endif
 #  define HAVE_FDOPENDIR_RUNTIME 1
 #  define HAVE_MKDIRAT_RUNTIME 1
 #  define HAVE_RENAMEAT_RUNTIME 1
@@ -4346,7 +4350,7 @@ os.link
     *
     src_dir_fd : dir_fd = None
     dst_dir_fd : dir_fd = None
-    follow_symlinks: bool = True
+    follow_symlinks: bool(c_default="-1", py_default="(os.name != 'nt')") = 
PLACEHOLDER
 
 Create a hard link to a file.
 
@@ -4364,31 +4368,46 @@ src_dir_fd, dst_dir_fd, and follow_symlinks may not be 
implemented on your
 static PyObject *
 os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd,
              int dst_dir_fd, int follow_symlinks)
-/*[clinic end generated code: output=7f00f6007fd5269a input=b0095ebbcbaa7e04]*/
+/*[clinic end generated code: output=7f00f6007fd5269a input=1d5e602d115fed7b]*/
 {
 #ifdef MS_WINDOWS
     BOOL result = FALSE;
 #else
     int result;
 #endif
-#if defined(HAVE_LINKAT)
-    int linkat_unavailable = 0;
-#endif
 
-#ifndef HAVE_LINKAT
-    if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) {
-        argument_unavailable_error("link", "src_dir_fd and dst_dir_fd");
-        return NULL;
+#ifdef HAVE_LINKAT
+    if (HAVE_LINKAT_RUNTIME) {
+        if (follow_symlinks < 0) {
+            follow_symlinks = 1;
+        }
     }
+    else
 #endif
-
-#ifndef MS_WINDOWS
-    if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
-        PyErr_SetString(PyExc_NotImplementedError,
-                        "link: src and dst must be the same type");
-        return NULL;
-    }
+    {
+        if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) {
+            argument_unavailable_error("link", "src_dir_fd and dst_dir_fd");
+            return NULL;
+        }
+/* See issue 85527: link() on Linux works like linkat without 
AT_SYMLINK_FOLLOW,
+   but on Mac it works like linkat *with* AT_SYMLINK_FOLLOW. */
+#if defined(MS_WINDOWS) || defined(__linux__)
+        if (follow_symlinks == 1) {
+            argument_unavailable_error("link", "follow_symlinks=True");
+            return NULL;
+        }
+#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || 
defined(__OpenBSD__) || defined(__DragonFly__) || (defined(__sun) && 
defined(__SVR4))
+        if (follow_symlinks == 0) {
+            argument_unavailable_error("link", "follow_symlinks=False");
+            return NULL;
+        }
+#else
+        if (follow_symlinks >= 0) {
+            argument_unavailable_error("link", "follow_symlinks");
+            return NULL;
+        }
 #endif
+    }
 
     if (PySys_Audit("os.link", "OOii", src->object, dst->object,
                     src_dir_fd == DEFAULT_DIR_FD ? -1 : src_dir_fd,
@@ -4406,44 +4425,18 @@ os_link_impl(PyObject *module, path_t *src, path_t 
*dst, int src_dir_fd,
 #else
     Py_BEGIN_ALLOW_THREADS
 #ifdef HAVE_LINKAT
-    if ((src_dir_fd != DEFAULT_DIR_FD) ||
-        (dst_dir_fd != DEFAULT_DIR_FD) ||
-        (!follow_symlinks)) {
-
-        if (HAVE_LINKAT_RUNTIME) {
-
-            result = linkat(src_dir_fd, src->narrow,
-                dst_dir_fd, dst->narrow,
-                follow_symlinks ? AT_SYMLINK_FOLLOW : 0);
-
-        }
-#ifdef __APPLE__
-        else {
-            if (src_dir_fd == DEFAULT_DIR_FD && dst_dir_fd == DEFAULT_DIR_FD) {
-                /* See issue 41355: This matches the behaviour of !HAVE_LINKAT 
*/
-                result = link(src->narrow, dst->narrow);
-            } else {
-                linkat_unavailable = 1;
-            }
-        }
-#endif
+    if (HAVE_LINKAT_RUNTIME) {
+        result = linkat(src_dir_fd, src->narrow,
+            dst_dir_fd, dst->narrow,
+            follow_symlinks ? AT_SYMLINK_FOLLOW : 0);
     }
     else
-#endif /* HAVE_LINKAT */
+#endif
+    {
+        /* linkat not available */
         result = link(src->narrow, dst->narrow);
-    Py_END_ALLOW_THREADS
-
-#ifdef HAVE_LINKAT
-    if (linkat_unavailable) {
-        /* Either or both dir_fd arguments were specified */
-        if (src_dir_fd  != DEFAULT_DIR_FD) {
-            argument_unavailable_error("link", "src_dir_fd");
-        } else {
-            argument_unavailable_error("link", "dst_dir_fd");
-        }
-        return NULL;
     }
-#endif
+    Py_END_ALLOW_THREADS
 
     if (result)
         return path_error2(src, dst);
@@ -5935,12 +5928,6 @@ internal_rename(path_t *src, path_t *dst, int 
src_dir_fd, int dst_dir_fd, int is
         return path_error2(src, dst);
 
 #else
-    if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
-        PyErr_Format(PyExc_ValueError,
-                     "%s: src and dst must be the same type", function_name);
-        return NULL;
-    }
-
     Py_BEGIN_ALLOW_THREADS
 #ifdef HAVE_RENAMEAT
     if (dir_fd_specified) {
@@ -10613,12 +10600,6 @@ os_symlink_impl(PyObject *module, path_t *src, path_t 
*dst,
 
 #else
 
-    if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
-        PyErr_SetString(PyExc_ValueError,
-            "symlink: src and dst must be the same type");
-        return NULL;
-    }
-
     Py_BEGIN_ALLOW_THREADS
 #ifdef HAVE_SYMLINKAT
     if (dir_fd != DEFAULT_DIR_FD) {

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to