Skip to content

Commit 79efbb7

Browse files
obilaniugiampaolo
authored andcommitted
bpo-24538: Fix bug in shutil involving the copying of xattrs to read-only files. (PR-13212)
Extended attributes can only be set on user-writeable files, but shutil previously first chmod()ed the destination file to the source's permissions and then tried to copy xattrs. This will cause failures if attempting to copy read-only files with xattrs, as occurs with Git clones on Lustre FS.
1 parent 948ed8c commit 79efbb7

File tree

4 files changed

+15
-1
lines changed

4 files changed

+15
-1
lines changed

Lib/shutil.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,9 @@ def lookup(name):
359359
mode = stat.S_IMODE(st.st_mode)
360360
lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),
361361
follow_symlinks=follow)
362+
# We must copy extended attributes before the file is (potentially)
363+
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
364+
_copyxattr(src, dst, follow_symlinks=follow)
362365
try:
363366
lookup("chmod")(dst, mode, follow_symlinks=follow)
364367
except NotImplementedError:
@@ -382,7 +385,6 @@ def lookup(name):
382385
break
383386
else:
384387
raise
385-
_copyxattr(src, dst, follow_symlinks=follow)
386388

387389
def copy(src, dst, *, follow_symlinks=True):
388390
"""Copy data and mode bits ("cp src dst"). Return the file's destination.

Lib/test/test_shutil.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,12 +531,20 @@ def _raise_on_src(fname, *, follow_symlinks=True):
531531

532532
# test that shutil.copystat copies xattrs
533533
src = os.path.join(tmp_dir, 'the_original')
534+
srcro = os.path.join(tmp_dir, 'the_original_ro')
534535
write_file(src, src)
536+
write_file(srcro, srcro)
535537
os.setxattr(src, 'user.the_value', b'fiddly')
538+
os.setxattr(srcro, 'user.the_value', b'fiddly')
539+
os.chmod(srcro, 0o444)
536540
dst = os.path.join(tmp_dir, 'the_copy')
541+
dstro = os.path.join(tmp_dir, 'the_copy_ro')
537542
write_file(dst, dst)
543+
write_file(dstro, dstro)
538544
shutil.copystat(src, dst)
545+
shutil.copystat(srcro, dstro)
539546
self.assertEqual(os.getxattr(dst, 'user.the_value'), b'fiddly')
547+
self.assertEqual(os.getxattr(dstro, 'user.the_value'), b'fiddly')
540548

541549
@support.skip_unless_symlink
542550
@support.skip_unless_xattr

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ Stephen Bevan
149149
Ron Bickers
150150
Natalia B. Bidart
151151
Adrian von Bidder
152+
Olexa Bilaniuk
152153
David Binger
153154
Dominic Binks
154155
Philippe Biondi
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In `shutil.copystat()`, first copy extended file attributes and then file
2+
permissions, since extended attributes can only be set on the destination
3+
while it is still writeable.

0 commit comments

Comments
 (0)