OSRLogo
OSRLogoOSRLogoOSRLogo x Subscribe to The NT Insider
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

Kernel: Calling User Mode - Using the Inverted Call Model

 Click Here to Download: Code Associated With This Article Zip Archive, 65KB

Driver writers often ask whether or not a device driver can call a user-mode function.  While the simple answer to this is that it isn’t possible, the inverted call model provides a mechanism that can be used to achieve similar functionality.  Fortunately, this model is not only supported in the Windows OS architecture, it is used by existing operating system components!

 

The basic design of such a system consists of a service, a driver, and some protocol between the two.  It works with monolithic drivers as well as layered drivers and only requires that the driver be able to receive device control operations (or their equivalent if you are using one of the driver library models).

 

 Figure 1 — Simple Inverted Call Model

 

Of course, exactly what these operations do depends upon the particular needs of your driver, but the basic mechanism needed is comparable:

 

  • A queuing mechanism for requests that are awaiting responses from the service;
  • A queuing mechanism for service threads awaiting requests to process;
  • A protocol for describing the request to the service and matching up the service response with the original request; and
  • Clean-up mechanism in case the request is cancelled or the service terminates prematurely

 

The first part of this process relies upon one of the existing queue mechanisms, whether it is a linked list, a kernel queue object, or some other driver-invented process.  The second requires a means of signaling between the service and the driver, whether it is using enqueued IRPs, dispatcher objects, completion ports, or some other driver developer favored mechanism.

 

To demonstrate this technique we wrote what started out as a “simple” example.  While the resulting example is not quite so simple it does demonstrate this basic technique and should allow developers to implement their own inverted-call mechanisms.

 

In developing this example we focused on three key components:

 

  • The “application” that reads and writes to the device;
  • The “control service” that satisfies the inverted calls from the kernel driver; and
  • The kernel driver that posts requests from the application to the control service and joins up the original request with the control service response

 

The example is a data sink/data source example, where data is written from one (or more) applications and the control service links up the write and read operations.  A few notes about this example:

 

  • All code paths run at IRQL < DISPATCH_LEVEL;
  • Data structures are stored in paged pool;
  • No special queueing structure is used

 

The example itself would not be a particularly useful driver since the service it provides is already available from the OS (and in far more efficient mechanisms).  However, since the goal was to demonstrate an asynchronous inverted call implementation, our emphasis was on the implementation and not on the functionality of the driver.  In the balance of this article we will describe the basic structure of the driver.  The full source code for the example will be posted on the OSR web site.

 

Driver Entry

 

We chose to implement this driver using the legacy driver model.  There is nothing inherent about this driver that requires it be a legacy driver, but because it is a software-only device it was a simpler model and appropriate for this example.

 

Thus, as a legacy driver the DriverEntry function is responsible for creating the device objects that will be used by the driver.  In our example we create two separate device objects, one is the control object, the other the data object.  The control object is configured in such a way that it may only be opened once (“exclusive open”) while the data object allows concurrent read/write access to it.

 

Each object type has a different device extension and device type.  The driver distinguishes between the two devices based upon their device type and maintains two global points to each one.  Having created the two devices and initialized the device extensions of each, the driver sets up its entry points and then returns success to the I/O Manager.

 

The key data structures here are the device extensions.  For the control device, there are two queues – one queue that represents the list of waiting control threads (if any) and the other queue that represents the list of data requests awaiting indication to the control application.  Thus, this is the heart of the inverted call model implementation.

 

For the data device, there are also two queues – one representing read operations, the other write operations.  In each of these cases, the queue is used to track data operations for which the driver is awaiting requests from the control application.  This implementation supports the “fully asynchronous” model common to Windows XP drivers but is not the only way to achieve this functionality – an alternative would be to block the requesting thread and perform the operation synchronously.  Indeed, such a solution would be simpler from an implementation standpoint, and is one of the key reasons the example became somewhat complicated.

 

Create

 

The function of the create entry point is mostly trivial – it enables access to the device.  One point to note here is that having the control application open the control device causes activation of the data device, as without the control application the data operations are pointless.

 

Cleanup

 

The cleanup function is unusual for a typical device driver but is a convenient way for a driver using the inverted call model to clean up any outstanding operations.  The issue here is not the pending control IRPs, which could have been dispatched using standard cancel routines, but rather the pending data IRPs that will not receive responses from the now defunct control operation.

 

One advantage of the cleanup processing is that any pending IRPs for the control device will be effectively cancelled, obviating the need for separate cancellation routines.

 

Read/Write

 

The read and write function has no meaning for the control application as shown in Figure 2.

 

  if (OSR_COMM_CONTROL_TYPE == DeviceObject->DeviceType) {

 

    //

    // Control device does not support read operations

    //

    status = STATUS_INVALID_DEVICE_REQUEST;

 

  }

 

Figure 2

 

Thus much of the processing for read and write is associated with the data device.  The example must handle the various states in which the driver might find itself operating.  For example, it might be the case that the control application is not running.  In that case the request to read or write via the data object is rejected with an error (STATUS_INVALID_ DEVICE_REQUEST).

 

If the device is active then we enqueue the IRP unconditionally. We know this works because the I/O subsystem is, by its very nature, inherently asynchronous (a driver can return STATUS_PENDING for any operation).  Of course, returning STATUS_PENDING for read or write is common.

 

Once enqueued, the driver must determine if a control thread is available.  If one is available, the request must be dispatched to the control thread.  If a thread is not available, the request must be enqueued to the control object indicating it is awaiting a control thread.

 

Thus, each data request is in a queue appropriate to the type of operation (a read or write request queue).  If the data request is awaiting dispatch to the control application, it must also be enqueued on a separate queue for the control object.  Since each queue is protected by its own lock, we must make certain that we define the lock order and always acquire the locks in the same order to prevent potential deadlocks from arising.

 

Thus, being present in one of the data request queues indicates that the data request is awaiting an answer from the control application.  Being present in the control request queue indicates that the data request is awaiting dispatch to the control application!

 

Since this is a data sink/source example, there is additional data movement handling which isn’t terribly important to the example.  The code for dispatching to the control application can be quite simple, as it is in the read operation case shown in Figure 3.

 

controlRequest->RequestType = OSR_COMM_READ_REQUEST;

 

          controlRequest->RequestBuffer = Irp->AssociatedIrp.SystemBuffer;

         

          //

          // Note that length is in the same location for both read and write

          //

          controlRequest->RequestBufferLength =

IoGetCurrentIrpStackLocation(Irp)->Parameters.Read.Length;

         

          //

          // And complete the control request

          //

          controlIrp->IoStatus.Status = STATUS_SUCCESS;

         

          controlIrp->IoStatus.Information = sizeof(OSR_COMM_CONTROL_REQUEST);

         

          IoCompleteRequest(controlIrp, IO_NO_INCREMENT);

 

Figure 3 – Dispatching to the Control Application (Read)

 

The write operation case is a bit more complicated because of the data movement.

 

DeviceControl

 

The device control operation is of primary interest to the control application because the control application uses it to send and receive requests from the kernel driver.  The control application and kernel driver support three separate control operations:

 

  • Get a new data request; or
  • Respond to a data request; or
  • Respond to a data request then get a new data request

 

The last option is the one that would typically be used by a control application in order to perpetuate execution and handling of requests.  The driver thus has two distinct operations it handles dispatching a response to a waiting data request and dispatching a queued data request to the control application.

 

The sample code implements this by using two helper functions – ProcessResponse and ProcessControlRequest.  The first routine (ProcessResponse) determines the correct data request queue to traverse (either read or write) and then walks through the queue looking for an entry with a matching request ID.  If it finds one, it satisfies the data request and then completes the original data request operation.  If it does not find one, no action is taken.  This might happen, for example, if a data request were timed out or canceled (although we do not do either in the example code).

 

The second routine (ProcessControlRequest) must first look to see if there are any data requests awaiting dispatch to the control application.  If there are none waiting, it enqueues the control IRP.  Note that careful locking here is important – otherwise it is possible for entries to be inserted into one queue or the other in such a fashion that a request becomes “orphaned”, which will lead to improper behavior!  The example code is shown in Figure 4.

 

  //

  // First, we need to lock the control queue before we do anything else

  //

  ExAcquireFastMutex(&controlExt->ServiceQueueLock);

 

  ExAcquireFastMutex(&controlExt->RequestQueueLock);

 

  //

  // Check request queue

  //

  if (!IsListEmpty(&controlExt->RequestQueue)) {

 

    listEntry = RemoveHeadList(&controlExt->RequestQueue);

 

    status = STATUS_SUCCESS;

 

  } else {

 

    //

    // We have to insert the control IRP into the queue

    //

    IoMarkIrpPending(Irp);

 

    InsertTailList(&controlExt->ServiceQueue, &Irp->Tail.Overlay.ListEntry);

 

    status = STATUS_PENDING;

 

  }

 

  //

  // OK.  At this point we can drop both locks

  //

  ExReleaseFastMutex(&controlExt->RequestQueueLock);

 

  ExReleaseFastMutex(&controlExt->ServiceQueueLock);

 

Figure 4 – ProcessControlRequest

 

Thus, the goal here is to ensure that nothing is inserted into the service queue at the same time we are inserting the new control IRP into the control request queue!

 

If there is a waiting data request, it is removed from the queue, the control request structure is initialized, and the control request IRP is completed.  The data request IRP is not completed yet because it must still await the answer from the control application!

 

Conclusion

 

The sample code demonstrates a read/write data source/sink model.  However, this model will work for essentially arbitrary operations by the control application.  It can be expanded to provide additional services – DNS name translation, for instance.  The advantage of this model is that it allows drivers to utilize the full range of services easily available to user mode applications in a manner that is completely compatible with the operating system’s structure.

 

Fair warning however: the risk of the inverted call model is that it becomes easier to introduce “cycles” or deadlocks between user mode components and kernel mode components.  For example, if the “application” in question is some generic OS component (e.g., the Win32 process) and the control service attempts to utilize the same generic OS component (such as a Win32 API call) then it is possible that a deadlock will result.  While such deadlocks are not common, they can – and do – occur!

 

Despite this warning, the inverted call model is still a powerful technique, and one that driver writers should maintain as part of their programming arsenal.

 

 

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

"Usefull, but lacks pinpoints"
The article is very usefull but lacks in pointing out main principles of data exchange between userland and kerneland. Keeps eyes of the ball. But still it is good to have it.

Rating:
18-Mar-11, Elf Gordon


"Bug in OsrCommReadWrite"
In OsrCommReadWrite, if there are unexpected errors, the routine will complete the IRP with an error code. For example, //// mdl = IoAllocateMdl(controlRequest->RequestBuffer...); if(NULL == mdl) { status = STATUS_INSUFFICIENT_RESOURCES; // ^ ExReleaseFastMutex(&controlExt->ServiceQueueLock); Irp->IoStatus.Status = status; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); } ////

However, the same IRP has been inserted into either the Read or Write queue earlier. This "zombie" IRP will cause a MULTIPLE_IRP_COMPLETE_REQUESTS bugcheck when the cleanup routine tries to cancel it. Need to scan and remove the IRP from the R/W queue before calling IoComplete.

Rating:
05-Nov-03, Nemo Mai


"There seems to be a bug in CancelPendingRequestList"
For control queues the data should be extracted by using "ServiceListEntry" not "ListEntry". Otherwise, the extracted dataRequest->Irp is not a valid IRP. Canceling the IRP will result in a bug check. // Extract the data request from the list entry dataRequest = CONTAINING_RECORD(listEntry, OSR_COMM_DATA_REQUEST, ListEntry); <---

Here is the output from WinDbg // kernel mode OSRCOMM: OsrCommReadWrite: insert listEntry (E19F92C0) with irp (E19F92CC) to request queue (81D47FA4)

// User mode service app exiting here thread: Prepare to exit thread thread: Cancel all pending io thread: Terminated app: Quit. //

// kernel mode OSRCOMM: OsrCommCleanup: Cleanup request queue OSRCOMM: list entry: E19F92C0, invalid dataRequest->Irp (E19F92D4)

Rating:
05-Nov-03, Nemo Mai


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