WDFREQUESTs are not for sharing

FYI: this is a bit of a long post, but I wanted to be thorough and illustrative and give some insight into how the framework works and potential design that could have been made, but were not for the sake of simplicity and performance

A common misconception a WDFREQUEST handle is the assumption that the WDFREQUEST handle value “follows” (e.g. stays the same) the PIRP around everywhere that the PIRP goes to.  Basically, the idea is that everywhere that the PIRP is sent or presented, the same WDFREQUST handle will be used.  The reality is that the same WDFREQUEST handle value will only be used while the PIRP is owned by the WDFDEVICE for which it was created.  This means that if you send the WDFREQUEST to another driver with a call to WdfRequestSend (e.g. transfer ownership to the driver you are sending it to), the driver which receives the PIRP will have a different handle for the WFDREQUEST.

This means that the WDFREQUEST cannot be used to send extra data or buffers to the driver which will receive the sent request.  For instance, this pattern does not work.  First, in the sending driver (let's say request's handle value is 0xA) you format a request context, format the request and send it to the WDFIOTARGET

 typedef struct _EXTRA_BUFFER {
   PVOID Data;
   ULONG DataLength;
   ...
} EXTRA_BUFFER, *PEXTRA_BUFFER;

PEXTRA_BUFFER pExtra = NULL;
WDF_OBEJCT_ATTRIBUTES woa;
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&woa, EXTRA_BUFFER);
NTSTATUS status;

status = WdfObjectAllocateContext(request, &woa, (PVOID*) &pExtra);

// .. initialize pExtra ...
// ... format request as an internal IOCTL ...

if (WdfRequestSend(request, ...) == FALSE) {
   WdfRequestComplete(request, WdfRequestGetStatus(request));
}

And then in the receiving driver's WDFQUEUE dispatch routine would have a new WDFREQUEST handle value (let’s say 0xAA) and not the sender’s WDFREQUEST (0xA). 

 VOID EvtInternalDeviceIoControl(
    __in WDFQUEUE Queue,
    __in WDFREQUEST Request,
    __in size_t OutputBufferLength,
    __in size_t InputBufferLength,
    __in ULONG IoControlCode
    ) { ... }

Think about how KMDf would have been implemented if the same WDFREQUEST was presented to the lower driver.  One of two things would have had to occur:

  • The first option would have been for the WDFREQUEST handle value to be smuggled somewhere in the PIRP (with the most likely candidate being DriverContext[]), but that would have been unreliable and prone to compatibility issues with non KMDF drivers in the stack.
  • The second option would have been to maintain a global mapping in KMDF that mapped from PIRP pointer value to WDFREQUEST handle.  This would have been very expensive to maintain because we would have had to look up the mapping every time a WDFQUEUE handled PIRP arrived in the framework.  It would have grown more even more expensive because this proposed map would have been guarded by a global lock which meant that all KMDF drivers would have been acquiring and releasing this one lock, making it very contentious and a huge performance problem.

Let’s take a step back and look at why there is a WDFREQUEST handle value when you send a request to another device.  Here is what happens when a PIRP arrives in a KMDF driver

  1. Call the potentially registered WDM preprocess routine
  2. If it is a PIRP that will be processed by a WDFQUEUE (a read/write/IOCTL/internal IOCTL), allocate a WDFREQUEST for the PIRP. If it is not one of these types, pass it to the appropriate pipeline in the framework
  3. Call the potentially registered in process callback
  4. Pass the WDFREQUEST to the WDFQUEUE handler so that it will be presented on the appropriate WDFQUEUE
  5. The WDFQUEUE calls the right IO event callback that you registered when the WDFQUEUE was created

Step #2 is the key here.  The WDFREQUEST is always allocated (from a lookaside if that matters).  It means that there is no 1:1 correspondence between a PIRP and WDFREQUEST value. Let’s say we wanted to have a 1:1 correspondence for a singular WDFDEVICE though(and not across multiple WDFDEVICE as in the first example). We would need a WDFDEVICE based mapping of active PIRPs to WDFREQUEST handles to see if the PIRP is an active PIRP in the WDFDEVICE.  As in the case with the previously proposed global mapping, such a WDFDEVICE wide mapping would also be prohibitively expensive.  While the lock contention would move from a global scope to a WDFDEVICE scope and thus reducing some contention, such a lock would still be a performance hot spot since all PIRPs arriving into the driver would be contending on this lock.

When you look at how KMDF allocates a WDFREQUEST it becomes clear that the WDFRQUEST loosely maps back to the current stack location in the PIRP, not the PIRP itself!  Think about the sending the WDFREQUEST to another driver case.  The sending driver has its own stack location and its own WDFREQUEST. The receiving driver has its own stack location and WDFREQUEST as well.  Just to reinforce this idea, let’s consider a final example.  Let’s say

  • Your driver received a WDFREQUEST formatted for IOCTL ‘A’ in your dispatch routine
  • In processing the request your driver formatted (the next stack location) it for IOCTL ‘B’
  • The request is sent to the top of your stack (which means your driver will see the PIRP in its dispatch routine eventually as IOCTL ‘B’). 

When PIRP enters your driver as IOCTL 'B', it will have a new WDFREQUEST handle!  This is completely by design.  The first stack location (IOCTL ‘A’) has one WDFREQUEST, the subsequent stack location (IOCTL ‘B’) has another WDFREQUEST. 

In conclusion, WDFREQUEST handles are local to a specific WDFDEVICE.  In fact, they are local to a specific stack location.  The context off of a WDFREQUEST can only be used by your driver for that particular WDFDEVICE and is not meant to be shared across WDFDEVICEs.