一次 Kingston U 盘重定向中获取 Device Descriptor 超时问题排查

背景

最近在实现一个基于 USB/IP 的 U 盘重定向功能。整体架构大致如下:

  • 本地 Windows 客户端通过 libusb + UsbDk 访问真实 USB 设备;
  • 客户端接收远端 USB/IP 请求;
  • 本地通过 libusb_control_transferlibusb_bulk_transfer 等接口完成真实 USB 通信;
  • 服务端 Linux 通过 vhci_hcd 创建虚拟 USB 设备。

测试过程中,某款 Kingston DataTraveler 3.0 U 盘在多次重定向、断网重连后,偶现服务端无法正常枚举设备,最终远端无法出现可用盘符或块设备。

现象

服务端 dmesg 中可以看到类似日志:

复制代码
unable to get BOS descriptor or descriptor too short
unable to read config index 0 descriptor/all
can't read configurations, error -110
unable to enumerate USB device

其中 error -110 对应 Linux 的 ETIMEDOUT,说明服务端 USB 栈在枚举阶段等待描述符响应超时。

客户端日志中也能看到 libusb_control_transfer 返回超时:

复制代码
libusb_control_transfer err: -7
descType=2

-7 对应:

复制代码
LIBUSB_ERROR_TIMEOUT

最初从日志看,失败点主要集中在:

  • BOS descriptor,descType = 15
  • Configuration descriptor,descType = 2

因此一开始怀疑是 BOS 或 config descriptor 请求本身存在问题。

排查过程

为了避免只依赖完整产品流程,我们单独写了一个测试程序,对同一个 U 盘反复执行不同类型的 descriptor 请求。

测试项包括:

复制代码
GET_DESCRIPTOR DEVICE, wLength = 8
GET_DESCRIPTOR DEVICE, wLength = 18
GET_DESCRIPTOR BOS
GET_DESCRIPTOR CONFIG
libusb_get_device_descriptor 缓存路径
libusb_get_config_descriptor 缓存路径

经过多轮测试,发现一个关键现象:

  • 单独反复请求 BOS descriptor,基本稳定;
  • 单独反复请求 config descriptor,基本稳定;
  • 使用 libusb_get_device_descriptor() 缓存路径,基本稳定;
  • 但反复真实下发 GET_DESCRIPTOR DEVICE 请求时,较容易出现 timeout;
  • wLength = 8wLength = 18 都可能触发。

也就是说,表面上服务端失败点可能落在 BOS/config descriptor,但真正更可疑的是前面反复执行的 device descriptor control 请求。

为什么会有 8 字节 Device Descriptor 请求

USB 枚举过程中,主机经常会先读取 device descriptor 的前 8 字节,用于获取:

复制代码
bMaxPacketSize0

也就是端点 0 的最大包大小。

完整 device descriptor 长度是 18 字节,但请求中的 wLength 不一定只会是 18。常见请求包括:

复制代码
wValue = 0x0100, wLength = 8
wValue = 0x0100, wLength = 18

甚至也可能出现更大的 wLength,设备通过 short packet 返回实际的 18 字节。

代码中的问题

原来的代码已经对完整的 18 字节 device descriptor 做了特殊处理:

复制代码
if (GET_DESCRIPTOR DEVICE && wLength == LIBUSB_DT_DEVICE_SIZE)
{
    libusb_get_device_descriptor(...);
}

也就是说,当远端请求 18 字节 device descriptor 时,本地不会真实下发 control transfer,而是直接使用 libusb 缓存的 device descriptor。

但是对于 8 字节请求,旧逻辑没有覆盖,仍然会走:

复制代码
libusb_control_transfer(...)

实际日志中也验证了这一点:

复制代码
route=control
wValue=0x100
wLength=8
descType=1

这意味着每次重定向或断网重连时,都会真实向设备发送一次 GET_DESCRIPTOR DEVICE, wLength=8

修复思路

修复方向是:所有标准 device descriptor 请求都走缓存路径,而不是只处理 18 字节请求。

判断条件从:

复制代码
wLength == LIBUSB_DT_DEVICE_SIZE

改为:

复制代码
wLength > 0

并返回:

复制代码
min(wLength, LIBUSB_DT_DEVICE_SIZE)

示例逻辑:

复制代码
if (requestType == LIBUSB_ENDPOINT_IN &&
    bRequest == LIBUSB_REQUEST_GET_DESCRIPTOR &&
    wValue == (LIBUSB_DT_DEVICE << 8) &&
    wIndex == 0 &&
    wLength > 0)
{
    struct libusb_device_descriptor desc = {0};

    status = libusb_get_device_descriptor(dev, &desc);
    if (status == LIBUSB_SUCCESS)
    {
        desc.bcdUSB = libusb_cpu_to_le16(desc.bcdUSB);
        desc.idVendor = libusb_cpu_to_le16(desc.idVendor);
        desc.idProduct = libusb_cpu_to_le16(desc.idProduct);
        desc.bcdDevice = libusb_cpu_to_le16(desc.bcdDevice);

        uint16_t len = std::min<uint16_t>(wLength, LIBUSB_DT_DEVICE_SIZE);
        memcpy(data, &desc, len);
        status = len;
    }
}

这里需要注意,libusb_get_device_descriptor() 返回的是 host-endian 结构体,而 USB descriptor 在线路上是 little-endian。因此多字节字段最好通过 libusb_cpu_to_le16() 转换后再返回。

结论

这次问题的核心不是简单的 BOS/config descriptor 请求失败,而是:

libusb + UsbDk + Kingston DataTraveler 3.0 的组合场景下,反复真实下发 GET_DESCRIPTOR DEVICE 请求,尤其是 8 字节请求,可能导致设备或 EP0 control 通道进入不稳定状态,后续 BOS/config 请求表现为 timeout。

修复后,将所有 device descriptor 请求统一改为使用 libusb_get_device_descriptor() 的缓存结果返回,避免重复真实访问设备 EP0,问题得到规避。

经验总结

  1. descriptor timeout 的失败点不一定是真正根因。
  2. USB 枚举中的 8 字节 device descriptor 请求非常常见,不能只处理 18 字节版本。
  3. libusb_get_device_descriptor() 是缓存路径,不会再次向设备发送 control 请求。
  4. 在 USB/IP 重定向场景中,尽量减少对 EP0 的重复真实请求。
  5. 对特殊 U 盘兼容性问题,最好用独立测试程序拆分验证,而不是只看完整产品日志。
相关推荐
UestcXiye1 小时前
GoogleTest 使用指南 | 单元覆盖率分析
c++·单元测试·googletest
王老师青少年编程1 小时前
csp信奥赛C++高频考点专项训练之前缀和&差分 --【一维前缀和】:“非常男女”计划
c++·前缀和·csp·高频考点·信奥赛·“非常男女”计划
故事和你911 小时前
洛谷-【图论2-4】连通性问题2
开发语言·数据结构·c++·算法·动态规划·图论
Brilliantwxx1 小时前
【C++】 二叉搜索树
开发语言·c++·算法
于小猿Sup12 小时前
VMware在Ubuntu22.04驱动Livox Mid360s
linux·c++·嵌入式硬件·自动驾驶
小小编程路14 小时前
C++ 多线程与并发
java·jvm·c++
程序leo源15 小时前
Qt窗口详解
开发语言·数据库·c++·qt·青少年编程·c#
zh_xuan16 小时前
解决VS Code 控制台中文乱码
c++·vscode·乱码
郭涤生16 小时前
飞凌 RK3588 开发板同显 / 异显模式切换
c++·rk3588