]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-154726: Fix `shutil.copyfile()` for symlinks to special files with `follow_symlink...
authorŁukasz <lukaszlapinski7@gmail.com>
Fri, 31 Jul 2026 11:36:05 +0000 (13:36 +0200)
committerGitHub <noreply@github.com>
Fri, 31 Jul 2026 11:36:05 +0000 (13:36 +0200)
Lib/shutil.py
Lib/test/test_shutil.py
Misc/NEWS.d/next/Library/2026-07-26-11-45-34.gh-issue-154726.hLJk5-.rst [new file with mode: 0644]

index 6a2e2b2ffdae2c09ceb16756c5ec27462d2c80b0..94617ec296f5087e2cb1317cc52cd54f4fc7713c 100644 (file)
@@ -292,8 +292,11 @@ def copyfile(src, dst, *, follow_symlinks=True):
     if _samefile(src, dst):
         raise SameFileError("{!r} and {!r} are the same file".format(src, dst))
 
+    copy_symlink = not follow_symlinks and _islink(src)
     file_size = 0
     for i, fn in enumerate([src, dst]):
+        if copy_symlink and i == 0:
+            continue
         try:
             st = _stat(fn)
         except OSError:
@@ -315,7 +318,7 @@ def copyfile(src, dst, *, follow_symlinks=True):
             if _WINDOWS and i == 0:
                 file_size = st.st_size
 
-    if not follow_symlinks and _islink(src):
+    if copy_symlink:
         os.symlink(os.readlink(src), dst)
     else:
         with open(src, 'rb') as fsrc:
index 6832bea094fc1dc0e0fb96736e5c0a8cbdc9deea..ed5d15ecc7ddad6ec81396b9d5b3092e78f41ab6 100644 (file)
@@ -1574,6 +1574,47 @@ class TestCopy(BaseTest, unittest.TestCase):
         self.assertRaisesRegex(shutil.SpecialFileError, 'is a socket',
                                shutil.copyfile, __file__, sock_path)
 
+    def _check_copyfile_symlink_to_special_file(self, target):
+        tmp_dir = self.mkdtemp()
+        src = os.path.join(tmp_dir, 'src')
+        dst = os.path.join(tmp_dir, 'dst')
+        os.symlink(target, src)
+
+        shutil.copyfile(src, dst, follow_symlinks=False)
+
+        self.assertTrue(os.path.islink(dst))
+        self.assertEqual(os.readlink(dst), target)
+
+    @os_helper.skip_unless_symlink
+    @unittest.skipUnless(os.path.exists('/dev/null'), 'requires /dev/null')
+    def test_copyfile_symlink_to_character_device(self):
+        self._check_copyfile_symlink_to_special_file('/dev/null')
+
+    @os_helper.skip_unless_symlink
+    @unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
+    @unittest.skipIf(sys.platform == "vxworks",
+                    "fifo requires special path on VxWorks")
+    def test_copyfile_symlink_to_named_pipe(self):
+        fifo_path = os.path.join(self.mkdtemp(), 'fifo')
+        try:
+            os.mkfifo(fifo_path)
+        except PermissionError as e:
+            self.skipTest('os.mkfifo(): %s' % e)
+        self._check_copyfile_symlink_to_special_file(fifo_path)
+
+    @os_helper.skip_unless_symlink
+    @socket_helper.skip_unless_bind_unix_socket
+    def test_copyfile_symlink_to_socket(self):
+        sock_path = os.path.join(self.mkdtemp(), 'sock')
+        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        self.addCleanup(sock.close)
+        try:
+            socket_helper.bind_unix_socket(sock, sock_path)
+        except OSError as e:
+            self.skipTest(f'cannot bind AF_UNIX socket: {e}')
+        self.addCleanup(os_helper.unlink, sock_path)
+        self._check_copyfile_symlink_to_special_file(sock_path)
+
     @unittest.skipUnless(os.path.exists('/dev/null'), 'requires /dev/null')
     def test_copyfile_character_device(self):
         self.assertRaisesRegex(shutil.SpecialFileError, 'is a character device',
diff --git a/Misc/NEWS.d/next/Library/2026-07-26-11-45-34.gh-issue-154726.hLJk5-.rst b/Misc/NEWS.d/next/Library/2026-07-26-11-45-34.gh-issue-154726.hLJk5-.rst
new file mode 100644 (file)
index 0000000..73cf40a
--- /dev/null
@@ -0,0 +1,3 @@
+Fix :func:`shutil.copyfile` to copy a symbolic link to a special file when
+``follow_symlinks=False`` instead of raising
+:exc:`~shutil.SpecialFileError`.