From 53f91b2b23a6ba1116905532512cddacd9254fb4 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Tue, 18 Nov 2025 12:19:05 +0300 Subject: [PATCH] gh-42948: Fix shutil.move() on permission-restricted filesystems --- Lib/shutil.py | 9 +++- Lib/test/test_shutil.py | 46 +++++++++++++++++++ ...5-11-18-07-50-00.gh-issue-42948.uT9xKl.rst | 3 ++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-11-18-07-50-00.gh-issue-42948.uT9xKl.rst diff --git a/Lib/shutil.py b/Lib/shutil.py index 8d8fe145567822..e48135e733d341 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -443,8 +443,13 @@ def lookup(name): else: st = lookup("stat")(src, follow_symlinks=follow) mode = stat.S_IMODE(st.st_mode) - lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns), - follow_symlinks=follow) + try: + lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns), + follow_symlinks=follow) + except OSError as e: + # Ignore permission errors when setting file times (gh-42948). + if e.errno not in (errno.EPERM, errno.EACCES): + raise # We must copy extended attributes before the file is (potentially) # chmod()'ed read-only, otherwise setxattr() will error with -EACCES. _copyxattr(src, dst, follow_symlinks=follow) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index ebb6cf88336249..2ffeb4e0802298 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1251,6 +1251,52 @@ def _chflags_raiser(path, flags, *, follow_symlinks=True): finally: os.chflags = old_chflags + def test_copystat_handles_utime_errors(self): + # gh-42948: copystat should ignore permission errors when setting times + tmpdir = self.mkdtemp() + file1 = os.path.join(tmpdir, 'file1') + file2 = os.path.join(tmpdir, 'file2') + create_file(file1, 'xxx') + create_file(file2, 'xxx') + + def make_utime_raiser(err): + def _utime_raiser(path, times=None, *, ns=None, dir_fd=None, + follow_symlinks=True): + ex = OSError() + ex.errno = err + raise ex + return _utime_raiser + + for err in errno.EPERM, errno.EACCES: + with unittest.mock.patch('os.utime', side_effect=make_utime_raiser(err)): + shutil.copystat(file1, file2) + + with unittest.mock.patch('os.utime', side_effect=make_utime_raiser(errno.EINVAL)): + self.assertRaises(OSError, shutil.copystat, file1, file2) + + @mock_rename + def test_move_handles_utime_errors(self): + # gh-42948: move should succeed despite utime permission errors + src_dir = self.mkdtemp() + dst_dir = self.mkdtemp() + src_file = os.path.join(src_dir, 'file') + dst_file = os.path.join(dst_dir, 'file') + create_file(src_file, 'content') + + def _utime_raiser(path, times=None, *, ns=None, dir_fd=None, + follow_symlinks=True): + ex = OSError() + ex.errno = errno.EPERM + raise ex + + with unittest.mock.patch('os.utime', side_effect=_utime_raiser): + result = shutil.move(src_file, dst_file) + self.assertEqual(result, dst_file) + self.assertTrue(os.path.exists(dst_file)) + self.assertFalse(os.path.exists(src_file)) + with open(dst_file) as f: + self.assertEqual(f.read(), 'content') + ### shutil.copyxattr @os_helper.skip_unless_xattr diff --git a/Misc/NEWS.d/next/Library/2025-11-18-07-50-00.gh-issue-42948.uT9xKl.rst b/Misc/NEWS.d/next/Library/2025-11-18-07-50-00.gh-issue-42948.uT9xKl.rst new file mode 100644 index 00000000000000..59203a153d5a96 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-18-07-50-00.gh-issue-42948.uT9xKl.rst @@ -0,0 +1,3 @@ +:func:`shutil.copystat` now ignores permission errors when setting file +times. This fixes :func:`shutil.move` failures on filesystems where the user +has write access but is not the owner. Patch by Shamil Abdulaev.