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