Skip to content

Commit 40fee16

Browse files
committed
[pathlib] Prevent Path.copy() from processing special files
Previously, `pathlib.Path.copy()` would indiscriminately attempt to open and read from the source path if it wasn't a directory or symlink. This caused severe issues with special files: 1. Blocking indefinitely when reading from a FIFO (named pipe). 2. Entering an infinite loop when reading from zero-generators like `/dev/zero`. This commit modifies the copy logic to explicitly verify that the source is a regular file using `is_file()` before attempting to copy its content. If the source exists but is a special file, `io.UnsupportedOperation` is now raised. This change aligns `pathlib`'s behavior with safety expectations and prevents resource exhaustion or hanging processes. Existing behavior for dangling symlinks (raising FileNotFoundError) is preserved. Tests added: - `test_copy_fifo`: Ensures copying a FIFO raises UnsupportedOperation. - `test_copy_char_device`: Ensures copying a character device (e.g. /dev/null) raises UnsupportedOperation.
1 parent 7e28ae5 commit 40fee16

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

Lib/pathlib/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1328,8 +1328,13 @@ def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False):
13281328
child, follow_symlinks, preserve_metadata)
13291329
if preserve_metadata:
13301330
_copy_info(source.info, self)
1331-
else:
1331+
elif source.info.is_file():
1332+
self._copy_from_file(source, preserve_metadata)
1333+
elif not source.info.exists(follow_symlinks=follow_symlinks):
13321334
self._copy_from_file(source, preserve_metadata)
1335+
else:
1336+
raise io.UnsupportedOperation(
1337+
f"{source!r} is a special file and cannot be copied")
13331338

13341339
def _copy_from_file(self, source, preserve_metadata=False):
13351340
ensure_different_files(source, self)

Lib/test/test_pathlib/test_copy.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,36 @@ class LocalToLocalPathCopyTest(CopyTestBase, unittest.TestCase):
169169
source_ground = LocalPathGround(Path)
170170
target_ground = LocalPathGround(Path)
171171

172+
def test_copy_fifo(self):
173+
import io
174+
import os
175+
if not hasattr(os, 'mkfifo'):
176+
self.skipTest('os.mkfifo() required')
177+
178+
source = self.source_root / 'test.fifo'
179+
try:
180+
os.mkfifo(source)
181+
except OSError:
182+
self.skipTest("cannot create fifo")
183+
184+
target = self.target_root / 'copy_fifo'
185+
with self.assertRaises(io.UnsupportedOperation):
186+
source.copy(target)
187+
188+
def test_copy_char_device(self):
189+
import io
190+
import os
191+
# /dev/null is a character device on POSIX
192+
source = Path('/dev/null')
193+
if not source.exists() or not source.is_char_device():
194+
self.skipTest('/dev/null required')
195+
196+
target = self.target_root / 'copy_null'
197+
# This should fail immediately with UnsupportedOperation
198+
# If it were buggy, it might loop infinitely or copy empty file depending on implementation
199+
with self.assertRaises(io.UnsupportedOperation):
200+
source.copy(target)
201+
172202

173203
if __name__ == "__main__":
174204
unittest.main()

0 commit comments

Comments
 (0)