What is the right way to read and parse configuration descriptors?


Hi, my name is Fizalkhan Peermohamed. I am a Developer in the Windows USB team. In this post, I am going to describe the right way to read and parse configuration descriptors to avoid system crashes due to malformed descriptors.


 


First, I will describe how client drivers today retreive configuration descriptors from a device, then describe how USBD_ParseConfigurationDescriptorEx function parse and extract the interface descriptor from it, then describe how malformed configuration descriptors can cause the parse function to overrun the buffer and crash the system, and then finally describe a new function that we have added in Windows 7 to safely validate descriptors before parsing. 


 


Let us find out how client drivers today obtain configuration descriptors:


 


1)  Client drivers use UsbBuidGetDescriptorRequest to build descriptor read requests. By passing in USB_CONFIGURATION_DESCRIPTOR_TYPE as the second parameter to this function a client driver could build a configuration descriptor read request.


 


2)  The client driver then sends this read request to the device to get the data belonging to the configuration descriptor with a read length equal to the sizeof(USB_CONFIGURATION_DESCRIPTOR) – which is 9 bytes. A configuration descriptor layout is as shown below:  


  


CONFIGURATION DESCRIPTOR TABLE































Offset


Field


0


bLength


1


bDescriptorType


2


wTotalLength


4


bNumInterfaces


5


bConfigurationValue


6


iConfiguration


7


bmAttributes


8


bMaxPower


 


 


 


 


 


 


 


 


 


 


 


 


3)  Once the client driver reads the nine bytes of the configuration descriptor, it looks at the wTotalLength field to figure out how many bytes to allocate for all the descriptors including the configuration descriptor.


There is no method in USB to get the interface and endpoint descriptors separately. These have to be read along with the configuration descriptors. The wTotalLength field in the configuration descriptor gives you the total length of the buffer needed to accommodate all these descriptors.


 


4)  The client driver then performs a memory allocation for wTotalLength bytes, and issues a second configuration descriptor read request with that size.


 


5)  It then calls USBD_ParseConfigurationDescriptorEx and passes the descriptor buffer it read in step 4 above to extrace the interfaces present in the configuration.


 


What does the USBD_ParseConfigurationDescriptorEx function do?

 


This function essentially walks the list of descriptors in the descriptor buffer to extract the interface descriptor. The first byte in these descriptors is always a bLength field which gives the length of the descriptor, while the second field is a bType field which tells you what kind of descriptor it is. USBD_ParseConfigurationDescriptorEx uses the bLength field to walk the list of descriptors and uses the bType field to find the interface descriptor. USBD_ParseConfigurationDescriptorEx also uses the wTotalLength field in the configuration descriptor to know where it should stop parsing. All this would work very well in an ideal world where there is no hardware error or glitches.


 


Let’s see what would happen in the following sequence:




  • The client driver reads the first nine bytes of the configuration descriptor. All the fields are correct in this read.


  • The client driver allocates wTotalLength amount of memory for the full (all descriptors) buffer and issues another configuration read. This time the wTotalLength value in the configuration descriptors is invalid – it is greater than the wTotalLength value we received in the first read. This could be because of a hardware error.


  • As a result, USBD_ParseConfigurationDescriptorEx walks beyond the original wTotalLength bytes in the buffer and tries to access unallocated memory and crashes the system.


Even though the hardware error is not the fault of the client driver, the client driver would get blamed for the system crash.


 


How can the client driver protect itself from this?


 


We introduced a new function on Windows7 called USBD_ValidateConfigurationDescriptor to validate the descriptors and flag an error if they are malformed so that the client driver can take appropriate action. This function is currently not documented in the WDK. The MSDN documentation for this function will be made public very soon. However the protoype for this function is in usbdlib.h and exported for public use in Windows7 WDK. 


 


__drv_maxIRQL(DISPATCH_LEVEL)


DECLSPEC_IMPORT


USBD_STATUS


USBD_ValidateConfigurationDescriptor(


    __in_bcount(BufferLength) PUSB_CONFIGURATION_DESCRIPTOR ConfigDesc,


    __in ULONG BufferLength,


    __in USHORT Level,


    __out PUCHAR *Offset,


    __in_opt ULONG Tag)


 


Unlike USBD_ParseConfigurationDescriptorEx, you provide the length of the allocated buffer to this function. This function looks at the retrieved descriptors and flags as an error if the descriptors would make you walk out of bounds.


 


This function takes a Level parameter from 1-3 which would do progressively more checking as you go from 1-3.


 


Using a Level value of 1 would check to make sure that the wTotalLength field is within the bounds of the allocated memory


Using a level value of 2 would check to make sure that the bLength field in the individual descriptors is also within buffer bounds.


 


The following is a list of the checks that are done for each level as documented in the function prototype in usbdlib.h:


 


Level 1:


·         Check if BufferLength is atleast as big as size of USB_CONFIGURATION_DESCRIPTOR


·         Check if ConfigDesc->bLength is atleast as big as size of USB_CONFIGURATION_DESCRIPTOR . This would mean a zero bLength ConfigDesc would be flagged as an error


·         Check if ConfigDesc->wTotalLength accounts for atleast the number of interfaces reported  by


ConfigDesc->bNumInterfaces


 


Level 2: Includes Level 1 checking and Full pass-through of the config descriptor checking for the following:


·         Unique endpoint addresses and interface numbers


·         Number of interfaces contained in the descriptor


·         Ensures the bLength values of the USB descriptors do not exceed the length of the buffer.


·         Basic validation of information in a USB_INTERFACE_ASSOCIATION_DESCRIPTOR


              


Level 3: Includes all of the validation for levels 1-2 plus the following:


·         Validation of the number of endpoints in each interface


·         Enforcement of the USB spec descriptor bLength sizes. This means that a zeo bLength Descriptor would be flagged as an error.


·         Check to see if all interface numbers are in sequential(not necessarily consecutive) order.


 


Also when it finds an error this function returns a pointer to the location at which the error occurred, in the offset parameter.


 


Summary: A client driver should use the following sequence to safely parse the descriptors:




 


·         Read first nine bytes of the configuration descriptors


·         Allocate Config->wTotalLength bytes memory


·         Read Config->wTotalLength bytes of the descriptors


·         Call USBD_ValidateConfigurationDescriptor and if it returns success then call USBD_ParseConfigurationDescriptorEx, else do error processing.


 


 


The following pseudo-code illustrates how to get a configuration descriptors and validate it:           


 


Begin GetValidatedConfigDescriptorSet


 


Urb  =  AllocateMemory  (sizeof(URB_CONTROL_DESCRIPTOR_REQUEST))


ConfigDesc  =  AllocateMemory  (sizeof(USB_CONFIGURATION_DESCRIPTOR))


Call UsbBuildGetDescriptorRequest to Build Urb


Send Urb to USB Stack to read ConfigDesc and wait for it to complete


 


      If ConfigDesc->wTotalLength  > sizeof(USB_CONFIGURATION_DESCRIPTOR)


 


            Size = ConfigDesc->wTotalLength


            FreeMemory(ConfigDesc)


            ConfigDesc = AllocateMemory (Size)


 Call UsbBuildGetDescriptorRequest to Build Urb


       Send Urb to USB Stack to read ConfigDesc and wait for it to complete


 


      EndIf


 


      FreeMemory(Urb)


      Call USBD_ValidateConfigurationDescriptor


 


      If above call return success


            Return ConfigDesc


      Else


            FreeMemory(ConfigDesc)


            Return NULL


      EndIf


 


End GetValidatedConfigDescriptorSet


 


 

Comments (4)

  1. Ben Voigt [C++ MVP] says:

    So what is the recommended practice for downlevel systems?  Obviously the driver should check that the wTotalLength field has not changed.  Are the other checks (Level 2 & 3) needed to avoid a crash or only for correctness? (if the data is corrupt then correct results are a lost cause anyway)

    Also, even non-paged pool is pretty cheap compared to bus transfers, why wouldn’t one start with a moderate buffer size of say 4k, and in most cases avoid the need for a second descriptor transfer?

  2. USB Blog says:

    For downlevel systems that might not have the USBD_ValidateConfigurationDescriptor function , you could roll your own validation code. At the minimum you could check to make sure wTotalLength in the config descriptor and bTotalLength in each descriptor does not cause you to go off bounds.

    Allocating arbitrarily large amount of memory to read the descriptor is not advisable.But if you have device whose entire  descriptor strucutre size is fixed and you know what that number is in your driver , then you can read all the descriptors in one transfer

    USB Core

  3. J.R. Heisey says:

    I would like to see a similar example for user mode applications.

  4. Mike Kenyon says:

    winusb.h exposes two undocumented functions: WinUsb_ParseDescriptors() and WinUsb_ParseConfigurationDescriptors().  Are these functions similar in function to the above? Any chance of some documentation on them anyway?

    Thanks.

Skip to main content