python / python/cpython

`shutil.copytree()` incorrectly classifies relative symlinks as dangling

Open
#156,210 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

stdlib type-bug
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

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
  • gh-156214

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in Lib/shutil.py at _copytree() and inspect the linked PR gh-156214 before making changes. Reproduce the relative-symlink cases from the issue, then add or update regression coverage in the existing shutil tests so valid links are copied and dangling links are ignored regardless of the current working directory.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.