]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-81881: Raise `SpecialFileError` for sockets and devices in `shutil.copyfile` ...
authorSavannah Ostrowski <savannah@python.org>
Tue, 30 Jun 2026 19:50:41 +0000 (12:50 -0700)
committerGitHub <noreply@github.com>
Tue, 30 Jun 2026 19:50:41 +0000 (19:50 +0000)
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Senthil Kumaran <senthil@python.org>
Doc/library/shutil.rst
Doc/whatsnew/3.15.rst
Lib/shutil.py
Lib/test/test_shutil.py
Misc/NEWS.d/next/Library/2025-12-14-06-44-34.gh-issue-81881.qancdQ.rst [new file with mode: 0644]

index 6febc7a187a15f841fe4ac45a2e24d679b6f1d6e..2822e979990c4de1faeaf048b8b640077a31b9ff 100644 (file)
@@ -64,8 +64,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
@@ -87,10 +87,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
 
index a90f986b7354c151c8230f29ede1b6f74af16375..aad4758297c30d18f8d4c6c72de6ebf7dcac8095 100644 (file)
@@ -1398,6 +1398,14 @@ shelve
   (Contributed by Furkan Onder in :gh:`99631`.)
 
 
+shutil
+------
+
+* :func:`shutil.copyfile` now also raises :exc:`~shutil.SpecialFileError` for
+  sockets and device files.
+  (Contributed by Savannah Ostrowski in :gh:`142693`.)
+
+
 socket
 ------
 
index 5095318da2331461e06b56d905a462381f9e5e69..6a2e2b2ffdae2c09ceb16756c5ec27462d2c80b0 100644 (file)
@@ -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
 
index bb901220fb408c13b887705c8e1a655b12140a93..6832bea094fc1dc0e0fb96736e5c0a8cbdc9deea 100644 (file)
@@ -10,6 +10,7 @@ import os
 import os.path
 import errno
 import functools
+import socket
 import subprocess
 import random
 import string
@@ -29,7 +30,7 @@ except ImportError:
     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,50 @@ class TestCopy(BaseTest, unittest.TestCase):
         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)
+        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')
+        self.assertRaisesRegex(shutil.SpecialFileError, 'is a socket',
+                               shutil.copyfile, __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',
+                               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()
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 (file)
index 0000000..6b6ca6a
--- /dev/null
@@ -0,0 +1 @@
+:func:`shutil.copyfile` now raises :exc:`~shutil.SpecialFileError` for sockets and device files.