Bug report
Bug description:
I recently read this article "bugs that Rust won't catch" and thought I'd check if CPython has similar issues in shutil, I found this.
shutil.move, in the cross-device move case (if os.rename raises OSError), calls _destinsrc(src, dst) to prevent moving a directory into itself, which would cause copytree to recurse infinitely. The guard compares path strings after os.path.abspath, which normalises e.g. . or .., but does not resolve symlinks. A symlink component anywhere in dst could make the string comparison miss, silently bypassing the guard.
(Note that _destinsrc is only called in the cross-device/cross-filesystem fallback path, when os.rename raises OSError and copytree is used instead.)
To fix this, _destinsrc could be updated to use realpath instead of abspath:
def _destinsrc(src, dst): src = os.path.realpath(src) dst = os.path.realpath(dst) # ...
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Linked PRs
- gh-149835: Resolve symlinks in shutil._destinsrc #149851
- gh-149835:
_destinsrcto use realpath instead of abspath #149986 - [3.15] gh-149835: Use realpath() instead of abspath() in shutil.move() (GH-149986) #151009
- [3.14] gh-149835: Use realpath() instead of abspath() in shutil.move() (GH-149986) #151010
- [3.13] gh-149835: Use realpath() instead of abspath() in shutil.move() (GH-149986) #151011