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

Posted

技术标签:

【中文标题】GetRawInputDeviceInfo 指示 RIDI_DEVICENAME 的缓冲区大小为 1 个字符【英文标题】:GetRawInputDeviceInfo indicates a buffer size of 1 character for RIDI_DEVICENAME 【发布时间】:2021-04-09 21:16:01 【问题描述】:

RIDI_DEVICENAME 的行为很荒谬。根据文档,

返回值

输入:UINT

如果成功,该函数返回一个非负数,表示复制到pData的字节数。

如果pData 不足以容纳数据,则函数返回-1。如果 pDataNULL,则函数返回零值。在这两种情况下,pcbSize 都设置为pData 缓冲区所需的最小大小。

致电GetLastError 找出任何其他错误。

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

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

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

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

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

在以本地用户身份运行的 Windows 8.1 上,如果 pData 为空,我会得到 ERROR_INSUFFICIENT_BUFFER,当我提供缓冲区时,我会返回失败 ((UINT)-1) 并且 GetLastError() 返回零。

我也刚刚尝试提出一个可能足够大的缓冲区大小,但也失败了。

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

Windows HID Device Name Format https://***.com/a/64320052/103167

【问题讨论】:

是的,这个 api 确实出错了。它适用于本地设备,但正如您所注意到的 - ERROR_INSUFFICIENT_BUFFER 和虚拟 erd 鼠标/键盘的 cbSize 中的错误数据。 您的代码有什么问题 - 您在 GetRawInputDeviceInfoW 之后无条件调用/检查 GetLastError,当 api 设置最后一个错误时,仅在 ( UINT)-1 返回。在其他情况下 - GetLastError 的值是随机的 【参考方案1】:

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

事实上更正确地将返回值声明为有符号(LONGINT)但不是 UINT

存在3种情况:

1. 函数返回负值(或者如果想要-1):这是错误 案例和设计 - 必须设置最后一个错误。但实际上不是 总是设置(实现错误)。

最常见的错误:

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

hDevice 无效句柄 - 返回 ERROR_INVALID_HANDLE

uiCommand 无效 RIDI_XXX 常量 - ERROR_INVALID_PARAMETER

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

再次 - 仅在这种情况下 (-1) 存在感知调用 GetLastError();

2. 函数只有在 pDataNULL 的情况下才可能返回 0。 *pcbSize 设置为 pData 缓冲区所需的最小大小。

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

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

pcb尺寸 [进出]

指向一个变量的指针,该变量包含数据的大小(以字节为单位) pData.

如果RIDI_DEVICENAME字符

所以设计(返回值类型 - 无符号)和混合字节/字符已经可见非常严重的问题。许多不同的情况。

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

PDEVICEINFO pDeviceInfo = HMValidateHandle(hDevice, TYPE_DEVICEINFO);

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

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 &gt;= 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)(我们无法控制)。因此,如果名称长度为零 - 始终返回 -1error #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 上的驱动程序返回的字符串格式化)有非常严格的限制:

如果司机返回带有非法字符的 ID,系统将 错误检查。具有以下值的字符在 ID 中是非法的 对于这个 IRP:

小于等于0x20('') 大于 0x7F 等于 0x2C (',')

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


然后我已经在您的代码中查看了错误-您在 GetRawInputDeviceInfoW 之后无条件调用 ::GetLastError();-但返回值仅在 api 返回 -1 的情况下才有意义

解释观察到的行为:

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

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

因为pDeviceInfo-&gt;ustrName.Length == 0 - 1 将在 *pcbSize 内返回 如果 A 版本 - - 错误 - 2*1==2 将被返回。 但是当 e 在 pData 中传递的不是 NULL - 我们会陷入这个

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

所以无论如何你都可以按大小传递任何缓冲区,因为(cbOutSize &lt;= 2) - -1 将被返回并且最后一个错误未设置


可能的解决方案 - 一开始 - 永远不要使用 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));
    

【讨论】:

从我对std::wstring 的使用中可以看出,我已经在为RIDI_DEVICENAME 测量所有字符。我从未见过ERROR_NOACCESSERROR_INVALID_HANDLEERROR_INVALID_PARAMETER 我立即致电::GetLastError() 以确保在其他任何更改之前捕获它(例如std::cerr 的用法)。但我只在函数返回 0 或 -1 时检查返回值。你在想象那里有一个错误。 我确实怀疑 RDP 环境中的事情可能很奇怪,所以我也直接在我正在建立 RDP 连接的计算机上运行。但即使字符串的长度为零,该函数仍然不应该失败,它应该告诉我它需要一个字符(对于 NUL),然后当我给它一个字符的空间时它应该成功。但事实并非如此。 感谢您在底层代码中找到if (cbOutSize &lt;= 2),我将更新我的代码以强制更大的缓冲区。 @BenVoigt - 我从未见过 ERROR_NOACCESS.. 可能是我不好解释.. 我只是一般地解释什么错误可以返回这个 api 以及在什么条件下。 if (lasterror != ERROR_INSUFFICIENT_BUFFER) 我在你的代码中查看了这个 - 所以你在检查返回值之前无条件地检查最后一个错误。并且立即确保我在其他任何更改之前捕获它 - 这不是强制性的。最后一个错误只能作为某些 winapi 调用的结果进行更改 - 直接或间接。如果您只使用编译器内部结构 - 这不会调用 winapi 并且不会更改最后一个错误【参考方案2】:

此代码在我的 PC 上运行良好。不确定,但确实可能是 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);

【讨论】:

以上是关于GetRawInputDeviceInfo 指示 RIDI_DEVICENAME 的缓冲区大小为 1 个字符的主要内容,如果未能解决你的问题,请参考以下文章

Spring AOP 切点指示器

指示的英文翻译

将“活动指示器”添加到 TableView 并在 Swift 中恢复为“披露指示器”

SpringAOP所支持的AspectJ切点指示器

警报中的指示器,指示器不显示(使用 UIAlertController 解决)

Android自定义ViewPager图片指示器,兼容实现底部横线指示器