Skip to content

[DO NOT MERGE] bugs 9949 and 37834 together #15310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 40 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
33021c5
bpo-37834: Enable os.readlink and os.path.is_link to recognize app ex…
zooba Aug 12, 2019
6b5d22d
Add reparse_tag to stat structure and add test
zooba Aug 12, 2019
658a355
Revert readlink change and only traverse actual symlinks
zooba Aug 12, 2019
75d039e
Treat junctions as links. Update tests
zooba Aug 13, 2019
3a64166
Fix NEWS item
zooba Aug 13, 2019
ec43378
Revert getfinalpathname change, add test fixes until bpo-9949 is fixed
zooba Aug 13, 2019
ebcbca2
Add link target resolution and revert test changes
zooba Aug 13, 2019
1aba497
Revert shutil test
zooba Aug 13, 2019
b65f06f
[WIP] bpo-9949: Enable symlink traversal for ntpath.realpath
zooba Aug 14, 2019
df54868
Fixes link resolution and test file creation
zooba Aug 14, 2019
f82129b
Fix x-plat handling in test_ntpath and additional error code
zooba Aug 14, 2019
8ee4046
Fix use of os.path in ntpath tests
zooba Aug 14, 2019
b93c9e8
Fix additional uses of os.path
zooba Aug 14, 2019
5f9816f
Validate that the path prefix is unnecessary before removing
zooba Aug 14, 2019
d1d0726
Don't need second call to _getfinalpathname
zooba Aug 14, 2019
c178e45
Rewrite Windows stat() implementation to let the OS traverse symlinks…
zooba Aug 14, 2019
8ed0cf6
Fix st_mode when unable to read path
zooba Aug 14, 2019
863e8c4
Fix some uninitialized values. Remove bad changes
zooba Aug 14, 2019
9bc4192
Fix test discovery mocks
zooba Aug 14, 2019
541dcf8
Remove get_target_path and fix tests
zooba Aug 15, 2019
f2d2e31
Fix special handling for non-traversable reparse points
zooba Aug 15, 2019
d93caf7
Merge branch 'bpo-37834' into bpo-9949-37834
zooba Aug 15, 2019
9dc0608
Avoid traversing a symlink cycle
zooba Aug 15, 2019
7bc2568
Merge branch 'bpo-9949' into bpo-9949-37834
zooba Aug 15, 2019
2f84d8c
Treat all unfollowable links equally.
zooba Aug 16, 2019
6139abf
Merge branch 'bpo-37834' into bpo-9949-37834
zooba Aug 16, 2019
4cb450e
Fix isdir test
zooba Aug 16, 2019
79a3f54
Merge branch 'bpo-37834' into bpo-9949-37834
zooba Aug 16, 2019
28c9bb4
Simplify and document cycle handling in os.path.realpath
zooba Aug 16, 2019
203869f
Merge branch 'bpo-9949' into bpo-9949-37834
zooba Aug 16, 2019
4ee15a8
Update docs
zooba Aug 16, 2019
ab485a5
Update whatsnew
zooba Aug 16, 2019
11e8092
Merge branch 'bpo-9949' into bpo-9949-37834
zooba Aug 16, 2019
25511ea
Merge branch 'bpo-37834' into bpo-9949-37834
zooba Aug 16, 2019
327cf57
Clarify docs
zooba Aug 16, 2019
39bec11
Update patch with implementation from Eryk Sun.
zooba Aug 19, 2019
d73d077
Swap order of tests to avoid unnecessary failed API calls
zooba Aug 19, 2019
51c338e
Merge branch 'bpo-37834' into bpo-9949-37834
zooba Aug 19, 2019
57cc198
Remove workaround for bpo20541 as NUL now exists
zooba Aug 20, 2019
0b926e1
Merge branch 'bpo-37834' into bpo-9949-37834
zooba Aug 20, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Doc/library/os.path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,18 @@ the :mod:`glob` module.)
.. function:: realpath(path)

Return the canonical path of the specified filename, eliminating any symbolic
links encountered in the path (if they are supported by the operating system).
links encountered in the path (if they are supported by the operating
system).

.. note::
When symbolic link cycles occur, the returned path will be one member of
the cycle, but no guarantee is made about which member that will be.

.. versionchanged:: 3.6
Accepts a :term:`path-like object`.

.. versionchanged:: 3.8
Symbolic links are now resolved on Windows.

.. function:: relpath(path, start=os.curdir)

Expand Down
48 changes: 47 additions & 1 deletion Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1858,6 +1858,9 @@ features:
.. versionchanged:: 3.6
Accepts a :term:`path-like object` for *src* and *dst*.

.. versionchanged:: 3.8
On Windows, now opens reparse points that represent another file
(name surrogates).

.. function:: mkdir(path, mode=0o777, *, dir_fd=None)

Expand Down Expand Up @@ -2053,6 +2056,11 @@ features:
.. versionchanged:: 3.8
Accepts a :term:`path-like object` and a bytes object on Windows.

.. versionchanged:: 3.8
Added support for directory junctions, and changed to return the
substitution path (which typically includes ``\\?\`` prefix) rather than
the optional "print name" field that was previously returned.

.. function:: remove(path, *, dir_fd=None)

Remove (delete) the file *path*. If *path* is a directory, an
Expand Down Expand Up @@ -2358,6 +2366,13 @@ features:
This method can raise :exc:`OSError`, such as :exc:`PermissionError`,
but :exc:`FileNotFoundError` is caught and not raised.

.. versionchanged:: 3.8
On Windows, now returns ``True`` for directory junctions as well as
symlinks. To determine whether the entry is actually a symlink to a
directory or a directory junction, compare
``entry.stat(follow_symlinks=False).st_reparse_tag`` against
``stat.IO_REPARSE_TAG_SYMLINK`` or ``stat.IO_REPARSE_TAG_MOUNT_POINT``.

.. method:: stat(\*, follow_symlinks=True)

Return a :class:`stat_result` object for this entry. This method
Expand Down Expand Up @@ -2403,6 +2418,16 @@ features:
This function can support :ref:`specifying a file descriptor <path_fd>` and
:ref:`not following symlinks <follow_symlinks>`.

On Windows, passing ``follow_symlinks=False`` will disable following all
types of reparse points, including directory junctions. Otherwise, if the
operating system is unable to follow a reparse point (for example, when it
is a custom reparse point type with no filesystem support), the stat result
for the original link is returned as if ``follow_symlinks=False`` had been
specified. To obtain stat results for the final path in this case, use the
:func:`os.path.realpath` function to resolve the path name as far as
possible and call :func:`lstat` on the result. This does not apply to
dangling symlinks or junction points, which will raise the usual exceptions.

.. index:: module: stat

Example::
Expand All @@ -2427,6 +2452,14 @@ features:
.. versionchanged:: 3.6
Accepts a :term:`path-like object`.

.. versionchanged:: 3.8
On Windows, all reparse points that can be resolved by the operating
system are now followed, and passing ``follow_symlinks=False``
disables following all name surrogate reparse points. If the operating
system reaches a reparse point that it is not able to follow, *stat* now
returns the information for the original path as if
``follow_symlinks=False`` had been specified instead of raising an error.


.. class:: stat_result

Expand Down Expand Up @@ -2578,7 +2611,7 @@ features:

File type.

On Windows systems, the following attribute is also available:
On Windows systems, the following attributes are also available:

.. attribute:: st_file_attributes

Expand All @@ -2587,6 +2620,12 @@ features:
:c:func:`GetFileInformationByHandle`. See the ``FILE_ATTRIBUTE_*``
constants in the :mod:`stat` module.

.. attribute:: st_reparse_tag

When :attr:`st_file_attributes` has the ``FILE_ATTRIBUTE_REPARSE_POINT``
set, this field contains the tag identifying the type of reparse point.
See the ``IO_REPARSE_TAG_*`` constants in the :mod:`stat` module.

The standard module :mod:`stat` defines functions and constants that are
useful for extracting information from a :c:type:`stat` structure. (On
Windows, some items are filled with dummy values.)
Expand Down Expand Up @@ -2614,6 +2653,13 @@ features:
.. versionadded:: 3.7
Added the :attr:`st_fstype` member to Solaris/derivatives.

.. versionadded:: 3.8
Added the :attr:`st_reparse_tag` member on Windows.

.. versionchanged:: 3.8
On Windows, the :attr:`st_mode` member now identifies directory
junctions as links instead of directories.

.. function:: statvfs(path)

Perform a :c:func:`statvfs` system call on the given path. The return value is
Expand Down
7 changes: 7 additions & 0 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,13 @@ call fails (for example because the path doesn't exist).
``False`` is also returned if the path doesn't exist; other errors (such
as permission errors) are propagated.

.. versionchanged:: 3.8
On Windows, now returns ``True`` for directory junctions as well as
symlinks. To determine whether the path is actually a symlink to a
directory or a directory junction, compare ``Path.lstat().st_reparse_tag``
against ``stat.IO_REPARSE_TAG_SYMLINK`` or
``stat.IO_REPARSE_TAG_MOUNT_POINT``.


.. method:: Path.is_socket()

Expand Down
4 changes: 4 additions & 0 deletions Doc/library/shutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@ Directory and files operations
Added a symlink attack resistant version that is used automatically
if platform supports fd-based functions.

.. versionchanged:: 3.8
On Windows, will no longer delete the contents of a directory junction
before removing the junction.

.. attribute:: rmtree.avoids_symlink_attacks

Indicates whether the current platform and implementation provides a
Expand Down
10 changes: 10 additions & 0 deletions Doc/library/stat.rst
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,13 @@ for more detail on the meaning of these constants.
FILE_ATTRIBUTE_VIRTUAL

.. versionadded:: 3.5

On Windows, the following constants are available for comparing against the
``st_reparse_tag`` member returned by :func:`os.lstat`. These are well-known
constants, but are not an exhaustive list.

.. data:: IO_REPARSE_TAG_SYMLINK
IO_REPARSE_TAG_MOUNT_POINT
IO_REPARSE_TAG_APPEXECLINK

.. versionadded:: 3.8
22 changes: 22 additions & 0 deletions Doc/whatsnew/3.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,19 @@ A new :func:`os.memfd_create` function was added to wrap the
``memfd_create()`` syscall.
(Contributed by Zackery Spytz and Christian Heimes in :issue:`26836`.)

On Windows, much of the manual logic for handling reparse points (symlinks)
has been delegated to the operating system. Specifically, :func:`os.stat`
will now traverse anything supported by the operating system, while
:func:`os.lstat` will not traverse anything. The stat result now includes
:attr:`stat_result.st_reparse_tag` for reparse points, and :func:`os.readlink`
is now able to read directory junctions.

Directory results from :func:`os.scandir` on Windows will now return true for
both :meth:`os.DirEntry.is_symlink` and :meth:`os.DirEntry.is_dir` when the
entry is a directory junction (this would already happen for symbolic links
to directories). To distinguish a directory junction from a symlink, use
``stat(follow_symlinks=False).st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT``.


os.path
-------
Expand All @@ -824,6 +837,12 @@ characters or bytes unrepresentable at the OS level.
environment variable and does not use :envvar:`HOME`, which is not normally set
for regular user accounts.

:func:`~os.path.isdir` on Windows no longer returns true for a link to a
non-existent directory.

:func:`~os.path.realpath` on Windows now resolves reparse points, including
symlinks and directory junctions.


ncurses
-------
Expand Down Expand Up @@ -909,6 +928,9 @@ format for new archives to improve portability and standards conformance,
inherited from the corresponding change to the :mod:`tarfile` module.
(Contributed by C.A.M. Gerlach in :issue:`30661`.)

:func:`shutil.rmtree` on Windows now removes directory junctions without
removing their contents first.


ssl
---
Expand Down
1 change: 1 addition & 0 deletions Include/fileutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ struct _Py_stat_struct {
time_t st_ctime;
int st_ctime_nsec;
unsigned long st_file_attributes;
unsigned long st_reparse_tag;
};
#else
# define _Py_stat_struct stat
Expand Down
100 changes: 81 additions & 19 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,8 +519,87 @@ def abspath(path):
except (OSError, ValueError):
return _abspath_fallback(path)

# realpath is a no-op on systems without islink support
realpath = abspath
try:
from nt import _getfinalpathname, readlink as _nt_readlink
except ImportError:
# realpath is a no-op on systems without _getfinalpathname support.
realpath = abspath
else:
def _readlink_deep(path, seen=None):
if seen is None:
seen = set()

while normcase(path) not in seen:
seen.add(normcase(path))
try:
path = _nt_readlink(path)
except OSError as ex:
# Stop on file (2) or directory (3) not found, or
# paths that are not reparse points (4390)
if ex.winerror in (2, 3, 4390):
break
raise
except ValueError:
# Stop on reparse points that are not symlinks
break
return path

def _getfinalpathname_nonstrict(path):
# Allow file (2) or directory (3) not found, invalid syntax (123),
# and symlinks that cannot be followed (1921)
allowed_winerror = 2, 3, 123, 1921

# Non-strict algorithm is to find as much of the target directory
# as we can and join the rest.
tail = ''
seen = set()
while path:
try:
path = _readlink_deep(path, seen)
path = _getfinalpathname(path)
return join(path, tail) if tail else path
except OSError as ex:
if ex.winerror not in allowed_winerror:
raise
path, name = split(path)
if path and not name:
return abspath(path + tail)
tail = join(name, tail) if tail else name
return abspath(tail)

def realpath(path):
path = os.fspath(path)
if isinstance(path, bytes):
prefix = b'\\\\?\\'
unc_prefix = b'\\\\?\\UNC\\'
new_unc_prefix = b'\\\\'
cwd = os.getcwdb()
else:
prefix = '\\\\?\\'
unc_prefix = '\\\\?\\UNC\\'
new_unc_prefix = '\\\\'
cwd = os.getcwd()
had_prefix = path.startswith(prefix)
path = _getfinalpathname_nonstrict(path)
# The path returned by _getfinalpathname will always start with \\?\ -
# strip off that prefix unless it was already provided on the original
# path.
if not had_prefix and path.startswith(prefix):
# For UNC paths, the prefix will actually be \\?\UNC\
# Handle that case as well.
if path.startswith(unc_prefix):
spath = new_unc_prefix + path[len(unc_prefix):]
else:
spath = path[len(prefix):]
# Ensure that the non-prefixed path resolves to the same path
try:
if _getfinalpathname(spath) == path:
path = spath
except OSError as ex:
pass
return path


# Win9x family and earlier have no Unicode filename support.
supports_unicode_filenames = (hasattr(sys, "getwindowsversion") and
sys.getwindowsversion()[3] >= 2)
Expand Down Expand Up @@ -633,23 +712,6 @@ def commonpath(paths):
raise


# determine if two files are in fact the same file
try:
# GetFinalPathNameByHandle is available starting with Windows 6.0.
# Windows XP and non-Windows OS'es will mock _getfinalpathname.
if sys.getwindowsversion()[:2] >= (6, 0):
from nt import _getfinalpathname
else:
raise ImportError
except (AttributeError, ImportError):
# On Windows XP and earlier, two files are the same if their absolute
# pathnames are the same.
# Non-Windows operating systems fake this method with an XP
# approximation.
def _getfinalpathname(f):
return normcase(abspath(f))


try:
# The genericpath.isdir implementation uses os.stat and checks the mode
# attribute to tell whether or not the path is a directory.
Expand Down
Loading