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
。如果pData
是NULL
,则函数返回零值。在这两种情况下,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
...但我需要接口路径才能走得更远。
【问题讨论】:
是的,这个 api 确实出错了。它适用于本地设备,但正如您所注意到的 -ERROR_INSUFFICIENT_BUFFER
和虚拟 erd 鼠标/键盘的 cbSize 中的错误数据。
您的代码有什么问题 - 您在 GetRawInputDeviceInfoW 之后无条件调用/检查 GetLastError,当 api 设置最后一个错误时,仅在 ( UINT)-1 返回。在其他情况下 - GetLastError 的值是随机的
【参考方案1】:
GetRawInputDeviceName
在声明/实现/文档中有几个错误
事实上更正确地将返回值声明为有符号(LONG
或 INT
)但不是 UINT
存在3种情况:
1. 函数返回负值(或者如果想要-1
):这是错误
案例和设计 - 必须设置最后一个错误。但实际上不是
总是设置(实现错误)。
最常见的错误:
pcbSize 或 pData 指向无效或只读内存位置。这种情况下的常见错误ERROR_NOACCESS
(翻译自
STATUS_ACCESS_VIOLATION
)
hDevice 无效句柄 - 返回 ERROR_INVALID_HANDLE
uiCommand 无效 RIDI_XXX 常量 - ERROR_INVALID_PARAMETER
*pcbSize 对数据来说不够大 - 在这种情况下,*pcbSize 设置为 pData 缓冲区所需的最小大小。 ERROR_INSUFFICIENT_BUFFER
再次 - 仅在这种情况下 (-1
) 存在感知调用 GetLastError();
2. 函数只有在 pData 为 NULL 的情况下才可能返回 0。 *pcbSize 设置为 pData 缓冲区所需的最小大小。
3.函数返回正值(>0)这意味着这个计数
字节(以防 RIDI_PREPARSEDDATA
或 RIDI_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;
//...
需要cbOutSize
与cbBufferSize = *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
(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
上的驱动程序返回的字符串格式化)有非常严格的限制:
如果司机返回带有非法字符的 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->ustrName.Length == 0
- 1 将在 *pcbSize 内返回
如果 A 版本 - - 错误 - 2*1==2
将被返回。
但是当 e 在 pData 中传递的不是 NULL - 我们会陷入这个
if (cbOutSize <= 2) // !!!! error !!!!
retval = -1;
goto leave;
所以无论如何你都可以按大小传递任何缓冲区,因为(cbOutSize <= 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_NOACCESS
或ERROR_INVALID_HANDLE
或ERROR_INVALID_PARAMETER
。
我立即致电::GetLastError()
以确保在其他任何更改之前捕获它(例如std::cerr
的用法)。但我只在函数返回 0 或 -1 时检查返回值。你在想象那里有一个错误。
我确实怀疑 RDP 环境中的事情可能很奇怪,所以我也直接在我正在建立 RDP 连接的计算机上运行。但即使字符串的长度为零,该函数仍然不应该失败,它应该告诉我它需要一个字符(对于 NUL),然后当我给它一个字符的空间时它应该成功。但事实并非如此。
感谢您在底层代码中找到if (cbOutSize <= 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 个字符的主要内容,如果未能解决你的问题,请参考以下文章
将“活动指示器”添加到 TableView 并在 Swift 中恢复为“披露指示器”