OSRLogo
OSRLogoOSRLogoOSRLogo x Seminar Ad
OSRLogo
x

Everything Windows Driver Development

x
x
x
GoToHomePage xLoginx
 
 

    Thu, 14 Mar 2019     118020 members

   Login
   Join


 
 
Contents
  Online Dump Analyzer
OSR Dev Blog
The NT Insider
The Basics
File Systems
Downloads
ListServer / Forum
  Express Links
  · The NT Insider Digital Edition - May-June 2016 Now Available!
  · Windows 8.1 Update: VS Express Now Supported
  · HCK Client install on Windows N versions
  · There's a WDFSTRING?
  · When CAN You Call WdfIoQueueP...ously

How to Get There from Here -- Redirecting Create Requests

 Click Here to Download: Code Associated With This Article Zip Archive, 2MB

In the last issue of The NT Insider, we talked about how the Object Manager parsed names in the article Meandering through the Object Manager.   Here, we'll use the knowledge that we gained from that article in order to understand and implement a driver that can redirect create requests from one Device Object to another Device Object.

The Problem

Let's consider the following problem: A company builds hardware encryption processing devices, and wants a solution where there can be one or more of these devices plugged into a system at any given time.  The number of encryption processors installed is determined by the user; they can install one encryption processor if their needs are modest, or they can install several processors if they need lots of encryption "horsepower." To make this work, requests for encryption services arriving at our encryption driver must be automatically distributed among the installed processors in a company-defined manner.

How can we solve the problem?  I can think of three potential solutions. 

In the first solution, we create a user dynamic link library (DLL) that would register for PnP Notification and be alerted whenever our devices were inserted or removed.  This DLL would then use the "SetupDi" interface to enumerate the devices and distribute requests for encryption services among the available encryption processors in a round-robin fashion.  The only problem with this is that the device interfaces or symbolic links that the driver would have to create would make these devices visible to the users.  While this might not be a big deal, it may not be something that your company would want to expose.

In the second solution, we create two drivers that would provide essentially the same functionality as the user DLL, but do it within the kernel.  One driver would have an interface exposed to user mode and handle the redirecting of the requests, and the other driver would handle the encryption devices.  This is probably the most flexible approach, but it does require the extra overhead of writing and maintaining two drivers. 

In the third solution, we provide all the functionality that our two-driver solution would provide, but we do it in one driver.  This is the solution that we will be discussing and working with in this article.

The Issues Involved

In order to make this solution work, there are a few issues that we need to address. Here's what we need:

  • A way to be notified whenever a new encryption device is inserted or removed
  • To make sure that each device has a unique name in the Object Manager's name space so that the Object Manager can find it when parsing an input name
  • To understand all the issues surrounding the I/O Manager's handling of an IRP_MJ_CREATE that completes with STATUS_REPARSE 

Let's discuss each of these issues in turn.

Issue 1--Notification

"What is so tough about this?" you might ask.  It seems to have a simple solution, and it does.   Since we are writing the PnP driver to handle the encryption devices, we will be notified via our ADD_DEVICE handler whenever a new device is added, and we will be notified via our IRP_MN_REMOVE handler of devices being removed.    It's typical Plug and Play handling and you should be very comfortable with this approach.  This is the solution we will be using, since we are implementing a one-driver solution.

Issue 2--Naming

The way we'll handle the job of distributing requests for encryption services among the encryption processor devices is by using STATUS_REPARSE.  (We'll describe how this works in the next section.)  For now, all that you need to understand is that we need two sets of device object names. The first--which will be the master device object name to which all user requests are directed--will be something like "\DosDevices\Encryption" or a device interface that we register with IoRegisterDeviceInterface.  The second set of names will contain one for each encryption processor device.

As we are going to be redirecting IRP_MJ_CREATE requests to the encryption processor device objects, each device object requires a unique name in the Object Manager name space. Fortunately this doesn't involve rocket science--all we have to do is create a monotonically-increasing count to append to the device name for our encryption processor devices.  Viola! Instantly unique.  In addition, after creating this unique name, it must somehow be relayed to the driver (via the mechanisms discussed in Issue 1) so that the Object Manager can redirect IRP_MJ_CREATE requests to that named device object.  We'll talk about this later.

Issue 3--STATUS_REPARSE

How do we get the I/O Manager to take an IRP_MJ_CREATE request that was targeted to one device object and get it to send that request to a different device object?   This is where it's important to understand STATUS_REPARSE.   STATUS_REPARSE is a special status that the I/O Manager looks for (1) in the IRPs IoStatus.Status field during I/O completion processing and (2) in IopParseDevice after it has called a driver's IRP_MJ_CREATE handler.  

As you may recall from Meandering Through the Object Manager, when the Object Manager gets a name, it begins parsing it component by component to determine which object is the target of the create request.   So our device object must be selected to receive the original IRP_MJ_CREATE request because the name in the file object was resolved down to our device object in the name space.  

Using STATUS_REPARSE, we can cause the Object Manager to select a different device by replacing the original name in the input file object (irpSp->FileObject->Name) with the name of that different device object.  All it takes to do this is:

  • Change the Unicode name that's in the received IRP_MJ_CREATE FileObject Name field
  • Complete the IRP with the special status STATUS_REPARSE placed in IoStatus.Status
  • Fill the IRP's IoStatus.Information field with the value IO_REPARSE

These operations are depicted in Figure 1.

When the I/O Manager sees the special status STATUS_REPARSE and IO_REPARSE in the information field, it begins examining the name contained within the file object from the beginning.  If the parsing of that (changed) name results in the Object Manager finding a device object with that name then the IRP_MJ_CREATE request will be delivered to that device.   It sounds pretty simple, and thankfully, it is.

One item that we should discuss in more detail is the value that your driver must put into the IRPs IoStatus.Information field.  There are actually two values for this field when returning STATUS_REPARSE: IO_REPARSE and IO_REMOUNT.  If an I/O request is completed with STATUS_REPARSE, and IO_REMOUNT is in the Information field of the I/O status block, the I/O Manager will try to remount the media volume associated with the request. If the remount operation fails, the volume is dismounted.   On the other hand, when an I/O request is completed with STATUS_REPARSE and the value IO_REPARSE is in the   I/O status block's Information field, this tells the I/O Manager that the name within the file object needs to be reparsed from the beginning.  This is exactly what we want to do to make our solution work.

Thus, whenever we receive a create request from a user application to our master device object, we will:

  • Change the name in the file object of the create request to the name of one of the encryption device objects that was created for our encryption devices
  • Return the status STATUS_REPARSE
  • Use the information value IO_REPARSE

Now let's look at some implementation details.

Implementation Details--Design Issues

In order to implement our solution there are a number of design issues that we need to discuss before proceeding.

Multiple Device Objects

If you haven't figured it out already, the one-driver approach is interesting in that our driver will have to create multiple device objects that provide differing functionality.  One device object will represent the user visible interface; we will call it the Master Device Object (MDO), and it is responsible for redirecting IRP_MJ_CREATE requests.  The other device object will be the Functional Device Object (FDO) for each encryption device that our driver is servicing.  Because our driver is going to be dealing with device objects which represent different functionality, our driver has an issue to confront.  How do we distinguish an IRP_MJ_CREATE request for the MDO from creates for the FDOs?

While some developers may elect to give each of their device objects a different device type when calling IoCreateDevice, we have elected to store the distinguishing information in each device object's device extension.  When we laid out the structure definition for each device extension, we made sure that each structure defined started with the same COMMON_EXT, named Common.  The layout of this structure is as follows:

typedef struct _COMMON_EXT
{
//
// Set to TRUE if this is the Redirector device
// object.   FALSE if an Encryption device
// object.
BOOLEAN      Redirector;
 
//
// Address of Driver Extension used for storing
// global Information.
//
PREDIRECT_DRIVER_OBJECT_EXT DriverObjectExt;
 
} COMMON_EXT; *PCOMMON_EXT;

By having each structure start with the same field, our dispatch routines can easily determine what type of device object is involved in the request and handle it appropriately.    We have seen approaches where each structure starts with a dispatch table that contains the appropriate handler for that type of device object, and the dispatch handler simply jumps through that table for processing.   Whatever floats your boat.

Since our driver is going to be creating device objects, we need to determine when and where we are going to create them. We know that our FDOs for the encryption devices are going to be created in our registered ADD_DEVICE handler, because that is the callback that the Plug and Play Manager uses to notify us of arriving devices.  This is standard PnP programming.   What might not be so clear is where and when to create our MDO.  We actually have two options here; we could create the MDO in our DriverEntry routine or we could elect to create it when our first FDO has been created in ADD_DEVICE.  We?ve elected to do it in our ADD_ DEVICE handler since our product cannot offer encryption services unless an encryption device is present.

Knowing Which Devices Are Present

In order to redirect a request to another device, our MDO must have a way to determine which encryption devices are present and able to handle requests.   You may think that all you have to do is loop through your device object's NextDevice pointer. However, on a PnP system where your encryption devices could come and go at any time, this solution would be deadly because you don't control access to this list.  Therefore, we need a safer and more reliable method; in other words, we must create a list to which we can synchronize access.   What we have elected to do is to have each FDO register itself with the MDO whenever it wants to be in the list of devices to handle encryption requests (via a routine called RegisterWithRedirect)--as shown in Figure 2--and also have it deregister itself when it is unavailable (via UnregisterWithRedirect).

 /////////////////////////////////////////////////////////////////////
//
//  RegisterWithRedirect
//
//    This routine registers the input device with the Controller. 
//    The Controller
//    links the device into the list of registered devices.
//
//  INPUTS:
//    DevExt - Address of the device extension for the registering
//    device.
// 
//  OUTPUTS:
//      None.
//
//  RETURNS:
//      None.
//
//  IRQL:
//    This routine is called at IRQL PASSIVE_LEVEL.
//
//  NOTES:
//    If the device was previously registered, it is not added to
//    the list.
//
///////////////////////////////////////////////////////////////////
void RegisterWithRedirect(PENCRYPT_DEVICE_EXT DevExt)
{
    PREDIRECT_CTL_EXT ctlExt = (PREDIRECT_CTL_EXT)                 
    DevExt->Common.DriverObjectExt
                      ->RedirectorDeviceObject->DeviceExtension;
    //
    //  Acquire the List Mutex.
    //
    KeEnterCriticalRegion();
    ExAcquireFastMutex(&ctlExt->DeviceNameListLock);
    //
    // Make sure that the device is not registered already.  If it 
    // is, get out of here.
    //
    if(DevExt->Registered) {
        ExReleaseFastMutex(&ctlExt->DeviceNameListLock);
        KeLeaveCriticalRegion();
        return;
    }
    //
    // Add the new entry to the end of the list.  
    //
    InsertTailList(&ctlExt->DeviceNameList,
           &DevExt->RegisteredListEntry);
    //
    // Set the registered flag in the Devices Extension.
    //
    DevExt->Registered = TRUE;
    //
    // Increment the count of registered devices.  
    //
    ctlExt->DeviceNameListCount++;
    //
    // Release the List Mutex.
    //
    ExReleaseFastMutex(&ctlExt->DeviceNameListLock);
    KeLeaveCriticalRegion();
}

Figure 2 --  RegisterWithRedirect

Let's discuss RegisterWithRedirect in more detail.   The first thing that this routine does is acquire the Mutex that protects access to the MDO's list of registered FDOs (i.e., encryption devices).  Once this is acquired, the routine checks the Registered flag in the registering FDO's device extension.  If it is TRUE then this device was already registered, so we release the Mutex and exit.   Otherwise, we link this FDO into our MDO's list of registered encryption devices via the RegisterListEntry field in the FDO's device extension, and set the RegisterFlag to TRUE.  We also increment the MDO's count of registered encryption devices before releasing the Mutex that controls the list.   Now, whenever the MDO receives an IRP_MJ_CREATE request, it can redirect that request to an encryption device that has been added to our list.

Implementation Details--Redirecting Requests

Now that we have the basics out of the way, it's time to discuss the details of redirecting an IRP_MJ_CREATE request from one device object to another.   This is done via the routine RedirectCreateRequest, as shown in Figure 3.  This routine is called whenever an IRP_MJ_CREATE request is received by the MDO device object and driver redirection is enabled (we added an IOCTL to the driver which allows redirection to be enabled or disabled).  As you will notice, this routine acquires the Mutex that protects the list of encryption devices that are registered with the MDO (see Figure 2 if you forgot how a FDO registered with the MDO).  The list is actually threaded through each FDO's device extension (ENCRYPT_DEVICE_EXT) RegisteredListEntry field.  The ENCRYPT_DEVICE_EXT structure has (1) a flag named Register that indicates whether or not the FDO was previously registered, (2) the RegisterListEntry field for linking registered encryption devices, and (3) the unique device name that was used when ADD_DEVICE called IoCreateDevice to create the FDO.  Why do we include the name? Well, mostly for ease.  When the FDO is the next target for a redirected IRP_MJ_CREATE request, we simply replace the name in the current IRP stack location's file object (currentIrpStack->FileObject) with the unique name in the device object.


/////////////////////////////////////////////////////////////////////

//
//  RedirectCreateRequest
//    This routine is responsible for redirecting create request to a registered device based on a round robin algorithm. 
//
//  INPUTS:
//    CtlExt - Address of the controller device extension.
//    Irp - Address of Create Irp to redirect.
// 
//  OUTPUTS:
//    None.
//
//  RETURNS:
//    STATUS_REPARSE if the IRP is to be redirected, otherwise another status is returned.
//
//  IRQL:
//    This routine is called at IRQL PASSIVE_LEVEL.
//
//  NOTES:
//    If no devices were registered, the Irp cannot be redirected.
//
/////////////////////////////////////////////////////////////////////
NTSTATUS RedirectCreateRequest(PREDIRECT_CTL_EXT CtlExt,PIRP Irp)
{
    ULONG index = 0;
    PLIST_ENTRY pEntry;
    PENCRYPT_DEVICE_EXT devExt = NULL;
    PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp);
    PWCHAR pBuffer = NULL;
 
    //
    // Wait for the List Mutex.
    //
    KeEnterCriticalRegion();
    ExAcquireFastMutex(&CtlExt->DeviceNameListLock);
 
    //
    // See if there are any devices contained with in list.
    //
    if(IsListEmpty(&CtlExt->DeviceNameList)) {
 
        //
        // No devices were registered, so we must return an error.
        //
        ExReleaseFastMutex(&CtlExt->DeviceNameListLock);
        KeLeaveCriticalRegion();
        return STATUS_INSUFFICIENT_RESOURCES;
    }
 
    //
    // Determine if our next device index exceeds the number of entries in the list.  If so, we reset the index back to 0.
    //
    if(CtlExt->DeviceNameListIndex >= CtlExt->DeviceNameListCount) {
 
        
        CtlExt->DeviceNameListIndex = 0;
    }
    
    //
    // Find the entry in the list to process.   Essentially we just count entries until we hit our selected index.
    //
    pEntry = CtlExt->DeviceNameList.Flink;
    while(index < CtlExt->DeviceNameListIndex) {
        pEntry = pEntry->Flink;
        index++;
    }
 
    //
    // Increment the index so that the next time this routine is called we pick the next entry in the list.
    //
    CtlExt->DeviceNameListIndex++;
 
    //
    // Calculate the beginning of the device extension 
    //
    devExt = (PENCRYPT_DEVICE_EXT) 
    CONTAINING_RECORD(pEntry,ENCRYPT_DEVICE_EXT,RegisteredListEntry);
 
    //
    // Allocate a paged pool buffer to hold the name of the device that we are redirecting the create to.
    //
    pBuffer = (PWCHAR) 
    ExAllocatePoolWithTag(PagedPool,devExt->DeviceName.Length,'mnDR');
 
 
    //
    // Make sure that we have a buffer.  If not we cannot redirect the request.
    //
 
 
    if(!pBuffer) {
        //
        // No buffer allocated, so we have to return an error.
        //
        ExReleaseFastMutex(&CtlExt->DeviceNameListLock);
        KeLeaveCriticalRegion();
        return STATUS_INSUFFICIENT_RESOURCES;
    }
 
    //
    // Copy the device name to the allocated buffer
    //
    RtlCopyMemory(pBuffer,devExt->DeviceName.Buffer,
                  devExt->DeviceName.Length);
 
    //
    // Fix up the UNICODE_STRING fields in the FILE_OBJECT for the new name.
    //
    ioStack->FileObject->FileName.Length = devExt->DeviceName.Length;
    ioStack->FileObject->FileName.MaximumLength = 
                         devExt->DeviceName.Length;
 
    //
    // Delete the buffer used in the FILE_OBJECT file name if it exists.
    //
    if(ioStack->FileObject->FileName.Buffer) {
        ExFreePool(ioStack->FileObject->FileName.Buffer);
    }
 
    //
    // Put our allocated buffer in the FILE_OBJECT File Name.  This now contains the name of the device that this create is going
    // to be redirected to.
    //
    ioStack->FileObject->FileName.Buffer = pBuffer;
 
    DbgPrint("RedirectCreateRequest to %wZ\n",&devExt->DeviceName);
 
    //
    // Tell the IO Manager that the name needs to be reparsed.
    //
    Irp->IoStatus.Status = STATUS_REPARSE;
    Irp->IoStatus.Information = IO_REPARSE;
      
    //
    // Release our mutex.
    //
    ExReleaseFastMutex(&CtlExt->DeviceNameListLock);
    KeLeaveCriticalRegion();
 
    return STATUS_REPARSE;
} 

Figure 3 --  RedirectCreateRequest

Replacing the name is quite simple.  As you know, the name associated with the file object is stored in the FileName field.  This field is a UNICODE_STRING structure which contains the Length, MaximumLength and pointer to the Buffer  (which is allocated from paged pool).  To replace the name, we need to allocate a new paged pool buffer big enough to hold the name contained within our FDO extension DeviceName field. We then copy our name to it and ensure that the Length and MaximumLength fields are set correctly.  Lastly, we need to delete the buffer pointer currently contained in the FileName.Buffer field of the file object, if it exists.  If it exists, you ask? 

As mentioned in our previous article, Meandering through the Object Manager, the file object--after parsing by the Object Manager--contains the unparsed portion of the name that may represent a name space supported by your driver.  If it does, then in addition to allocating memory for the name contained in the FDO extension DeviceName field, we must also allocate additional memory so that we can append the unparsed portion of the name.  Here's an example:   Suppose that you were implementing a driver like Microsoft's Distributed File System (DFS), and your driver was called to handle an IRP_MJ_CREATE  to open Z:\BOB\MARK.TXT and Z: was the name of your device object.  The file object passed to your IRP_MJ_CREATE dispatch routine would contain the name "\BOB\MARK.TXT," which is the unparsed portion of the name.   If you now wanted to redirect this create request to "\Device\Fred," you would need to replace the name in the file object with the name "\Device\Fred\BOB\ MARK.TXT."  Note that this functionality is not demonstrated in our code in Figure 2, because our driver does not support its own name space.

Finally, after replacing the name in the file object, we can set the IRP IoStatus.Status field to STATUS_REPARSE, set the IoStatus.Information field to IO_REPARSE, call IoCompleteRequest on the IRP, and return STATUS_REPARSE from our IRP_MJ_CREATE dispatch handler.  By the way, if the device object passed to our IRP_MJ_CREATE dispatch routine is not the MDO device object, then it must be one of our FDO device objects, and we will process it as necessary for our encryption device.

Cleaning up the Loose Ends

As mentioned earlier, our driver must be able to distinguish between the two types of device objects it services, namely the MDO and FDO.  Therefore, almost all of our dispatch routines must be coded to look at the type of device object received and process it accordingly.   Remember that the only valid IRP major function that our MDO can receive is IRP_MJ_CREATE.  Because our IRP_MJ_CREATE handler never returns STATUS_SUCCESS for the IRP_MJ_CREATE request, the MDO is never opened, and thus the user never receives a handle for a file object that is associated with the MDO device object.

Now, we did say that almost all of our dispatch routines must be coded to look at the type of device object received; almost all.  The MDO was not created as the result of a physical device coming online; it was created as a standalone device object and is not attached to a PDO.  This means that the IRP_MJ_PNP and IRP_MJ_POWER dispatch handlers will never receive an IRP targeted for the MDO.

Another loose end to tie up is how to delete the MDO when there are no more encryption devices to service.  As we all know from PnP, when a device is removed from the system, the PnP Manager calls the device's IRP_MN_REMOVE handler to process the remove.  As the devices to be removed registered with the MDO by calling RegisterWithRedirect, these devices must call UnregisterWithRedirect to unregister with the MDO.  In designing the driver, we conveniently added a reference count in the MDO for the number of registered devices.  So, when the count of registered devices decrements to zero, in UnregisterWithRedirect, the code can delete the MDO.

Summary

As you can see, redirecting an IRP_MJ_CREATE request is not really a difficult problem to solve once you understand how the Object Manager parses names and how the I/O Manager handles STATUS_REPARSE.   This article should give you some new programming techniques to add to your repertoire.

User Comments
Rate this article and give us feedback. Do you find anything missing? Share your opinion with the community!
Post Your Comment

Post Your Comments.
Print this article.
Email this article.
bottom nav links