From 582968a5e8e8cd9a2957cf53afbf786139cfb585 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Sat, 13 Dec 2025 22:41:34 -0800 Subject: [PATCH 1/5] Make socket and devices return SpecialFileError --- Doc/library/shutil.rst | 9 ++++++--- Doc/whatsnew/3.15.rst | 8 ++++++++ Lib/shutil.py | 10 +++++++++- Lib/test/test_shutil.py | 44 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index 3a4631e7c657fe..fcfb9b89c1520b 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -67,8 +67,8 @@ Directory and files operations The destination location must be writable; otherwise, an :exc:`OSError` exception will be raised. If *dst* already exists, it will be replaced. - Special files such as character or block devices and pipes cannot be - copied with this function. + Special files such as character or block devices, pipes, and sockets cannot + be copied with this function. If *follow_symlinks* is false and *src* is a symbolic link, a new symbolic link will be created instead of copying the @@ -90,10 +90,13 @@ Directory and files operations copy the file more efficiently. See :ref:`shutil-platform-dependent-efficient-copy-operations` section. + .. versionchanged:: 3.15 + :exc:`SpecialFileError` is now also raised for sockets and device files. + .. exception:: SpecialFileError This exception is raised when :func:`copyfile` or :func:`copytree` attempt - to copy a named pipe. + to copy a named pipe, socket, or device file. .. versionadded:: 2.7 diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index a94486dd4805bd..e466c83d43e53a 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -630,6 +630,14 @@ shelve (Contributed by Andrea Oliveri in :gh:`134004`.) +shutil +------ + +* :func:`shutil.copyfile` now also raises :exc:`~shutil.SpecialFileError` for + sockets and device files. + (Contributed by Savannah Ostrowski in :gh:`81881`.) + + socket ------ diff --git a/Lib/shutil.py b/Lib/shutil.py index 8d8fe145567822..2b45fbbf5ad7aa 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -300,10 +300,18 @@ def copyfile(src, dst, *, follow_symlinks=True): # File most likely does not exist pass else: - # XXX What about other special files? (sockets, devices...) if stat.S_ISFIFO(st.st_mode): fn = fn.path if isinstance(fn, os.DirEntry) else fn raise SpecialFileError("`%s` is a named pipe" % fn) + elif stat.S_ISSOCK(st.st_mode): + fn = fn.path if isinstance(fn, os.DirEntry) else fn + raise SpecialFileError("`%s` is a socket" % fn) + elif stat.S_ISBLK(st.st_mode): + fn = fn.path if isinstance(fn, os.DirEntry) else fn + raise SpecialFileError("`%s` is a block device" % fn) + elif stat.S_ISCHR(st.st_mode): + fn = fn.path if isinstance(fn, os.DirEntry) else fn + raise SpecialFileError("`%s` is a character device" % fn) if _WINDOWS and i == 0: file_size = st.st_size diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index ebb6cf88336249..2d7c1804a02495 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -10,6 +10,7 @@ import os.path import errno import functools +import socket import subprocess import random import string @@ -29,7 +30,7 @@ posix = None from test import support -from test.support import os_helper +from test.support import os_helper, socket_helper from test.support.os_helper import TESTFN, FakePath TESTFN2 = TESTFN + "2" @@ -1550,13 +1551,46 @@ def test_copyfile_named_pipe(self): except PermissionError as e: self.skipTest('os.mkfifo(): %s' % e) try: - self.assertRaises(shutil.SpecialFileError, - shutil.copyfile, TESTFN, TESTFN2) - self.assertRaises(shutil.SpecialFileError, - shutil.copyfile, __file__, TESTFN) + self.assertRaisesRegex(shutil.SpecialFileError, 'is a named pipe', + shutil.copyfile, TESTFN, TESTFN2) + self.assertRaisesRegex(shutil.SpecialFileError, 'is a named pipe', + shutil.copyfile, __file__, TESTFN) finally: os.remove(TESTFN) + @socket_helper.skip_unless_bind_unix_socket + def test_copyfile_socket(self): + sock_path = os.path.join(self.mkdtemp(), 'sock') + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.addCleanup(sock.close) + socket_helper.bind_unix_socket(sock, sock_path) + self.addCleanup(os_helper.unlink, sock_path) + self.assertRaisesRegex(shutil.SpecialFileError, 'is a socket', + shutil.copyfile, sock_path, sock_path + '.copy') + self.assertRaisesRegex(shutil.SpecialFileError, 'is a socket', + shutil.copyfile, __file__, sock_path) + + @unittest.skipIf(os.name == 'nt', 'requires /dev/null') + def test_copyfile_character_device(self): + self.assertRaisesRegex(shutil.SpecialFileError, 'is a character device', + shutil.copyfile, '/dev/null', TESTFN) + src_file = os.path.join(self.mkdtemp(), 'src') + create_file(src_file, 'foo') + self.assertRaisesRegex(shutil.SpecialFileError, 'is a character device', + shutil.copyfile, src_file, '/dev/null') + + def test_copyfile_block_device(self): + block_dev = None + for dev in ['/dev/loop0', '/dev/sda', '/dev/vda', '/dev/disk0']: + if os.path.exists(dev) and stat.S_ISBLK(os.stat(dev).st_mode): + if os.access(dev, os.R_OK): + block_dev = dev + break + if block_dev is None: + self.skipTest('no accessible block device found') + self.assertRaisesRegex(shutil.SpecialFileError, 'is a block device', + shutil.copyfile, block_dev, TESTFN) + def test_copyfile_return_value(self): # copytree returns its destination path. src_dir = self.mkdtemp() From 841e44d08070c3990074024a6e99a33abeea7b01 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 06:44:38 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-12-14-06-44-34.gh-issue-81881.qancdQ.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-12-14-06-44-34.gh-issue-81881.qancdQ.rst diff --git a/Misc/NEWS.d/next/Library/2025-12-14-06-44-34.gh-issue-81881.qancdQ.rst b/Misc/NEWS.d/next/Library/2025-12-14-06-44-34.gh-issue-81881.qancdQ.rst new file mode 100644 index 00000000000000..6b6ca6a1918bdd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-14-06-44-34.gh-issue-81881.qancdQ.rst @@ -0,0 +1 @@ +:func:`shutil.copyfile` now raises :exc:`~shutil.SpecialFileError` for sockets and device files. From ed30a99c58dcaa8e0e4d2d772b57e27ae58d3141 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Sat, 13 Dec 2025 22:45:19 -0800 Subject: [PATCH 3/5] Fix whats new --- Doc/whatsnew/3.15.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index e466c83d43e53a..c510552a049add 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -635,7 +635,7 @@ shutil * :func:`shutil.copyfile` now also raises :exc:`~shutil.SpecialFileError` for sockets and device files. - (Contributed by Savannah Ostrowski in :gh:`81881`.) + (Contributed by Savannah Ostrowski in :gh:`142693`.) socket From 6c20f232e84f4db979cfe176ceefda4db2694d3e Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Sun, 14 Dec 2025 10:03:04 -0800 Subject: [PATCH 4/5] Fix failing test --- Lib/test/test_shutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 2d7c1804a02495..95ad4a531dcac8 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1570,7 +1570,7 @@ def test_copyfile_socket(self): self.assertRaisesRegex(shutil.SpecialFileError, 'is a socket', shutil.copyfile, __file__, sock_path) - @unittest.skipIf(os.name == 'nt', 'requires /dev/null') + @unittest.skipUnless(os.path.exists('/dev/null'), 'requires /dev/null') def test_copyfile_character_device(self): self.assertRaisesRegex(shutil.SpecialFileError, 'is a character device', shutil.copyfile, '/dev/null', TESTFN) From c9827dade05fd3f1e9f47f6f9116454dc1a3e50a Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Sun, 14 Dec 2025 10:45:46 -0800 Subject: [PATCH 5/5] Fix iOS socket path --- Lib/test/test_shutil.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 95ad4a531dcac8..e435da61014121 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1563,7 +1563,11 @@ def test_copyfile_socket(self): sock_path = os.path.join(self.mkdtemp(), 'sock') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.addCleanup(sock.close) - socket_helper.bind_unix_socket(sock, sock_path) + try: + socket_helper.bind_unix_socket(sock, sock_path) + except OSError as e: + # AF_UNIX path too long (e.g. on iOS) + self.skipTest(f'cannot bind AF_UNIX socket: {e}') self.addCleanup(os_helper.unlink, sock_path) self.assertRaisesRegex(shutil.SpecialFileError, 'is a socket', shutil.copyfile, sock_path, sock_path + '.copy')