Skip to content

bpo-31226: Distinguish win junction links against volume mount points #5998

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 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
184 changes: 75 additions & 109 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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);
Expand All @@ -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 */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment itself is very strange. Of course it can fail if you don't have access to the directory, e.g. it's in another user's profile and you're not an administrator.

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, '.');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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;
}
Expand Down
Loading