Last active
March 6, 2024 18:54
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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