summaryrefslogtreecommitdiffstats
path: root/Modules/posixmodule.c
diff options
context:
space:
mode:
authorSteve Dower <steve.dower@python.org>2019-08-21 22:27:33 (GMT)
committerGitHub <noreply@github.com>2019-08-21 22:27:33 (GMT)
commitdf2d4a6f3d5da2839c4fc11d31511c8e028daf2c (patch)
treee60154de9e835976aed87ab51ac3d5d9fda7f45f /Modules/posixmodule.c
parentbcc446f525421156fe693139140e7051d000592e (diff)
downloadcpython-df2d4a6f3d5da2839c4fc11d31511c8e028daf2c.zip
cpython-df2d4a6f3d5da2839c4fc11d31511c8e028daf2c.tar.gz
cpython-df2d4a6f3d5da2839c4fc11d31511c8e028daf2c.tar.bz2
bpo-37834: Normalise handling of reparse points on Windows (GH-15231)
bpo-37834: Normalise handling of reparse points on Windows * 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)
Diffstat (limited to 'Modules/posixmodule.c')
-rw-r--r--Modules/posixmodule.c387
1 files changed, 206 insertions, 181 deletions
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index 4f8c074..2302678 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -1625,6 +1625,7 @@ win32_wchdir(LPCWSTR path)
*/
#define HAVE_STAT_NSEC 1
#define HAVE_STRUCT_STAT_ST_FILE_ATTRIBUTES 1
+#define HAVE_STRUCT_STAT_ST_REPARSE_TAG 1
static void
find_data_to_file_info(WIN32_FIND_DATAW *pFileData,
@@ -1658,136 +1659,178 @@ 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;
- BY_HANDLE_FILE_INFORMATION info;
- ULONG reparse_tag = 0;
- wchar_t *target_path;
- const wchar_t *dot;
+ HANDLE hFile;
+ BY_HANDLE_FILE_INFORMATION fileInfo;
+ FILE_ATTRIBUTE_TAG_INFO tagInfo = { 0 };
+ DWORD fileType, error;
+ BOOL isUnhandledTag = FALSE;
+ int retval = 0;
- hFile = CreateFileW(
- path,
- FILE_READ_ATTRIBUTES, /* desired access */
- 0, /* share mode */
- 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_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS|
- FILE_FLAG_OPEN_REPARSE_POINT,
- NULL);
+ DWORD access = FILE_READ_ATTRIBUTES;
+ DWORD flags = FILE_FLAG_BACKUP_SEMANTICS; /* Allow opening directories. */
+ if (!traverse) {
+ flags |= FILE_FLAG_OPEN_REPARSE_POINT;
+ }
+ hFile = CreateFileW(path, access, 0, NULL, OPEN_EXISTING, flags, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
- /* 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();
- if (lastError != ERROR_ACCESS_DENIED &&
- lastError != ERROR_SHARING_VIOLATION)
- 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 (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
- if (traverse) {
- /* Should traverse, but could not open reparse point handle */
- SetLastError(lastError);
+ /* Either the path doesn't exist, or the caller lacks access. */
+ error = GetLastError();
+ switch (error) {
+ case ERROR_ACCESS_DENIED: /* Cannot sync or read attributes. */
+ case ERROR_SHARING_VIOLATION: /* It's a paging file. */
+ /* Try reading the parent directory. */
+ if (!attributes_from_dir(path, &fileInfo, &tagInfo.ReparseTag)) {
+ /* Cannot read the parent directory. */
+ SetLastError(error);
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)) {
- CloseHandle(hFile);
- return -1;
+ if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
+ if (traverse ||
+ !IsReparseTagNameSurrogate(tagInfo.ReparseTag)) {
+ /* The stat call has to traverse but cannot, so fail. */
+ SetLastError(error);
+ return -1;
+ }
}
- /* Close the outer open file handle now that we're about to
- reopen it with different flags. */
- if (!CloseHandle(hFile))
+ break;
+
+ case ERROR_INVALID_PARAMETER:
+ /* \\.\con requires read or write access. */
+ hFile = CreateFileW(path, access | GENERIC_READ,
+ FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
+ OPEN_EXISTING, flags, NULL);
+ if (hFile == INVALID_HANDLE_VALUE) {
+ SetLastError(error);
return -1;
+ }
+ break;
+ case ERROR_CANT_ACCESS_FILE:
+ /* bpo37834: open unhandled reparse points if traverse fails. */
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;
+ traverse = FALSE;
+ isUnhandledTag = TRUE;
+ hFile = CreateFileW(path, access, 0, NULL, OPEN_EXISTING,
+ flags | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
+ }
+ if (hFile == INVALID_HANDLE_VALUE) {
+ SetLastError(error);
+ return -1;
+ }
+ break;
- if (!get_target_path(hFile2, &target_path)) {
- CloseHandle(hFile2);
- return -1;
- }
+ default:
+ return -1;
+ }
+ }
- if (!CloseHandle(hFile2)) {
- return -1;
+ if (hFile != INVALID_HANDLE_VALUE) {
+ /* Handle types other than files on disk. */
+ fileType = GetFileType(hFile);
+ if (fileType != FILE_TYPE_DISK) {
+ if (fileType == FILE_TYPE_UNKNOWN && GetLastError() != 0) {
+ retval = -1;
+ goto cleanup;
+ }
+ DWORD fileAttributes = GetFileAttributesW(path);
+ memset(result, 0, sizeof(*result));
+ if (fileAttributes != INVALID_FILE_ATTRIBUTES &&
+ fileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
+ /* \\.\pipe\ or \\.\mailslot\ */
+ result->st_mode = _S_IFDIR;
+ } else if (fileType == FILE_TYPE_CHAR) {
+ /* \\.\nul */
+ result->st_mode = _S_IFCHR;
+ } else if (fileType == FILE_TYPE_PIPE) {
+ /* \\.\pipe\spam */
+ result->st_mode = _S_IFIFO;
+ }
+ /* FILE_TYPE_UNKNOWN, e.g. \\.\mailslot\waitfor.exe\spam */
+ goto cleanup;
+ }
+
+ /* Query the reparse tag, and traverse a non-link. */
+ if (!traverse) {
+ if (!GetFileInformationByHandleEx(hFile, FileAttributeTagInfo,
+ &tagInfo, sizeof(tagInfo))) {
+ /* Allow devices that do not support FileAttributeTagInfo. */
+ switch (GetLastError()) {
+ case ERROR_INVALID_PARAMETER:
+ case ERROR_INVALID_FUNCTION:
+ case ERROR_NOT_SUPPORTED:
+ tagInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL;
+ tagInfo.ReparseTag = 0;
+ break;
+ default:
+ retval = -1;
+ goto cleanup;
}
+ } else if (tagInfo.FileAttributes &
+ FILE_ATTRIBUTE_REPARSE_POINT) {
+ if (IsReparseTagNameSurrogate(tagInfo.ReparseTag)) {
+ if (isUnhandledTag) {
+ /* Traversing previously failed for either this link
+ or its target. */
+ SetLastError(ERROR_CANT_ACCESS_FILE);
+ retval = -1;
+ goto cleanup;
+ }
+ /* Traverse a non-link, but not if traversing already failed
+ for an unhandled tag. */
+ } else if (!isUnhandledTag) {
+ CloseHandle(hFile);
+ return win32_xstat_impl(path, result, TRUE);
+ }
+ }
+ }
- code = win32_xstat_impl(target_path, result, FALSE);
- PyMem_RawFree(target_path);
- return code;
+ if (!GetFileInformationByHandle(hFile, &fileInfo)) {
+ switch (GetLastError()) {
+ case ERROR_INVALID_PARAMETER:
+ case ERROR_INVALID_FUNCTION:
+ case ERROR_NOT_SUPPORTED:
+ retval = -1;
+ goto cleanup;
}
- } else
- CloseHandle(hFile);
+ /* Volumes and physical disks are block devices, e.g.
+ \\.\C: and \\.\PhysicalDrive0. */
+ memset(result, 0, sizeof(*result));
+ result->st_mode = 0x6000; /* S_IFBLK */
+ goto cleanup;
+ }
}
- _Py_attribute_data_to_stat(&info, reparse_tag, result);
- /* Set S_IEXEC if it is an .exe, .bat, ... */
- dot = wcsrchr(path, '.');
- if (dot) {
- if (_wcsicmp(dot, L".bat") == 0 || _wcsicmp(dot, L".cmd") == 0 ||
- _wcsicmp(dot, L".exe") == 0 || _wcsicmp(dot, L".com") == 0)
- result->st_mode |= 0111;
+ _Py_attribute_data_to_stat(&fileInfo, tagInfo.ReparseTag, result);
+
+ if (!(fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
+ /* Fix the file execute permissions. This hack sets S_IEXEC if
+ the filename has an extension that is commonly used by files
+ that CreateProcessW can execute. A real implementation calls
+ GetSecurityInfo, OpenThreadToken/OpenProcessToken, and
+ AccessCheck to check for generic read, write, and execute
+ access. */
+ const wchar_t *fileExtension = wcsrchr(path, '.');
+ if (fileExtension) {
+ if (_wcsicmp(fileExtension, L".exe") == 0 ||
+ _wcsicmp(fileExtension, L".bat") == 0 ||
+ _wcsicmp(fileExtension, L".cmd") == 0 ||
+ _wcsicmp(fileExtension, L".com") == 0) {
+ result->st_mode |= 0111;
+ }
+ }
}
- return 0;
+
+cleanup:
+ if (hFile != INVALID_HANDLE_VALUE) {
+ CloseHandle(hFile);
+ }
+
+ return retval;
}
static int
@@ -1806,9 +1849,8 @@ win32_xstat(const wchar_t *path, struct _Py_stat_struct *result, BOOL traverse)
default does not traverse symlinks and instead returns attributes for
the symlink.
- Therefore, win32_lstat will get the attributes traditionally, and
- win32_stat will first explicitly resolve the symlink target and then will
- call win32_lstat on that result. */
+ Instead, we will open the file (which *does* traverse symlinks by default)
+ and GetFileInformationByHandle(). */
static int
win32_lstat(const wchar_t* path, struct _Py_stat_struct *result)
@@ -1877,6 +1919,9 @@ static PyStructSequence_Field stat_result_fields[] = {
#ifdef HAVE_STRUCT_STAT_ST_FSTYPE
{"st_fstype", "Type of filesystem"},
#endif
+#ifdef HAVE_STRUCT_STAT_ST_REPARSE_TAG
+ {"st_reparse_tag", "Windows reparse tag"},
+#endif
{0}
};
@@ -1928,6 +1973,12 @@ static PyStructSequence_Field stat_result_fields[] = {
#define ST_FSTYPE_IDX ST_FILE_ATTRIBUTES_IDX
#endif
+#ifdef HAVE_STRUCT_STAT_ST_REPARSE_TAG
+#define ST_REPARSE_TAG_IDX (ST_FSTYPE_IDX+1)
+#else
+#define ST_REPARSE_TAG_IDX ST_FSTYPE_IDX
+#endif
+
static PyStructSequence_Desc stat_result_desc = {
"stat_result", /* name */
stat_result__doc__, /* doc */
@@ -2155,6 +2206,10 @@ _pystat_fromstructstat(STRUCT_STAT *st)
PyStructSequence_SET_ITEM(v, ST_FSTYPE_IDX,
PyUnicode_FromString(st->st_fstype));
#endif
+#ifdef HAVE_STRUCT_STAT_ST_REPARSE_TAG
+ PyStructSequence_SET_ITEM(v, ST_REPARSE_TAG_IDX,
+ PyLong_FromUnsignedLong(st->st_reparse_tag));
+#endif
if (PyErr_Occurred()) {
Py_DECREF(v);
@@ -3877,8 +3932,9 @@ os__getfinalpathname_impl(PyObject *module, path_t *path)
}
result = PyUnicode_FromWideChar(target_path, result_length);
- if (path->narrow)
+ if (result && path->narrow) {
Py_SETREF(result, PyUnicode_EncodeFSDefault(result));
+ }
cleanup:
if (target_path != buf) {
@@ -3888,44 +3944,6 @@ cleanup:
return result;
}
-/*[clinic input]
-os._isdir
-
- path as arg: object
- /
-
-Return true if the pathname refers to an existing directory.
-[clinic start generated code]*/
-
-static PyObject *
-os__isdir(PyObject *module, PyObject *arg)
-/*[clinic end generated code: output=404f334d85d4bf25 input=36cb6785874d479e]*/
-{
- DWORD attributes;
- path_t path = PATH_T_INITIALIZE("_isdir", "path", 0, 0);
-
- if (!path_converter(arg, &path)) {
- if (PyErr_ExceptionMatches(PyExc_ValueError)) {
- PyErr_Clear();
- Py_RETURN_FALSE;
- }
- return NULL;
- }
-
- Py_BEGIN_ALLOW_THREADS
- attributes = GetFileAttributesW(path.wide);
- Py_END_ALLOW_THREADS
-
- path_cleanup(&path);
- if (attributes == INVALID_FILE_ATTRIBUTES)
- Py_RETURN_FALSE;
-
- if (attributes & FILE_ATTRIBUTE_DIRECTORY)
- Py_RETURN_TRUE;
- else
- Py_RETURN_FALSE;
-}
-
/*[clinic input]
os._getvolumepathname
@@ -7796,11 +7814,10 @@ os_readlink_impl(PyObject *module, path_t *path, int dir_fd)
return PyBytes_FromStringAndSize(buffer, length);
#elif defined(MS_WINDOWS)
DWORD n_bytes_returned;
- DWORD io_result;
+ DWORD io_result = 0;
HANDLE reparse_point_handle;
char target_buffer[_Py_MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
_Py_REPARSE_DATA_BUFFER *rdb = (_Py_REPARSE_DATA_BUFFER *)target_buffer;
- const wchar_t *print_name;
PyObject *result;
/* First get a handle to the reparse point */
@@ -7813,42 +7830,51 @@ os_readlink_impl(PyObject *module, path_t *path, int dir_fd)
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_BACKUP_SEMANTICS,
0);
- Py_END_ALLOW_THREADS
-
- if (reparse_point_handle == INVALID_HANDLE_VALUE) {
- return path_error(path);
+ if (reparse_point_handle != INVALID_HANDLE_VALUE) {
+ /* New call DeviceIoControl to read the reparse point */
+ io_result = DeviceIoControl(
+ reparse_point_handle,
+ FSCTL_GET_REPARSE_POINT,
+ 0, 0, /* in buffer */
+ target_buffer, sizeof(target_buffer),
+ &n_bytes_returned,
+ 0 /* we're not using OVERLAPPED_IO */
+ );
+ CloseHandle(reparse_point_handle);
}
-
- Py_BEGIN_ALLOW_THREADS
- /* New call DeviceIoControl to read the reparse point */
- io_result = DeviceIoControl(
- reparse_point_handle,
- FSCTL_GET_REPARSE_POINT,
- 0, 0, /* in buffer */
- target_buffer, sizeof(target_buffer),
- &n_bytes_returned,
- 0 /* we're not using OVERLAPPED_IO */
- );
- CloseHandle(reparse_point_handle);
Py_END_ALLOW_THREADS
if (io_result == 0) {
return path_error(path);
}
- if (rdb->ReparseTag != IO_REPARSE_TAG_SYMLINK)
+ wchar_t *name = NULL;
+ Py_ssize_t nameLen = 0;
+ if (rdb->ReparseTag == IO_REPARSE_TAG_SYMLINK)
{
- PyErr_SetString(PyExc_ValueError,
- "not a symbolic link");
- return NULL;
+ name = (wchar_t *)((char*)rdb->SymbolicLinkReparseBuffer.PathBuffer +
+ rdb->SymbolicLinkReparseBuffer.SubstituteNameOffset);
+ nameLen = rdb->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(wchar_t);
}
- print_name = (wchar_t *)((char*)rdb->SymbolicLinkReparseBuffer.PathBuffer +
- rdb->SymbolicLinkReparseBuffer.PrintNameOffset);
-
- result = PyUnicode_FromWideChar(print_name,
- rdb->SymbolicLinkReparseBuffer.PrintNameLength / sizeof(wchar_t));
- if (path->narrow) {
- Py_SETREF(result, PyUnicode_EncodeFSDefault(result));
+ else if (rdb->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
+ {
+ name = (wchar_t *)((char*)rdb->MountPointReparseBuffer.PathBuffer +
+ rdb->MountPointReparseBuffer.SubstituteNameOffset);
+ nameLen = rdb->MountPointReparseBuffer.SubstituteNameLength / sizeof(wchar_t);
+ }
+ else
+ {
+ PyErr_SetString(PyExc_ValueError, "not a symbolic link");
+ }
+ if (name) {
+ if (nameLen > 4 && wcsncmp(name, L"\\??\\", 4) == 0) {
+ /* Our buffer is mutable, so this is okay */
+ name[1] = L'\\';
+ }
+ result = PyUnicode_FromWideChar(name, nameLen);
+ if (path->narrow) {
+ Py_SETREF(result, PyUnicode_EncodeFSDefault(result));
+ }
}
return result;
#endif
@@ -13647,7 +13673,6 @@ static PyMethodDef posix_methods[] = {
OS_PATHCONF_METHODDEF
OS_ABORT_METHODDEF
OS__GETFULLPATHNAME_METHODDEF
- OS__ISDIR_METHODDEF
OS__GETDISKUSAGE_METHODDEF
OS__GETFINALPATHNAME_METHODDEF
OS__GETVOLUMEPATHNAME_METHODDEF