From 40fee16093171f283c7feaa5a0a39b8d6dbc1994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Sat, 17 Jan 2026 23:18:58 +0800 Subject: [PATCH 1/4] [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. --- Lib/pathlib/__init__.py | 7 ++++++- Lib/test/test_pathlib/test_copy.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 44f967eb12dd4f..c03a010aa96915 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1328,8 +1328,13 @@ def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False): child, follow_symlinks, preserve_metadata) if preserve_metadata: _copy_info(source.info, self) - else: + elif source.info.is_file(): + self._copy_from_file(source, preserve_metadata) + elif not source.info.exists(follow_symlinks=follow_symlinks): self._copy_from_file(source, preserve_metadata) + else: + raise io.UnsupportedOperation( + f"{source!r} is a special file and cannot be copied") def _copy_from_file(self, source, preserve_metadata=False): ensure_different_files(source, self) diff --git a/Lib/test/test_pathlib/test_copy.py b/Lib/test/test_pathlib/test_copy.py index 5f4cf82a0314c4..38a454ac0e0787 100644 --- a/Lib/test/test_pathlib/test_copy.py +++ b/Lib/test/test_pathlib/test_copy.py @@ -169,6 +169,36 @@ class LocalToLocalPathCopyTest(CopyTestBase, unittest.TestCase): source_ground = LocalPathGround(Path) target_ground = LocalPathGround(Path) + def test_copy_fifo(self): + import io + import os + if not hasattr(os, 'mkfifo'): + self.skipTest('os.mkfifo() required') + + source = self.source_root / 'test.fifo' + try: + os.mkfifo(source) + except OSError: + self.skipTest("cannot create fifo") + + target = self.target_root / 'copy_fifo' + with self.assertRaises(io.UnsupportedOperation): + source.copy(target) + + def test_copy_char_device(self): + import io + import os + # /dev/null is a character device on POSIX + source = Path('/dev/null') + if not source.exists() or not source.is_char_device(): + self.skipTest('/dev/null required') + + target = self.target_root / 'copy_null' + # This should fail immediately with UnsupportedOperation + # If it were buggy, it might loop infinitely or copy empty file depending on implementation + with self.assertRaises(io.UnsupportedOperation): + source.copy(target) + if __name__ == "__main__": unittest.main() From f3d9d25080b66f8d4899af5170e82221d81252b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Sat, 17 Jan 2026 23:34:46 +0800 Subject: [PATCH 2/4] Fix trailing whitespace checks --- Lib/test/test_pathlib/test_copy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_pathlib/test_copy.py b/Lib/test/test_pathlib/test_copy.py index 38a454ac0e0787..2c885d36aa8a93 100644 --- a/Lib/test/test_pathlib/test_copy.py +++ b/Lib/test/test_pathlib/test_copy.py @@ -174,15 +174,15 @@ def test_copy_fifo(self): import os if not hasattr(os, 'mkfifo'): self.skipTest('os.mkfifo() required') - + source = self.source_root / 'test.fifo' try: os.mkfifo(source) except OSError: self.skipTest("cannot create fifo") - + target = self.target_root / 'copy_fifo' - with self.assertRaises(io.UnsupportedOperation): + with self.assertRaises(io.UnsupportedOperation): source.copy(target) def test_copy_char_device(self): @@ -192,7 +192,7 @@ def test_copy_char_device(self): source = Path('/dev/null') if not source.exists() or not source.is_char_device(): self.skipTest('/dev/null required') - + target = self.target_root / 'copy_null' # This should fail immediately with UnsupportedOperation # If it were buggy, it might loop infinitely or copy empty file depending on implementation From 895527c948aade8c55e6291e31057f47197c9944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Sat, 17 Jan 2026 23:38:06 +0800 Subject: [PATCH 3/4] Fix lint: remove unused import os --- Lib/test/test_pathlib/test_copy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_pathlib/test_copy.py b/Lib/test/test_pathlib/test_copy.py index 2c885d36aa8a93..2a6bc824f3ed7e 100644 --- a/Lib/test/test_pathlib/test_copy.py +++ b/Lib/test/test_pathlib/test_copy.py @@ -187,7 +187,6 @@ def test_copy_fifo(self): def test_copy_char_device(self): import io - import os # /dev/null is a character device on POSIX source = Path('/dev/null') if not source.exists() or not source.is_char_device(): From 3e9c566b43cb21ebee74191d51062a22d9dbf705 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:49:05 +0000 Subject: [PATCH 4/4] =?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/2026-01-17-15-49-04.gh-issue-143968.5R_yPR.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-01-17-15-49-04.gh-issue-143968.5R_yPR.rst diff --git a/Misc/NEWS.d/next/Library/2026-01-17-15-49-04.gh-issue-143968.5R_yPR.rst b/Misc/NEWS.d/next/Library/2026-01-17-15-49-04.gh-issue-143968.5R_yPR.rst new file mode 100644 index 00000000000000..eba8d2cd0dc18c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-17-15-49-04.gh-issue-143968.5R_yPR.rst @@ -0,0 +1 @@ +Prevent :meth:`pathlib.Path.copy` from attempting to copy special files like FIFOs or character devices, avoiding hangs or infinite loops.