Author eryksun
Recipients eryksun, paul.moore, steve.dower, tim.golden, zach.ware
Date 2019-08-19.20:59:06
SpamBayes Score -1.0
Marked as misclassified Yes
Message-id <1566248347.4.0.643485456029.issue37834@roundup.psfhosted.org>
In-reply-to
Content
Here's the requested overview for the case where name-surrogate reparse points are handled as winlinks (Windows links), but S_IFLNK is reserved for IO_REPARSE_TAG_SYMLINK. I took the time this afternoon to write it up in C, which hopefully is clearer than my prose.  It handles all CreateFileW failures inline, but uses a recursive call to traverse a non-link. No reparse tag values are hard coded in win32_xstat_impl.

I extended it to support devices that aren't file systems, such as "con", disk volumes, and raw disks. This enhancement was requested on another issue, but it may as well get updated in this issue if win32_xstat_impl is getting overhauled. Some of these devices are already supported by fstat(). The latter can be extended similarly to support volume devices and raw disk devices.

I opted to fail the call in the unhandled-tag case if it opens a link that should be traversed. Either it's an unhandled link, which is an unacceptable condition (i.e. it should be a sink, not a link), or it's a link or sequence of links to an unhandled reparse point. Returning the link reparse-point data is not what the caller wants from a successful stat().

---

static int
win32_xstat_impl(const wchar_t *path, struct _Py_stat_struct *result,
                 BOOL traverse)
{
    HANDLE hFile;
    BY_HANDLE_FILE_INFORMATION fileInfo;
    FILE_ATTRIBUTE_TAG_INFO tagInfo = { 0 };
    DWORD fileType, error;
    BOOL isUnhandledTag = FALSE;
    int retval = 0;

    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 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;
            }
            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;
                }
            }
            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) {
                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;

        default:
            return -1;
        }
    }

    if (hFile != INVALID_HANDLE_VALUE) {

        /* Query the reparse tag, and traverse a non-link. */
        if (!traverse) {
            if (!GetFileInformationByHandleEx(hFile, FileAttributeTagInfo,
                    &tagInfo, sizeof(tagInfo))) {
                /* Allow devices that do no support FileAttributeTagInfo. */
                error = GetLastError() ;
                if (error == ERROR_INVALID_PARAMETER ||
                    error == ERROR_INVALID_FUNCTION ||
                    error == ERROR_NOT_SUPPORTED) {
                    tagInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL;
                    tagInfo.ReparseTag = 0;
                } else {
                    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);
                }
            }
        }

        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;
        }

        if (!GetFileInformationByHandle(hFile, &fileInfo)) {
            error = GetLastError();
            if (error != ERROR_INVALID_PARAMETER &&
                error != ERROR_INVALID_FUNCTION &&
                error != ERROR_NOT_SUPPORTED) {
                retval = -1;
                goto cleanup;
            }
            /* 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(&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;
            }
        }
    }

cleanup:
    if (hFile != INVALID_HANDLE_VALUE) {
        CloseHandle(hFile);
    }

    return retval;
}
History
Date User Action Args
2019-08-19 20:59:07eryksunsetrecipients: + eryksun, paul.moore, tim.golden, zach.ware, steve.dower
2019-08-19 20:59:07eryksunsetmessageid: <1566248347.4.0.643485456029.issue37834@roundup.psfhosted.org>
2019-08-19 20:59:07eryksunlinkissue37834 messages
2019-08-19 20:59:06eryksuncreate