Bug report
Bug description:
Summary
shutil.copytree(..., symlinks=False, ignore_dangling_symlinks=True) tests a relative symlink target against the process current working directory rather than against the directory containing the symlink. Consequently, a valid relative symlink can be silently omitted from a successful copy, and a truly dangling symlink can still cause shutil.Error when the current working directory happens to contain a file with the same name as its target.
Affected code
Lib/shutil.py, _copytree():
linkto = os.readlink(srcname)
...
if not os.path.exists(linkto) and ignore_dangling_symlinks:
continue
os.readlink() returns the raw target. A relative target is interpreted by the operating system relative to the symlink's parent directory, but os.path.exists(linkto) interprets it relative to the process current working
directory.
Reproducer
import os
import shutil
import tempfile
with tempfile.TemporaryDirectory() as root:
src = os.path.join(root, "src")
dst = os.path.join(root, "dst")
os.mkdir(src)
with open(os.path.join(src, "target"), "w") as f:
f.write("data")
os.symlink("target", os.path.join(src, "link"))
old_cwd = os.getcwd()
try:
os.chdir(root) # root/target does not exist
shutil.copytree(src, dst, ignore_dangling_symlinks=True)
print("exception: none")
finally:
os.chdir(old_cwd)
print("source link exists?", os.path.lexists(os.path.join(src, "link")))
print("destination entries:", sorted(os.listdir(dst)))
print("destination link exists?", os.path.lexists(os.path.join(dst, "link")))
Actual output:
exception: none
source link exists? True
destination entries: ['target']
destination link exists? False
The source symlink is valid: src/link -> target resolves to src/target. With symlinks=False, the destination should contain a regular link file with the contents of src/target, in addition to target. Instead, the operation returns successfully with an incomplete destination tree.
The inverse case also fails: if src/link -> target is actually dangling but the current working directory contains target, it is not ignored and copytree() raises shutil.Error after creating the destination directory.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Linked PRs
Bug report
Bug description:
Summary
shutil.copytree(..., symlinks=False, ignore_dangling_symlinks=True)tests a relative symlink target against the process current working directory rather than against the directory containing the symlink. Consequently, a valid relative symlink can be silently omitted from a successful copy, and a truly dangling symlink can still causeshutil.Errorwhen the current working directory happens to contain a file with the same name as its target.Affected code
Lib/shutil.py,_copytree():os.readlink()returns the raw target. A relative target is interpreted by the operating system relative to the symlink's parent directory, butos.path.exists(linkto)interprets it relative to the process current workingdirectory.
Reproducer
Actual output:
The source symlink is valid:
src/link -> targetresolves tosrc/target. Withsymlinks=False, the destination should contain a regularlinkfile with the contents ofsrc/target, in addition totarget. Instead, the operation returns successfully with an incomplete destination tree.The inverse case also fails: if
src/link -> targetis actually dangling but the current working directory containstarget, it is not ignored andcopytree()raisesshutil.Errorafter creating the destination directory.CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Linked PRs
shutil.copytree()detection of dangling relative symlinks #156214