GetRawInputDeviceInfo 表示 RIDI_DEVICENAME 的缓冲区大小为 1 个字符

GetRawInputDeviceInfo indicates a buffer size of 1 character for RIDI_DEVICENAME

我从 RIDI_DEVICENAME 那里得到了荒谬的行为。根据文档,

Return value

Type: UINT

If successful, this function returns a non-negative number indicating the number of bytes copied to pData.

If pData is not large enough for the data, the function returns -1. If pData is NULL, the function returns a value of zero. In both of these cases, pcbSize is set to the minimum size required for the pData buffer.

Call GetLastError to identify any other errors.

忽略 -1UINT return 类型中不是可表示值的明显问题,似乎该函数应该告诉我所需的缓冲区大小,并且如果我提供这种大小的缓冲区,该函数应该成功或至少遵循它自己的失败规则。

但是,我根本没有看到这个。在 Windows 10 上,当 pData 为 null 时,该函数的 Unicode 版本将 pcbSize 设置为 1,否则保持不变,在所有情况下均失败。当 pData 为 null 时,该函数的 ANSI 版本将 pcbSize 设置为 2,否则将传入的任何值加倍,但仍然失败。

Headers 用于任一版本的测试代码:

#define WIN32_EXTRA_LEAN 1

#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

#include <windows.h>

ANSI 测试代码:

std::string GetRawInputDeviceName( HANDLE hRaw )
{
    UINT numChars = 0u;
    INT validChars;

    validChars = static_cast<INT>(::GetRawInputDeviceInfoA(hRaw, RIDI_DEVICENAME, nullptr, &numChars));
    auto lasterror = ::GetLastError();
    if (lasterror != ERROR_INSUFFICIENT_BUFFER) {
        std::wcerr << L"Failed to get length of name of raw input device, retcode = " << validChars << L", last error = " << lasterror << L"\n";
        return {};
    }

    std::string name;
    name.resize(numChars);
    validChars = static_cast<INT>(::GetRawInputDeviceInfoA(hRaw, RIDI_DEVICENAME, &name[0], &numChars));
    lasterror = ::GetLastError();

    if (validChars > 0) {
        name.resize(validChars);
        return name;
    }
    else {
        std::wcerr << L"Failed to get name of raw input device, retcode = " << validChars << L", last error = " << lasterror << L"\n";
        return {};
    }
}

Unicode 测试代码:

std::wstring GetRawInputDeviceName( HANDLE hRaw )
{
    UINT numChars = 0u;
    INT validChars;

    validChars = static_cast<INT>(::GetRawInputDeviceInfoW(hRaw, RIDI_DEVICENAME, nullptr, &numChars));
    auto lasterror = ::GetLastError();
    if (lasterror != ERROR_INSUFFICIENT_BUFFER) {
        std::wcerr << L"Failed to get length of name of raw input device, retcode = " << validChars << L", last error = " << lasterror << L"\n";
        return {};
    }

    std::wstring name;
    name.resize(numChars);
    validChars = static_cast<INT>(::GetRawInputDeviceInfoW(hRaw, RIDI_DEVICENAME, &name[0], &numChars));
    lasterror = ::GetLastError();

    if (validChars > 0) {
        name.resize(validChars);
        return name;
    }
    else {
        std::wcerr << L"Failed to get name of raw input device, retcode = " << validChars << L", last error = " << lasterror << L"\n";
        return {};
    }
}

在 Windows 10 上,通过 RDP 我一直得到 ERROR_INSUFFICIENT_BUFFER

在 Windows 8.1 运行 作为本地用户,如果 pData 为空,我得到 ERROR_INSUFFICIENT_BUFFER,当我提供缓冲区时,我返回失败((UINT)-1) 和 GetLastError() return 为零。

我也刚刚尝试提出 likely-large-enough 缓冲区大小,但也失败了。

这是怎么回事,获取接口路径名的正确方法是什么,我需要管理权限还是先调用一些其他API?我调用 GetRawInputDeviceList 或使用 GetRawInputDeviceInfoRIDI_DEVICEINFO 模式似乎没有任何问题...但我需要接口路径才能走得更远。

GetRawInputDeviceName 在声明/实现/文档中有几个错误

实际上更正确地将 return 值声明为有符号(LONGINT)而不是 UINT

存在 3 个案例:

1. 函数 return 负值(或者如果需要 -1):这是错误 案例和设计 - 必须设置最后一个错误。但真的不是 始终设置(执行错误)。

最常见的错误:

  • pcbSizepData 指向无效或只读内存位置。这种情况下的常见错误 ERROR_NOACCESS(翻译自 STATUS_ACCESS_VIOLATION)

  • hDevice 无效句柄 - ERROR_INVALID_HANDLE 是 returned

  • uiCommand 无效 RIDI_XXX 常量 - ERROR_INVALID_PARAMETER

  • *pcbSize 对于数据来说不够大 - 在这种情况下 *pcbSize 设置为最小值pData 缓冲区所需的大小。 ERROR_INSUFFICIENT_BUFFER

    再次 - 只有在这种情况下 (-1) 存在意义调用 GetLastError();

2. function return 0 只有当 pDataNULL。 *pcbSize 设置为 pData 缓冲区所需的最小大小。

3.函数return正值(>0)这意味着这个计数 字节(如果 RIDI_PREPARSEDDATARIDI_DEVICEINFO )或 字符(以防RIDI_DEVICENAME)写入缓冲区

所以这里的文档是错误的:

pcbSize [in, out]

Pointer to a variable that contains the size, in bytes, of the data in pData.

如果 RIDI_DEVICENAME 个字符中

所以已经可以看出非常严重的设计问题(return 值的类型 - 无符号)和混合 bytes/characters。许多不同的案例。

但随后在实施中存在严重错误。在函数句柄的开头 hDevice 转换为指针。

PDEVICEINFO pDeviceInfo = HMValidateHandle(hDevice, TYPE_DEVICEINFO);

(如果 0 returned - 我们在 ERROR_INVALID_HANDLE 退出时得到 -1)。

DEVICEINFO存在UNICODE_STRING ustrName - 这个名字并复制到用户模式

switch (uiCommand) {
case RIDI_DEVICENAME:
    /*
     * N.b. UNICODE_STRING counts the length by the BYTE count, not by the character count.
     * Our APIs always treat the strings by the character count. Thus, for RIDI_DEVICNAME
     * only, cbOutSize holds the character count, not the byte count, in spite of its
     * name. Confusing, but cch is the way to be consistent.
     */
    cbOutSize = pDeviceInfo->ustrName.Length / sizeof(WCHAR) + 1;   // for Null terminator
    break;

   //...
}

需要 cbOutSizecbBufferSize = *pcbSize;

相比

if (cbBufferSize >= cbOutSize)api开始复制操作

存在下一个代码

            case RIDI_DEVICENAME:
                if (cbOutSize <= 2) { // !!!! error !!!!
                    retval = -1;
                    goto leave;
                }
                RtlCopyMemory(pData, pDeviceInfo->ustrName.Buffer, pDeviceInfo->ustrName.Length);
                ((WCHAR*)pData)[1] = '\'; // convert nt prefix ( \??\ ) to win32 ( \?\ )
                ((WCHAR*)pData)[cbOutSize - 1] = 0; // make it null terminated
                break;

cbOutSize 这里 - 是设备名称的 (len + 1)(我们无法控制)。因此,如果名称的长度为零 - -1 总是 returned(error #1)但最后一个错误未设置(error #2) 当然存在并且 error #3 - 为什么设备名称的长度是 0?这一定不是。但如果终端服务设备(在 UMB 总线上创建的虚拟鼠标/键盘设备)存在此结果。

api 的完整代码(在内核中)

UINT NtUserGetRawInputDeviceInfo(
    HANDLE hDevice,
    UINT uiCommand,
    LPVOID pData,
    PUINT pcbSize)
{
    UINT cbOutSize = 0;
    UINT cbBufferSize;
    int retval = 0;

    EnterCrit(0, UserMode);
    UserAtomicCheck uac;

    try {
        ProbeForRead(pcbSize, sizeof(UINT), sizeof(DWORD));
        cbBufferSize = *pcbSize;
    } except (EXCEPTION_EXECUTE_HANDLER) {
       UserSetLastError(RtlNtStatusToDosError(GetExceptionCode()));// ERROR_NOACCESS    
       retval = -1;
       goto leave1;
    }

    EnterDeviceInfoListCrit_();

    PDEVICEINFO pDeviceInfo = HMValidateHandle(hDevice, TYPE_DEVICEINFO);

    if (pDeviceInfo == NULL) {
        UserSetLastError(ERROR_INVALID_HANDLE); 
        retval = -1;
        goto leave;
    }

    /*
     * Compute the size of the output and evaluate the uiCommand.
     */
    switch (uiCommand) {
    case RIDI_PREPARSEDDATA:
        if (pDeviceInfo->type == DEVICE_TYPE_HID) {
            cbOutSize = pDeviceInfo->hid.pHidDesc->hidCollectionInfo.DescriptorSize;
        } else {
            cbOutSize = 0;
        }
        break;
    case RIDI_DEVICENAME:
        /*
         * N.b. UNICODE_STRING counts the length by the BYTE count, not by the character count.
         * Our APIs always treat the strings by the character count. Thus, for RIDI_DEVICNAME
         * only, cbOutSize holds the character count, not the byte count, in spite of its
         * name. Confusing, but cch is the way to be consistent.
         */
        cbOutSize = pDeviceInfo->ustrName.Length / sizeof(WCHAR) + 1;   // for Null terminator
        break;

    case RIDI_DEVICEINFO:
        cbOutSize = sizeof(RID_DEVICE_INFO);
        break;

    default:
        UserSetLastError(ERROR_INVALID_PARAMETER);  
        retval = -1;
        goto leave;
    }

    if (pData == NULL) {
        /*
         * The app wants to get the required size.
         */
        try {
            ProbeForWrite(pcbSize, sizeof(UINT), sizeof(DWORD));
            *pcbSize = cbOutSize;
        } except (EXCEPTION_EXECUTE_HANDLER) {
            UserSetLastError(RtlNtStatusToDosError(GetExceptionCode()));// ERROR_NOACCESS   
            retval = -1;
            goto leave;
        }
        retval = 0;
    } else {
        if (cbBufferSize >= cbOutSize) {
            try {
                ProbeForWrite(pData, cbBufferSize, sizeof(DWORD));
                switch (uiCommand) {
                case RIDI_PREPARSEDDATA:
                    if (pDeviceInfo->type == DEVICE_TYPE_HID) {
                        RtlCopyMemory(pData, pDeviceInfo->hid.pHidDesc->pPreparsedData, cbOutSize);
                    }
                    break;

                case RIDI_DEVICENAME:
                    if (cbOutSize <= 2) { // !!!!
                        retval = -1;
                        goto leave;
                    }
                    RtlCopyMemory(pData, pDeviceInfo->ustrName.Buffer, pDeviceInfo->ustrName.Length);
                    ((WCHAR*)pData)[1] = '\'; // make it null terminated
                    ((WCHAR*)pData)[cbOutSize - 1] = 0; // make it null terminated
                    break;

                case RIDI_DEVICEINFO:
                    {
                        PRID_DEVICE_INFO prdi = (PRID_DEVICE_INFO)pData;

                        ProbeForRead(prdi, sizeof(UINT), sizeof(DWORD));
                        if (prdi->cbSize != cbOutSize) {
                            MSGERRORCLEANUP(ERROR_INVALID_PARAMETER);
                        }
                        ProbeForWrite(prdi, sizeof(RID_DEVICE_INFO), sizeof(DWORD));
                        RtlZeroMemory(prdi, sizeof(RID_DEVICE_INFO));
                        prdi->cbSize = cbOutSize;

                        switch (pDeviceInfo->type) {
                        case DEVICE_TYPE_HID:
                            prdi->dwType = RIM_TYPEHID;
                            prdi->hid.dwVendorId = pDeviceInfo->hid.pHidDesc->hidCollectionInfo.VendorID;
                            prdi->hid.dwProductId = pDeviceInfo->hid.pHidDesc->hidCollectionInfo.ProductID;
                            prdi->hid.dwVersionNumber = pDeviceInfo->hid.pHidDesc->hidCollectionInfo.VersionNumber;
                            prdi->hid.usUsagePage = pDeviceInfo->hid.pHidDesc->hidpCaps.UsagePage;
                            prdi->hid.usUsage = pDeviceInfo->hid.pHidDesc->hidpCaps.Usage;
                            break;

                        case DEVICE_TYPE_MOUSE:
                            prdi->dwType = RIM_TYPEMOUSE;
                            prdi->mouse.dwId = pDeviceInfo->mouse.Attr.MouseIdentifier;
                            prdi->mouse.dwNumberOfButtons = pDeviceInfo->mouse.Attr.NumberOfButtons;
                            prdi->mouse.dwSampleRate = pDeviceInfo->mouse.Attr.SampleRate;
                            break;

                        case DEVICE_TYPE_KEYBOARD:
                            prdi->dwType = RIM_TYPEKEYBOARD;
                            prdi->keyboard.dwType = GET_KEYBOARD_DEVINFO_TYPE(pDeviceInfo);
                            prdi->keyboard.dwSubType = GET_KEYBOARD_DEVINFO_SUBTYPE(pDeviceInfo);
                            prdi->keyboard.dwKeyboardMode = pDeviceInfo->keyboard.Attr.KeyboardMode;
                            prdi->keyboard.dwNumberOfFunctionKeys = pDeviceInfo->keyboard.Attr.NumberOfFunctionKeys;
                            prdi->keyboard.dwNumberOfIndicators = pDeviceInfo->keyboard.Attr.NumberOfIndicators;
                            prdi->keyboard.dwNumberOfKeysTotal = pDeviceInfo->keyboard.Attr.NumberOfKeysTotal;
                            break;
                        }
                    }
                    break;

                default:
                    __assume(false);
                }
            } except (EXCEPTION_EXECUTE_HANDLER) {
                UserSetLastError(RtlNtStatusToDosError(GetExceptionCode()));// ERROR_NOACCESS   
                retval = -1;
                goto leave;
            }
            retval = cbOutSize;
        } else {
            /*
             * The buffer size is too small.
             * Returns error, storing the required size in *pcbSize.
             */
            retval = -1;
            try {
                ProbeForWrite(pcbSize, sizeof(UINT), sizeof(DWORD));
                *pcbSize = cbOutSize;
                UserSetLastError(ERROR_INSUFFICIENT_BUFFER);
            } except (EXCEPTION_EXECUTE_HANDLER) {
                UserSetLastError(RtlNtStatusToDosError(GetExceptionCode()));// ERROR_NOACCESS   
                retval = -1;
                goto leave;
            }
        }
    }

leave:
    LeaveDeviceInfoListCrit_();
leave1:
    UserSessionSwitchLeaveCrit();

    return retval;
}

然后 GetRawInputDeviceInfoA 添加额外的错误比较 GetRawInputDeviceInfoW - 来自 *pcbSize 的值由于某种原因在 2 上倍增。但同样 - 这个错误在所有案件。 请注意,DeviceName(由 IRP_MN_QUERY_ID 上的驱动程序 return 格式化的字符串有非常严格的限制:

If a driver returns an ID with an illegal character, the system will bug check. Characters with the following values are illegal in an ID for this IRP:

  • Less than or equal to 0x20 (' ')
  • Greater than 0x7F
  • Equal to 0x2C (',')

所以即使在将 unicode 转换为 ansi 之后 - 设备名称的长度将是相同的(所有符号 < 0x80 )。所以 Ansi 版本不需要 *2 缓冲区大小。


那么我已经在你的代码中看到错误 - 你在 GetRawInputDeviceInfoW 之后无条件地调用 ::GetLastError(); - 但是 returned 值只有在 api [=203] 的情况下才有意义=] -1

解释观察到的行为:

对于本地设备 api 一般工作正确(如果我们的代码没有错误) 对于终端服务设备 - 长度为 0 ustrName。结果,如果我们在 pData 中传递 NULL - return 值将是

pDeviceInfo->ustrName.Length / sizeof(WCHAR) + 1;

因为 pDeviceInfo->ustrName.Length == 0 - 1 将被 returned inside *pcbSize 如果 A 版本 - - 错误 - 2*1==2 将被 returned。 但是当 e 在 pData 中不通过 NULL - 我们陷入这个

            if (cbOutSize <= 2) { // !!!! error !!!!
                retval = -1;
                goto leave;
            }

因此您可以按大小传递任何缓冲区,无论如何,因为 (cbOutSize <= 2) - -1 将被 returned 并且最后一个错误 未设置


可能的解决方案 - 首先 - 从不使用 ansi 版本 - GetRawInputDeviceInfoA

使用这个包装函数。

ULONG GetRawInputDeviceInfoExW(_In_opt_ HANDLE hDevice, 
                               _In_ UINT uiCommand, 
                               _Inout_updates_bytes_to_opt_(*pcbSize, *pcbSize) LPVOID pData, 
                               _Inout_ PUINT pcbSize)
{
    switch (int i = GetRawInputDeviceInfoW(hDevice, uiCommand, pData, pcbSize))
    {
    case 0:
        return ERROR_INSUFFICIENT_BUFFER;
    case 1:
        return ERROR_INVALID_NAME;
    default:
        if (0 > i)
        {
            return GetLastError();
        }
        *pcbSize = i;

        return NOERROR;
    }
}

使用示例:(/RTCs 必须禁用)

void Demo()
{
    PRAWINPUTDEVICELIST pRawInputDeviceList = 0;
    UINT uiNumDevices = 0; 
    UINT cch, cchAllocated = 0;
    union {
        PVOID buf;
        PWSTR name;
    };

    buf = 0;

    while (0 <= (int)GetRawInputDeviceList(pRawInputDeviceList, &uiNumDevices, sizeof(RAWINPUTDEVICELIST)))
    {
        if (pRawInputDeviceList)
        {
            do 
            {
                HANDLE hDevice = pRawInputDeviceList->hDevice;

                ULONG dwError;
                while (ERROR_INSUFFICIENT_BUFFER == (dwError = 
                    GetRawInputDeviceInfoExW(hDevice, RIDI_DEVICENAME, name, &(cch = cchAllocated))))
                {
                    if (cch > cchAllocated)
                    {
                        cchAllocated = RtlPointerToOffset(buf = alloca((cch - cchAllocated) * sizeof(WCHAR)), 
                            pRawInputDeviceList) / sizeof(WCHAR);
                    }
                    else
                    {
                        __debugbreak();
                    }
                }

                if (dwError == NOERROR)
                {
                    DbgPrint("[%p, %x %S]\n", hDevice, pRawInputDeviceList->dwType, name);
                }
                else
                {
                    DbgPrint("error = %u\n", dwError);
                }

            } while (pRawInputDeviceList++, --uiNumDevices);

            break;
        }
        pRawInputDeviceList = (PRAWINPUTDEVICELIST)alloca(uiNumDevices * sizeof(RAWINPUTDEVICELIST));
    }
}

这段代码在我的电脑上运行良好。不确定,但确实可能是 RDP 问题。

UINT result = ::GetRawInputDeviceInfoW(m_Handle, RIDI_DEVICENAME, nullptr, &size);
if (result == static_cast<UINT>(-1))
{
    //PLOG(ERROR) << "GetRawInputDeviceInfo() failed";
    return false;
}
DCHECK_EQ(0u, result);

std::wstring buffer(size, 0);
result = ::GetRawInputDeviceInfoW(m_Handle, RIDI_DEVICENAME, buffer.data(), &size);
if (result == static_cast<UINT>(-1))
{
    //PLOG(ERROR) << "GetRawInputDeviceInfo() failed";
    return false;
}
DCHECK_EQ(size, result);