Skip to content

Instantly share code, notes, and snippets.

@jigpu
Last active March 6, 2024 18:54
Show Gist options
  • Save jigpu/deb58497d7897fe731d0af2bfb58a574 to your computer and use it in GitHub Desktop.
Save jigpu/deb58497d7897fe731d0af2bfb58a574 to your computer and use it in GitHub Desktop.
Scan the USB bus and print out the BOS descriptors for each device, if they exist.
#!/usr/bin/python3
# dump_bos_descriptors.py
#
# Scan the USB bus and print out the BOS descriptors for each device, if
# they exist. Alternatively, decode and print out the BOS descriptor that
# is passed in via stdin.
#
# Usage: `./dump_bos_descriptors.py [< binary]`
#
# Requirements:
# - Python3 (sudo dnf install python3)
# - PyUSB (sudo dnf install python3-pyusb)
#
# This tool is designed to make it easy to quickly find and see the
# contents of the BOS (Binary Device Object Store) descriptor that is
# present on some USB devices. This descriptor may be used for several
# purposes such as providing information on SuperSpeed capabilities or
# platform capabilities.
#
# This tool is especially intended for examining BOS DS20 descriptors
# defined by the fwupd project. In addition to dumping data in the
# descriptor itself, this tool will dump the quirk data that it points
# to.
#
# This tool prints out both a human-readable summary of the fields
# contained in each descriptor as well as a binary dump of the raw
# data for manual analysis.
import sys
import os
import usb.core
import usb.util
import errno
import struct
import uuid
from collections import namedtuple
import textwrap
def extract_header(typename, structspec, tuplespec, data):
hdr_type = namedtuple(typename, tuplespec)
hdr_bytes = data[0 : struct.calcsize(structspec)]
return hdr_type._make(struct.unpack(structspec, hdr_bytes))
class BosDescriptor:
# See following sections in USB 3.2, Revision 1.1 spec:
# - 9.6.2 Binary Device Object Store (BOS)
# - Table 9-12. BOS Descriptor
HEADER_STRUCTSPEC = "<BBHB"
HEADER_TUPLESPEC = "bLength, bDescriptorType, wTotalLength, bNumDeviceCaps"
def __init__(self, data):
self._data = bytes(data)
def __str__(self):
return f"BosDescriptor with {self._header.bNumDeviceCaps} capabilities in {self._header.wTotalLength} bytes"
def __repr__(self):
return f"BosDescriptor({self._data})"
@property
def _header(self):
return extract_header(
"Header", self.HEADER_STRUCTSPEC, self.HEADER_TUPLESPEC, self._data
)
@staticmethod
def read(device, length=5):
"""
Read the BosDescriptor from a device. Reads up to 'length' bytes
from the device, or only the header if not specified.
"""
try:
# See following sections in USB 3.2, Revision 1.1 spec:
# - Table 9-3. Format of Setup Data
# - Table 9-4. Standard Device Requests
# - Table 9-5. Standard Request Codes
# - Table 9-6. Descriptor Types
# - 9.4.3 Get Descriptor
data = device.ctrl_transfer(
0x80, # Device-to-host | Standard | Device
0x06, # GET_DESCRIPTOR
0x0F00, # BOS << 8 | 0x00
0x0000,
length,
)
result = BosDescriptor(data)
if not result.is_valid():
raise Exception(
f"Data does not appear to be a BOS descriptor: {repr(result)}"
)
if length == 5:
result = BosDescriptor.read(device, result._header.wTotalLength)
return result
except usb.core.USBError as e:
if e.errno == errno.EPIPE:
raise Exception("Device does not appear to have a BOS descriptor")
raise e
def is_valid(self):
return (
self._header.bLength == 0x05
and self._header.bDescriptorType == 0x0F
and self._header.wTotalLength >= 0x05 + self._header.bNumDeviceCaps * 3
and self._header.bNumDeviceCaps > 0
and (len(self._data) == self._header.wTotalLength or len(self._data) == 5)
)
def capabilities(self):
"""
Return a list of BosDeviceCapabilityDescriptor objects contained in this BOS.
"""
result = []
offset = struct.calcsize(self.HEADER_STRUCTSPEC)
while offset < self._header.wTotalLength:
bLength = self._data[offset]
capability_data = self._data[offset : offset + bLength]
capability = BosDeviceCapabilityDescriptor(capability_data)
result.append(capability)
offset += capability._header.bLength
assert offset == self._header.wTotalLength
return result
class BosDeviceCapabilityDescriptor:
# See following sections in USB 3.2, Revision 1.1 spec:
# - 9.6.2 Binary Device Object Store (BOS)
# - Table 9-13. Format of a Device Capability Descriptor
HEADER_STRUCTSPEC = "<BBB"
HEADER_TUPLESPEC = "bLength, bDescriptorType, bDevCapabilityType"
def __init__(self, data):
self._data = data
def __str__(self):
return f"{type(self).__name__} :: {self.properties}"
def __repr__(self):
return f"{type(self).__name__}({self._data})"
@property
def _header(self):
return extract_header(
"Header", self.HEADER_STRUCTSPEC, self.HEADER_TUPLESPEC, self._data
)
@property
def properties(self):
return self._header._asdict()
@property
def _body(self):
return data[struct.calcsize(structspec) :]
def find_subclass(self):
"""
Find a more specialized subclass for this descriptor if possible.
This object can be coerced into that subclass.
"""
def try_coerce(typ):
copy = BosDeviceCapabilityDescriptor(self._data)
copy.__class__ = typ
try:
return copy.is_valid()
except:
return False
# TODO: Define subclasses for other capabilities and add
# them to this list.
for typ in [
BosDs20Descriptor,
BosPlatformDescriptor,
BosUsb20ExtensionDescriptor,
BosSuperspeedUsbDescriptor,
BosSuperspeedPlusDescriptor,
]:
if try_coerce(typ):
return typ
return None
class BosUsb20ExtensionDescriptor(BosDeviceCapabilityDescriptor):
# See following sections in USB 3.2, Revision 1.1 spec:
# - 9.6.2.1 USB 2.0 Extension
# - Table 9-14. Device Capability Type Codes
HEADER_STRUCTSPEC = BosDeviceCapabilityDescriptor.HEADER_STRUCTSPEC + "I"
HEADER_TUPLESPEC = BosDeviceCapabilityDescriptor.HEADER_TUPLESPEC + ", bmAttributes"
def is_valid(self):
return (
self._header.bDescriptorType == 0x10
and self._header.bDevCapabilityType == 0x02
)
class BosSuperspeedUsbDescriptor(BosDeviceCapabilityDescriptor):
# See following sections in USB 3.2, Revision 1.1 spec:
# - 9.6.2.2 SuperSpeed USB Device Capability
# - Table 9-14. Device Capability Type Codes
HEADER_STRUCTSPEC = BosDeviceCapabilityDescriptor.HEADER_STRUCTSPEC + "BHBBH"
HEADER_TUPLESPEC = (
BosDeviceCapabilityDescriptor.HEADER_TUPLESPEC
+ ", bmAttributes, wSpeedsSupported, bFunctionalitySupport, bU1DevExitLat, wU2DevExitLat"
)
def is_valid(self):
return (
self._header.bDescriptorType == 0x10
and self._header.bDevCapabilityType == 0x03
)
class BosPlatformDescriptor(BosDeviceCapabilityDescriptor):
# See following sections in USB 3.2, Revision 1.1 spec:
# - 9.6.2.4 Platform Descriptor
# - Table 9-14. Device Capability Type Codes
HEADER_STRUCTSPEC = BosDeviceCapabilityDescriptor.HEADER_STRUCTSPEC + "B16s"
HEADER_TUPLESPEC = (
BosDeviceCapabilityDescriptor.HEADER_TUPLESPEC
+ ", bReserved, PlatformCapabilityUUID"
)
@property
def uuid(self):
return uuid.UUID(bytes_le=self._header.PlatformCapabilityUUID)
@property
def properties(self):
data = super().properties
data["PlatformCapabilityUUID"] = self.uuid
data["valid"] = self.is_valid()
return data
def is_valid(self):
return (
self._header.bDescriptorType == 0x10
and self._header.bDevCapabilityType == 0x05
and self._header.bReserved == 0
and self.uuid is not None
)
class BosDs20Descriptor(BosPlatformDescriptor):
# See https://fwupd.github.io/libfwupdplugin/ds20.html
HEADER_STRUCTSPEC = BosPlatformDescriptor.HEADER_STRUCTSPEC + "4sHBB"
HEADER_TUPLESPEC = (
BosPlatformDescriptor.HEADER_TUPLESPEC
+ ", dwVersion, wLength, bVendorCode, bAltEnumCode"
)
@property
def properties(self):
data = super().properties
data["dwVersion"] = self.version
return data
@property
def version(self):
micro, minor, major, epoch = self._header.dwVersion
if epoch == 0:
return f"{major}.{minor}.{micro}"
raise Exception("Unknown version number format")
def is_valid(self):
return super().is_valid() and self.uuid == uuid.UUID(
"010aec63-f574-52cd-9dda-2852550d94f0"
)
def read_ds20_data(self, device):
return device.ctrl_transfer(
0xC0, self._header.bVendorCode, 0x0000, 0x0007, self._header.wLength
)
class BosSuperspeedPlusDescriptor(BosDeviceCapabilityDescriptor):
# See following sections in USB 3.2, Revision 1.1 spec:
# - 9.6.2.2 SuperSpeed USB Device Capability
# - Table 9-14. Device Capability Type Codes
HEADER_STRUCTSPEC = BosDeviceCapabilityDescriptor.HEADER_STRUCTSPEC + "BIHHI"
HEADER_TUPLESPEC = (
BosDeviceCapabilityDescriptor.HEADER_TUPLESPEC
+ ", bReserved, bmAttributes, wFunctionalitySupport, wReserved, bmSublinkSpeedAttr0"
)
def is_valid(self):
return (
self._header.bDescriptorType == 0x10
and self._header.bDevCapabilityType == 0x0A
)
def dump_ds20_quirk(bos, device):
capabilities = bos.capabilities()
for idx, cap in enumerate(capabilities):
subclass = cap.find_subclass()
if subclass is not None:
cap.__class__ = subclass
if subclass == BosDs20Descriptor:
if cap.is_valid():
ds20_data = cap.read_ds20_data(device)
print(
textwrap.indent(
f"DS20 Bytes (Cap {idx + 1}): {bytes(ds20_data)}", " ** "
)
)
print(
textwrap.indent(
f"DS20 Hex (Cap {idx + 1}): {bytes(ds20_data).hex()}", " ** "
)
)
else:
print(textwrap.indent("DS20 Descriptor is not valid", " ** "))
def dump_bos(bos):
print(str(bos))
print(repr(bos))
capabilities = bos.capabilities()
for idx, cap in enumerate(capabilities):
print(" -----")
subclass = cap.find_subclass()
if subclass is not None:
cap.__class__ = subclass
print(textwrap.indent(str(cap), f" Capability {idx + 1} :: "))
print(textwrap.indent(repr(cap), " >> "))
def dump_device(device):
vidpid = "{:02x}:{:02x}".format(device.idVendor, device.idProduct)
print(vidpid)
bos = None
try:
bos = BosDescriptor.read(device)
except Exception as e:
print(e)
return
dump_bos(bos)
dump_ds20_quirk(bos, device)
def dump_single_device(vid, pid):
device = usb.core.find(idVendor=vid, idProduct=pid)
if device is None:
raise Exception("Unable to find device")
dump_bos(device)
def dump_all_devices():
for device in usb.core.find(find_all=True):
dump_device(device)
print()
os.set_blocking(sys.stdin.fileno(), False)
user_data = sys.stdin.buffer.read()
if user_data is None:
dump_all_devices()
else:
bos = BosDescriptor(user_data)
if not bos.is_valid():
raise Exception(f"Data does not appear to be a BOS descriptor: {repr(bos)}")
dump_bos(bos)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment