diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 79a1969d5f83d6d..b91a1228a122f63 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -127,7 +127,7 @@ def copyfileobj(source_f, target_f): _ficlone(source_fd, target_fd) return except OSError as err: - if err.errno not in (EBADF, EOPNOTSUPP, ETXTBSY, EXDEV): + if err.errno not in (EBADF, EOPNOTSUPP, EPERM, ETXTBSY, EXDEV): raise err # Use OS copy where available. diff --git a/Lib/test/test_pathlib/test_copy.py b/Lib/test/test_pathlib/test_copy.py index 5f4cf82a0314c46..e798d84ecdf58ec 100644 --- a/Lib/test/test_pathlib/test_copy.py +++ b/Lib/test/test_pathlib/test_copy.py @@ -170,5 +170,31 @@ class LocalToLocalPathCopyTest(CopyTestBase, unittest.TestCase): target_ground = LocalPathGround(Path) +class CopyFileobjFallbackTest(unittest.TestCase): + def test_ficlone_eperm_falls_back(self): + # FICLONE can fail with EPERM on filesystems that block reflinks + # (e.g. overlayfs, or an ioctl blocked by SELinux). copyfileobj + # must fall back to another copy method instead of failing. + import errno + import tempfile + import unittest.mock + from pathlib import _os as pathlib_os + + def failing_ficlone(source_fd, target_fd): + raise OSError(errno.EPERM, 'Operation not permitted') + + with tempfile.TemporaryDirectory() as tmpdir: + source = f'{tmpdir}/source.txt' + target = f'{tmpdir}/target.txt' + with open(source, 'wb') as f: + f.write(b'hello world') + with open(source, 'rb') as source_f, open(target, 'wb') as target_f: + with unittest.mock.patch.object( + pathlib_os, '_ficlone', failing_ficlone): + pathlib_os.copyfileobj(source_f, target_f) + with open(target, 'rb') as f: + self.assertEqual(f.read(), b'hello world') + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-11-03-14-30-00.gh-issue-161241.abcdef.rst b/Misc/NEWS.d/next/Library/2026-11-03-14-30-00.gh-issue-161241.abcdef.rst new file mode 100644 index 000000000000000..aff9c6255d8eabb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-03-14-30-00.gh-issue-161241.abcdef.rst @@ -0,0 +1 @@ +Fix pathlib.Path.copy failing with PermissionError when the FICLONE reflink ioctl returns EPERM; it now falls back to other copy methods.