https://github.com/python/cpython/commit/f5afa895ae322ea89c5942b0b21094c124293dd8
commit: f5afa895ae322ea89c5942b0b21094c124293dd8
branch: main
author: Ćukasz <[email protected]>
committer: encukou <[email protected]>
date: 2026-07-31T13:36:05+02:00
summary:
gh-154726: Fix `shutil.copyfile()` for symlinks to special files with
`follow_symlinks=False` (GH-154728)
files:
A Misc/NEWS.d/next/Library/2026-07-26-11-45-34.gh-issue-154726.hLJk5-.rst
M Lib/shutil.py
M Lib/test/test_shutil.py
diff --git a/Lib/shutil.py b/Lib/shutil.py
index 6a2e2b2ffdae2c..94617ec296f508 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -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:
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
index 6832bea094fc1d..ed5d15ecc7ddad 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -1574,6 +1574,47 @@ def test_copyfile_socket(self):
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
index 00000000000000..73cf40a7207743
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-26-11-45-34.gh-issue-154726.hLJk5-.rst
@@ -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`.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]