diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 4c620ccae9c84c..0d00cdd48be4c4 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -2325,8 +2325,11 @@ def test_create_junction(self): self.assertTrue(os.path.exists(self.junction)) self.assertTrue(os.path.isdir(self.junction)) - # Junctions are not recognized as links. - self.assertFalse(os.path.islink(self.junction)) + # Junctions that are not volume mount points are recognized as links. + self.assertTrue(os.path.islink(self.junction)) + self.assertFalse(stat.S_ISDIR(os.lstat(self.junction).st_mode)) + target = os.readlink(self.junction) + self.assertTrue(os.path.samefile(target, self.junction_target)) def test_unlink_removes_junction(self): _winapi.CreateJunction(self.junction_target, self.junction) diff --git a/Misc/NEWS.d/next/Windows/2018-03-06-00-53-41.bpo-31226.ujysrj.rst b/Misc/NEWS.d/next/Windows/2018-03-06-00-53-41.bpo-31226.ujysrj.rst new file mode 100644 index 00000000000000..c5986f65ed56b0 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2018-03-06-00-53-41.bpo-31226.ujysrj.rst @@ -0,0 +1,4 @@ +Junctions are sometimes used as links (e.g. mklink /j) and sometimes as +volume mount points (e.g. mountvol.exe). islink, readlink, and lstat now +treat junction links as links, while retaining their old behavior for volume +mount points. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index e8dbdcc94aa701..38db62c1458268 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -459,7 +459,8 @@ PyOS_AfterFork(void) /* defined in fileutils.c */ void _Py_time_t_to_FILE_TIME(time_t, int, FILETIME *); void _Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *, - ULONG, struct _Py_stat_struct *); + BOOL, struct _Py_stat_struct *); +int _Py_is_reparse_link(const wchar_t*, ULONG, BOOL*, BOOL); #endif #ifdef MS_WINDOWS @@ -1306,32 +1307,6 @@ _Py_Sigset_Converter(PyObject *obj, void *addr) } #endif /* HAVE_SIGSET_T */ -#ifdef MS_WINDOWS - -static int -win32_get_reparse_tag(HANDLE reparse_point_handle, ULONG *reparse_tag) -{ - char target_buffer[_Py_MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; - _Py_REPARSE_DATA_BUFFER *rdb = (_Py_REPARSE_DATA_BUFFER *)target_buffer; - DWORD n_bytes_returned; - - if (0 == DeviceIoControl( - reparse_point_handle, - FSCTL_GET_REPARSE_POINT, - NULL, 0, /* in buffer */ - target_buffer, sizeof(target_buffer), - &n_bytes_returned, - NULL)) /* we're not using OVERLAPPED_IO */ - return FALSE; - - if (reparse_tag) - *reparse_tag = rdb->ReparseTag; - - return TRUE; -} - -#endif /* MS_WINDOWS */ - /* Return a dictionary corresponding to the POSIX environment table */ #if defined(WITH_NEXT_FRAMEWORK) || (defined(__APPLE__) && defined(Py_ENABLE_SHARED)) /* On Darwin/MacOSX a shared library or framework has no access to @@ -1624,48 +1599,17 @@ attributes_from_dir(LPCWSTR pszFile, BY_HANDLE_FILE_INFORMATION *info, ULONG *re return TRUE; } -static BOOL -get_target_path(HANDLE hdl, wchar_t **target_path) -{ - int buf_size, result_length; - wchar_t *buf; - - /* We have a good handle to the target, use it to determine - the target path name (then we'll call lstat on it). */ - buf_size = GetFinalPathNameByHandleW(hdl, 0, 0, - VOLUME_NAME_DOS); - if(!buf_size) - return FALSE; - - buf = (wchar_t *)PyMem_RawMalloc((buf_size + 1) * sizeof(wchar_t)); - if (!buf) { - SetLastError(ERROR_OUTOFMEMORY); - return FALSE; - } - - result_length = GetFinalPathNameByHandleW(hdl, - buf, buf_size, VOLUME_NAME_DOS); - - if(!result_length) { - PyMem_RawFree(buf); - return FALSE; - } - - buf[result_length] = 0; - - *target_path = buf; - return TRUE; -} - static int win32_xstat_impl(const wchar_t *path, struct _Py_stat_struct *result, BOOL traverse) { - int code; - HANDLE hFile, hFile2; + HANDLE hFile; BY_HANDLE_FILE_INFORMATION info; ULONG reparse_tag = 0; - wchar_t *target_path; + FILE_ATTRIBUTE_TAG_INFO taginfo; + BOOL ret; + BOOL is_link = FALSE; + DWORD lastError; const wchar_t *dot; hFile = CreateFileW( @@ -1675,9 +1619,8 @@ win32_xstat_impl(const wchar_t *path, struct _Py_stat_struct *result, NULL, /* security attributes */ OPEN_EXISTING, /* FILE_FLAG_BACKUP_SEMANTICS is required to open a directory */ - /* FILE_FLAG_OPEN_REPARSE_POINT does not follow the symlink. - Because of this, calls like GetFinalPathNameByHandle will return - the symlink path again and not the actual final path. */ + /* FILE_FLAG_OPEN_REPARSE_POINT prevents processing of reparse + points. */ FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS| FILE_FLAG_OPEN_REPARSE_POINT, NULL); @@ -1686,65 +1629,68 @@ win32_xstat_impl(const wchar_t *path, struct _Py_stat_struct *result, /* Either the target doesn't exist, or we don't have access to get a handle to it. If the former, we need to return an error. If the latter, we can use attributes_from_dir. */ - DWORD lastError = GetLastError(); + lastError = GetLastError(); if (lastError != ERROR_ACCESS_DENIED && - lastError != ERROR_SHARING_VIOLATION) + lastError != ERROR_SHARING_VIOLATION && + lastError != ERROR_INVALID_PARAMETER) return -1; /* Could not get attributes on open file. Fall back to reading the directory. */ if (!attributes_from_dir(path, &info, &reparse_tag)) /* Very strange. This should not fail now */ return -1; + if (!_Py_is_reparse_link(path, reparse_tag, &is_link, FALSE)) { + return -1; + } if (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { - if (traverse) { + if (!is_link || traverse) { /* Should traverse, but could not open reparse point handle */ SetLastError(lastError); return -1; } } } else { - if (!GetFileInformationByHandle(hFile, &info)) { - CloseHandle(hFile); - return -1; - } - if (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { - if (!win32_get_reparse_tag(hFile, &reparse_tag)) { + // Get file attributes + reparse tag first + if (!GetFileInformationByHandleEx(hFile, FileAttributeTagInfo, + &taginfo, sizeof(taginfo))) { + lastError = GetLastError(); + if (lastError != ERROR_INVALID_FUNCTION && + lastError != ERROR_INVALID_PARAMETER) { CloseHandle(hFile); + SetLastError(lastError); return -1; } - /* Close the outer open file handle now that we're about to - reopen it with different flags. */ - if (!CloseHandle(hFile)) + } else if (taginfo.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { + if (!_Py_is_reparse_link(path, taginfo.ReparseTag, &is_link, + FALSE)) { + CloseHandle(hFile); return -1; + } - if (traverse) { - /* In order to call GetFinalPathNameByHandle we need to open - the file without the reparse handling flag set. */ - hFile2 = CreateFileW( - path, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, - NULL, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS, - NULL); - if (hFile2 == INVALID_HANDLE_VALUE) - return -1; - - if (!get_target_path(hFile2, &target_path)) { - CloseHandle(hFile2); + if (!is_link || traverse) { + /* Close the outer open file handle now that we're about to + reopen it with different flags. */ + if (!CloseHandle(hFile)) { return -1; } - - if (!CloseHandle(hFile2)) { + // FILE_FLAG_OPEN_REPARSE_POINT to follow reparses: + hFile = CreateFileW( + path, FILE_READ_ATTRIBUTES, 0, + NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS, + NULL); + if (hFile == INVALID_HANDLE_VALUE) { return -1; } - - code = win32_xstat_impl(target_path, result, FALSE); - PyMem_RawFree(target_path); - return code; + is_link = FALSE; } - } else - CloseHandle(hFile); + } + ret = GetFileInformationByHandle(hFile, &info); + if (!CloseHandle(hFile) || !ret) { + return -1; + } } - _Py_attribute_data_to_stat(&info, reparse_tag, result); + _Py_attribute_data_to_stat(&info, is_link, result); /* Set S_IEXEC if it is an .exe, .bat, ... */ dot = wcsrchr(path, '.'); @@ -7695,6 +7641,8 @@ os_readlink_impl(PyObject *module, path_t *path, int dir_fd) _Py_REPARSE_DATA_BUFFER *rdb = (_Py_REPARSE_DATA_BUFFER *)target_buffer; const wchar_t *print_name; PyObject *result; + BOOL is_link; + USHORT pname_len; /* First get a handle to the reparse point */ Py_BEGIN_ALLOW_THREADS @@ -7729,17 +7677,28 @@ os_readlink_impl(PyObject *module, path_t *path, int dir_fd) return path_error(path); } - if (rdb->ReparseTag != IO_REPARSE_TAG_SYMLINK) - { + if (!_Py_is_reparse_link(path->wide, rdb->ReparseTag, &is_link, TRUE)) { + return PyErr_SetExcFromWindowsErrWithFilenameObject( + PyExc_WindowsError, GetLastError(), path); + } + if (!is_link) { PyErr_SetString(PyExc_ValueError, "not a symbolic link"); return NULL; } - print_name = (wchar_t *)((char*)rdb->SymbolicLinkReparseBuffer.PathBuffer + - rdb->SymbolicLinkReparseBuffer.PrintNameOffset); + if (rdb->ReparseTag == IO_REPARSE_TAG_SYMLINK) { + print_name = (wchar_t *)( + (char*)rdb->SymbolicLinkReparseBuffer.PathBuffer + + rdb->SymbolicLinkReparseBuffer.PrintNameOffset); + pname_len = rdb->SymbolicLinkReparseBuffer.PrintNameLength; + } else { + print_name = (wchar_t *)( + (char*)rdb->MountPointReparseBuffer.PathBuffer + + rdb->MountPointReparseBuffer.PrintNameOffset); + pname_len = rdb->MountPointReparseBuffer.PrintNameLength; + } - result = PyUnicode_FromWideChar(print_name, - rdb->SymbolicLinkReparseBuffer.PrintNameLength / sizeof(wchar_t)); + result = PyUnicode_FromWideChar(print_name, pname_len / sizeof(wchar_t)); if (path->narrow) { Py_SETREF(result, PyUnicode_EncodeFSDefault(result)); } @@ -12518,7 +12477,8 @@ DirEntry_from_find_data(path_t *path, WIN32_FIND_DATAW *dataW) DirEntry *entry; BY_HANDLE_FILE_INFORMATION file_info; ULONG reparse_tag; - wchar_t *joined_path; + BOOL is_link; + wchar_t *joined_path=NULL; entry = PyObject_New(DirEntry, &DirEntryType); if (!entry) @@ -12543,7 +12503,6 @@ DirEntry_from_find_data(path_t *path, WIN32_FIND_DATAW *dataW) goto error; entry->path = PyUnicode_FromWideChar(joined_path, -1); - PyMem_Free(joined_path); if (!entry->path) goto error; if (path->narrow) { @@ -12553,11 +12512,18 @@ DirEntry_from_find_data(path_t *path, WIN32_FIND_DATAW *dataW) } find_data_to_file_info(dataW, &file_info, &reparse_tag); - _Py_attribute_data_to_stat(&file_info, reparse_tag, &entry->win32_lstat); + if (!_Py_is_reparse_link(joined_path, reparse_tag, &is_link, TRUE)) { + PyErr_SetExcFromWindowsErrWithFilenameObject( + PyExc_WindowsError, GetLastError(), entry->path); + goto error; + } + _Py_attribute_data_to_stat(&file_info, is_link, &entry->win32_lstat); + PyMem_Free(joined_path); return (PyObject *)entry; error: + PyMem_Free(joined_path); Py_DECREF(entry); return NULL; } diff --git a/Python/fileutils.c b/Python/fileutils.c index b933874193b483..79e2f027914136 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -865,7 +865,7 @@ attributes_to_mode(DWORD attr) } void -_Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *info, ULONG reparse_tag, +_Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *info, BOOL is_link, struct _Py_stat_struct *result) { memset(result, 0, sizeof(*result)); @@ -878,7 +878,7 @@ _Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *info, ULONG reparse_tag, FILE_TIME_to_time_t_nsec(&info->ftLastAccessTime, &result->st_atime, &result->st_atime_nsec); result->st_nlink = info->nNumberOfLinks; result->st_ino = (((uint64_t)info->nFileIndexHigh) << 32) + info->nFileIndexLow; - if (reparse_tag == IO_REPARSE_TAG_SYMLINK) { + if (is_link) { /* first clear the S_IFMT bits */ result->st_mode ^= (result->st_mode & S_IFMT); /* now set the bits that make this a symlink */ @@ -1953,3 +1953,73 @@ _Py_GetLocaleconvNumeric(struct lconv *lc, PyMem_Free(oldloc); return res; } + + +#ifdef MS_WINDOWS + +/* Determine whether a reparse point is a link type. */ +int +_Py_is_reparse_link(IN const wchar_t *path, IN ULONG reparse_tag, + OUT BOOL* is_link, IN BOOL gil_held) +{ + BOOL ret; + DWORD buflen; + BOOL success = TRUE; + wchar_t *normpath = NULL; + wchar_t *mountpath = NULL; + + *is_link = FALSE; + + if (reparse_tag == IO_REPARSE_TAG_SYMLINK) { + *is_link = TRUE; + } else if (reparse_tag == IO_REPARSE_TAG_MOUNT_POINT) { + /* Junction point. Either a link (e.g. mklink /j) or a volume mount + points (e.g. mountvol.exe). Compare GetVolumePathNameW to path + to determine which case. If they are equal, it is a mount. + */ + success = FALSE; + + buflen = GetFullPathNameW(path, 0, NULL, NULL); + if (!buflen) { + goto cleanup; + } + normpath = PyMem_RawMalloc(buflen * sizeof(wchar_t)); + /* Volume path should be shorter than entire path */ + mountpath = PyMem_RawMalloc(buflen * sizeof(wchar_t)); + if (mountpath == NULL || normpath == NULL) { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + goto cleanup; + } + + // Normalize separators (slashes) + if (!GetFullPathNameW(path, buflen, normpath, NULL)) { + goto cleanup; + } + + if (gil_held) { + Py_BEGIN_ALLOW_THREADS + ret = GetVolumePathNameW(normpath, mountpath, + Py_SAFE_DOWNCAST(buflen, size_t, DWORD)); + Py_END_ALLOW_THREADS + } else { + ret = GetVolumePathNameW(normpath, mountpath, + Py_SAFE_DOWNCAST(buflen, size_t, DWORD)); + } + + if (!ret) { + goto cleanup; + } + + *is_link = wcsncmp(normpath, mountpath, buflen); + success = TRUE; + } + +cleanup: + + PyMem_RawFree(normpath); + PyMem_RawFree(mountpath); + + return success; +} + +#endif