Skip to content

Commit

Permalink
bpo-37834: Clarify reparse point handling on Windows.
Browse files Browse the repository at this point in the history
* ntpath.realpath() and nt.stat() will traverse all supported reparse points (previously was mixed)
* nt.lstat() will let the OS traverse reparse points that are not name surrogates (previously would not traverse any reparse point)
* nt.[l]stat() will only set S_IFLNK for symlinks (previous behaviour)
* nt.readlink() will read destinations for symlinks and junction points only

bpo-1311: os.path.exists('nul') now returns True on Windows
* nt.stat('nul').st_mode is now S_IFCHR (previously was an error)
  • Loading branch information
zooba committed Aug 21, 2019
1 parent 75e0649 commit 514d5d7
Show file tree
Hide file tree
Showing 17 changed files with 471 additions and 232 deletions.
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
19 changes: 19 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,9 @@ 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.

Expand Down Expand Up @@ -912,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
48 changes: 41 additions & 7 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,14 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
dstname = os.path.join(dst, srcentry.name)
srcobj = srcentry if use_srcentry else srcname
try:
if srcentry.is_symlink():
is_symlink = srcentry.is_symlink()
if is_symlink and os.name == 'nt':
# Special check for directory junctions, which appear as
# symlinks but we want to recurse.
lstat = srcentry.stat(follow_symlinks=False)
if lstat.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT:
is_symlink = False
if is_symlink:
linkto = os.readlink(srcname)
if symlinks:
# We can't just leave it to `copy_function` because legacy
Expand Down Expand Up @@ -537,6 +544,37 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
ignore_dangling_symlinks=ignore_dangling_symlinks,
dirs_exist_ok=dirs_exist_ok)

if hasattr(stat, 'FILE_ATTRIBUTE_REPARSE_POINT'):
# Special handling for directory junctions to make them behave like
# symlinks for shutil.rmtree, since in general they do not appear as
# regular links.
def _rmtree_isdir(entry):
try:
st = entry.stat(follow_symlinks=False)
return (stat.S_ISDIR(st.st_mode) and not
(st.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT
and st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT))
except OSError:
return False

def _rmtree_islink(path):
try:
st = os.lstat(path)
return (stat.S_ISLNK(st.st_mode) or
(st.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT
and st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT))
except OSError:
return False
else:
def _rmtree_isdir(entry):
try:
return entry.is_dir(follow_symlinks=False)
except OSError:
return False

def _rmtree_islink(path):
return os.path.islink(path)

# version vulnerable to race conditions
def _rmtree_unsafe(path, onerror):
try:
Expand All @@ -547,11 +585,7 @@ def _rmtree_unsafe(path, onerror):
entries = []
for entry in entries:
fullname = entry.path
try:
is_dir = entry.is_dir(follow_symlinks=False)
except OSError:
is_dir = False
if is_dir:
if _rmtree_isdir(entry):
try:
if entry.is_symlink():
# This can only happen if someone replaces
Expand Down Expand Up @@ -681,7 +715,7 @@ def onerror(*args):
os.close(fd)
else:
try:
if os.path.islink(path):
if _rmtree_islink(path):
# symlinks to directories are forbidden, see bug #1669
raise OSError("Cannot call rmtree on a symbolic link")
except OSError:
Expand Down
Loading

0 comments on commit 514d5d7

Please sign in to comment.