Completion Routines

Completion Routines

You often need to know the results of I/O requests that you pass down to lower levels of the driver hierarchy or that you originate. To find out what happened to a request, you install a completion routine by calling IoSetCompletionRoutine:

IoSetCompletionRoutine(Irp, CompletionRoutine, context, InvokeOnSuccess, InvokeOnError, InvokeOnCancel);

Irp is the request whose completion you want to know about. CompletionRoutine is the address of the completion routine you want called, and context is an arbitrary pointer-size value you want passed as an argument to the completion routine. The InvokeOnXxx arguments are Boolean values indicating whether you want the completion routine called in three different circumstances:

  • InvokeOnSuccess means you want the completion routine called when somebody completes the IRP with a status code that passes the NT_SUCCESS test.

  • InvokeOnError means you want the completion routine called when somebody completes the IRP with a status code that does not pass the NT_SUCCESS test.

  • InvokeOnCancel means you want the completion routine called when somebody calls IoCancelIrp before completing the IRP. I worded this quite delicately: IoCancelIrp will set the Cancel flag in the IRP, and that s the condition that gets tested if you specify this argument. A cancelled IRP might end up being completed with STATUS_CANCELLED (which would cause the NT_SUCCESS test to fail) or with any other status at all. If the IRP gets completed with an error and you specified InvokeOnError, InvokeOnError by itself will cause your completion routine to be called. Conversely, if the IRP gets completed without error and you specified InvokeOnSuccess, InvokeOnSuccess by itself will cause your completion routine to be called. In these cases, InvokeOnCancel will be redundant. But if you left out one or the other (or both) of InvokeOnSuccess or InvokeOnError, the InvokeOnCancel flag will let you see the eventual completion of an IRP whose Cancel flag has been set, no matter which status is used for the completion.

At least one of these three flags must be TRUE. Note that IoSetCompletionRoutine is a macro, so you want to avoid arguments that generate side effects. The three flag arguments and the function pointer, in particular, are each referenced twice by the macro.

IoSetCompletionRoutine installs the completion routine address and context argument in the nextIO_STACK_LOCATION that is, in the stack location in which the next lower driver will find its parameters. Consequently, the lowest-level driver in a particular stack of drivers doesn t dare attempt to install a completion routine. Doing so would be pretty futile, of course, because by definition of lowest-level driver there s no driver left to pass the request on to.

CAUTION
Recall that you are responsible for initializing the next I/O stack location before you call IoCallDriver. Do this initialization before you install a completion routine. This step is especially important if you use IoCopyCurrentIrpStackLocationToNext to initialize the next stack location because that function clears some flags that IoSetCompletionRoutine sets.

A completion routine looks like this:

NTSTATUS CompletionRoutine(PDEVICE_OBJECT fdo, PIRP Irp, PVOID context) {  return <some status code>; }

It receives pointers to the device object and the IRP, and it also receives whichever context value you specified in the call to IoSetCompletionRoutine. Completion routines can be called at DISPATCH_LEVEL in an arbitrary thread context but can also be called at PASSIVE_LEVEL or APC_LEVEL. To accommodate the worst case (DISPATCH_LEVEL), completion routines therefore need to be in nonpaged memory and must call only service functions that are callable at or below DISPATCH_LEVEL. To accommodate the possibility of being called at a lower IRQL, however, a completion routine shouldn t call functions such as KeAcquireSpinLockAtDpcLevel that assume they re at DISPATCH_LEVEL to start with.

There are really just two possible return values from a completion routine:

  • STATUS_MORE_PROCESSING_REQUIRED, which aborts the completion process immediately. The spelling of this status code obscures its actual purpose, which is to short-circuit the completion of an IRP. Sometimes, a driver actually does some additional processing on the same IRP. Other times, the flag just means, Yo, IoCompleteRequest! Like, don t touch this IRP no more, dude! Future versions of the DDK will therefore define an enumeration constant, StopCompletion, that is numerically the same as STATUS_MORE_PROCESSING_REQUIRED but more evocatively named. (Future printings of this book may also employ better grammar in describing the meaning to be ascribed the constant, at least if my editors get their way.)

  • Anything else, which allows the completion process to continue. Because any value besides STATUS_MORE_PROCESSING_REQUIRED has the same meaning as any other, I usually just code STATUS_SUCCESS. Future versions of the DDK will define STATUS_CONTINUE_COMPLETION and an enumeration constant, Con tinueCompletion, that are numerically the same as STATUS_SUCCESS.

I ll have more to say about these return codes a bit further on in this chapter.

NOTE
The device object pointer argument to a completion routine is the value left in the I/O stack location s DeviceObject pointer. IoCall Driver ordinarily sets this value. People sometimes create an IRP with an extra stack location so that they can pass parameters to a completion routine without creating an extra context structure. Such a completion routine gets a NULL device object pointer unless the creator sets the DeviceObject field.

How Completion Routines Get Called

IoCompleteRequest is responsible for calling all of the completion routines that drivers installed in their respective stack locations. The way the process works, as shown in the flowchart in Figure 5-7, is this: Somebody calls IoCompleteRequest to signal the end of processing for the IRP. IoCompleteRequest then consults the current stack location to see whether the driver above the current level installed a completion routine. If not, it moves the stack pointer up one level and repeats the test. This process repeats until a stack location is found that does specify a completion routine or until IoCompleteRequest reaches the top of the stack. Then IoCompleteRequest takes steps that eventually result in somebody releasing the memory occupied by the IRP (among other things).

When IoCompleteRequest finds a stack frame with a completion routine pointer, it calls that routine and examines the return code. If the return code is anything other than STATUS_MORE_PROCESSING_REQUIRED, IoComplete Request moves the stack pointer up one level and continues as before. If the return code is STATUS_MORE_PROCESSING_REQUIRED, however, IoComplete Request stops dead in its tracks and returns to its caller. The IRP will then be in a sort of limbo state. The driver whose completion routine halted the stack unwinding process is expected to do more work with the IRP and call IoCompleteRequest to resume the completion process.

figure 5-7 logic of iocompleterequest.

Figure 5-7. Logic of IoCompleteRequest.

Within a completion routine, a call to IoGetCurrentIrpStackLocation will retrieve the same stack pointer that was current when somebody called IoSetCompletionRoutine. You shouldn t rely in a completion routine on the contents of any lower stack location. To reinforce this rule, IoCompleteRequest zeroes most of the next location just before calling a completion routine.

Actual Question from Who Wants to Be a Gazillionaire Driver Tycoon:

Suppose you install a completion routine and then immediately call IoCompleteRequest. What do you suppose happens?
  1. Your computer implodes, creating a gravitational singularity into which the universe instantaneously collapses.

  2. You receive the blue screen of death because you re supposed to know better than to install a completion routine in this situation.

  3. IoCompleteRequest calls your completion routine. Unless the completion routine returns STATUS_MORE_PROCESSING_REQUIRED, IoCompleteRequest then completes the IRP normally.

  4. IoCompleteRequest doesn t call your completion routine. It completes the IRP normally.

    figure

The Problem of IoMarkIrpPending

Completion routines have one more detail to attend to. You can learn this the easy way or the hard way, as they say in the movies. First the easy way just follow this rule:

Execute the following code in any completion routine that does not return STATUS_MORE_PROCESSING_REQUIRED :

if (Irp->PendingReturned) IoMarkIrpPending(Irp);

Now we ll explore the hard way to learn about IoMarkIrpPending. Some I/O Manager routines manage an IRP with code that functions much as does this example:

KEVENT event; IO_STATUS_BLOCK iosb; KeInitializeEvent(&event, ...); PIRP Irp = IoBuildDeviceIoControlRequest(..., &event, &iosb); NTSTATUS status = IoCallDriver(SomeDeviceObject, Irp); if (status == STATUS_PENDING) { KeWaitForSingleObject(&event, ...); status = iosb.Status; } else <cleanup IRP>

The key here is that, if the returned status is STATUS_PENDING, the entity that creates this IRP will wait on the event that was specified in the call to IoBuildDeviceIoControlRequest. This discussion could also be about an IRP built by IoBuildSynchronousFsdRequest too the important factor is the conditional wait on the event.

So who, you might well wonder, signals that event? IoCompleteRequest does this signaling indirectly by scheduling an APC to the same routine that performs the <cleanup IRP> step in the preceding pseudocode. That cleanup code will do many tasks, including calling IoFreeIrp to release the IRP and KeSetEvent to set the event on which the creator might be waiting. For some types of IRP, IoCompleteRequest will always schedule the APC. For other types of IRP, though, IoCompleteRequest will schedule the APC only if the SL_PENDING_RETURNED flag is set in the topmost stack location. You don t need to know which types of IRP fall into these two categories because Microsoft might change the way this function works and invalidate the deductions you might make if you knew. You do need to know, though, that IoMark Pending is a macro whose only purpose is to set SL_PENDING_RETURNED in the current stack location. Thus, if the dispatch routine in the topmost driver on the stack does this:

NTSTATUS TopDriverDispatchSomething(PDEVICE_OBJECT fdo, PIRP Irp) { IoMarkIrpPending(Irp);  return STATUS_PENDING; }

things will work out nicely. (I m violating my naming convention here to emphasize where this dispatch function lives.) Because this dispatch routine returns STATUS_PENDING, the originator of the IRP will call KeWaitForSingleObject. Because the dispatch routine sets the SL_PENDING_RETURNED flag, IoCompleteRequest will know to set the event on which the originator waits.

But suppose the topmost driver merely passed the request down the stack, and the second driver pended the IRP:

NTSTATUS TopDriverDispatchSomething(PDEVICE_OBJECT fido, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceExtension; IoCopyCurrentIrpStackLocationToNext(Irp); return IoCallDriver(pdx->LowerDeviceObject, Irp); } NTSTATUS SecondDriverDispatchSomething(PDEVICE_OBJECT fdo, PIRP Irp) { IoMarkIrpPending(Irp);  return STATUS_PENDING; }

Apparently, the second driver s stack location contains the SL_PENDING_RETURNED flag, but the first driver s does not. IoCompleteRequest anticipates this situation, however, by propagating the SL_PENDING_RETURNED flag whenever it unwinds a stack location that doesn t have a completion routine associated with it. Because the top driver didn t install a completion routine, therefore, IoCompleteRequest will have set the flag in the topmost location, and it will have caused the completion event to be signaled.

In another scenario, the topmost driver uses IoSkipCurrentIrpStackLocation instead of IoCopyCurrentIrpStackLocationToNext. Here, everything works out by default. This is because the IoMarkIrpPending call in SecondDriverDispatchSomething sets the flag in the topmost stack location to begin with.

Things get sticky if the topmost driver installs a completion routine:

NTSTATUS TopDriverDispatchSomething(PDEVICE_OBJECT fido, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceExtension; IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp, TopDriverCompletionRoutine, ...); return IoCallDriver(pdx->LowerDeviceObject, Irp); } NTSTATUS SecondDriverDispatchSomething(PDEVICE_OBJECT fdo, PIRP Irp) { IoMarkIrpPending(Irp);  return STATUS_PENDING; }

Here IoCompleteRequest won t propagate SL_PENDING_RETURNED into the topmost stack location. I m not exactly sure why the Windows NT designers decided not to do this propagation, but it s a fact that they did so decide. Instead, just before calling the completion routine, IoCompleteRequest sets the PendingReturned flag in the IRP to whichever value SL_PENDING_RETURNED had in the immediately lower stack location. The completion routine must then take over the job of setting SL_PENDING_RETURNED in its own location:

NTSTATUS TopDriverCompletionRoutine(PDEVICE_OBJECT fido, PIRP Irp, ...) { if (Irp->PendingReturned) IoMarkIrpPending(Irp);  return STATUS_SUCCESS; }

If you omit this step, you ll find that threads deadlock waiting for someone to signal an event that s destined never to be signaled. So don t omit this step.

Given the importance of the call to IoMarkIrpPending, driver programmers through the ages have tried to find other ways of dealing with the problem. Here is a smattering of bad ideas.

Bad Idea # 1 Conditionally Call IoMarkIrpPending in the Dispatch Routine

The first bad idea is to try to deal with the pending flag solely in the dispatch routine, thereby keeping the completion routine pristine and understandable in some vague way:

NTSTATUS TopDriverDispatchSomething(PDEVICE_OBJECT fido, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceExtension; IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp, TopDriverCompletionRoutine, ...); NTSTATUS status = IoCallDriver(pdx->LowerDeviceObject, Irp); if (status == STATUS_PENDING) IoMarkIrpPending(Irp); // <== Argh! Don't do this! return status; }

The reason this is a bad idea is that the IRP might already be complete, and someone might already have called IoFreeIrp, by the time IoCallDriver returns. You must treat the pointer as poison as soon as you give it away to a function that might complete the IRP.

Bad idea # 2 Always Call IoMarkIrpPending in the Dispatch Routine

Here the dispatch routine unconditionally calls IoMarkIrpPending and then returns whichever value IoCallDriver returns:

NTSTATUS TopDriverDispatchSomething(PDEVICE_OBJECT fido, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceExtension; IoMarkIrpPending(Irp); // <== Don't do this either! IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp, TopDriverCompletionRoutine, ...); return IoCallDriver(pdx->LowerDeviceObject, Irp); }

This is a bad idea if the next driver happens to complete the IRP in its dispatch routine and returns a nonpending status. In this situation, IoCompleteRequest will cause all the completion cleanup to happen. When you return a nonpending status, the I/O Manager routine that originated the IRP might call the same completion cleanup routine a second time. This leads to a double-completion bug check.

Remember always to pair the call to IoMarkIrpPending with returning STATUS_PENDING. That is, do both or neither, but never one without the other.

Bad Idea # 3 Call IoMarkPending Regardless of the Return Code from the Completion Routine

In this example, the programmer forgot the qualification of the rule about when to make the call to IoMarkIrpPending from a completion routine:

NTSTATUS TopDriverDispatchSomething(PDEVICE_OBJECT fido, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceExtension; KEVENT event; KeInitializeEvent(&event, NotificationEvent, FALSE); IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp, TopDriverCompletionRoutine, &event, TRUE, TRUE, TRUE); IoCallDriver(pdx->LowerDeviceObject, Irp); KeWaitForSingleObject(&event, ...);  Irp->IoStatus.Status = status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; } NTSTATUS TopDriverCompletionRoutine(PDEVICE_OBJECT fido, PIRP Irp, PVOID pev) { if (Irp->PendingReturned) IoMarkIrpPending(Irp); // <== oops KeSetEvent((PKEVENT) pev, IO_NO_INCREMENT, FALSE); return STATUS_MORE_PROCESSING_REQUIRED; }

What s probably going on here is that the programmer wants to forward the IRP synchronously and then resume processing the IRP after the lower driver finishes with it. (See IRP-handling scenario 7 at the end of this chapter.) That s how you re supposed to handle certain PnP IRPs, in fact. This example can cause a double-completion bug check, though, if the lower driver happens to return STATUS_PENDING. This is actually the same scenario as in the previous bad idea: your dispatch routine is returning a nonpending status, but your stack frame has the pending flag set. People often get away with this bad idea, which existed in the IRP_MJ_PNP handlers of many early Windows 2000 DDK samples, because no one ever posts a Plug and Play IRP. (Therefore, PendingReturned is never set, and the incorrect call to IoMarkIrpPending never happens.)

A variation on this idea occurs when you create an asynchronous IRP of some kind. You re supposed to provide a completion routine to free the IRP, and you ll necessarily return STATUS_MORE_PROCESSING_REQUIRED from that completion routine to prevent IoCompleteRequest from attempting to do any more work on an IRP that has disappeared:

SOMETYPE SomeFunction() { PIRP Irp = IoBuildAsynchronousFsdRequest(...); IoSetCompletionRoutine(Irp, MyCompletionRoutine, ...); IoCallDriver(...); } NTSTATUS MyCompletionRoutine(PDEVICE_OBJECT junk, PIRP Irp, PVOID context) { if (Irp->PendingReturned) IoMarkIrpPending(Irp); // <== oops! IoFreeIrp(Irp); return STATUS_MORE_PROCESSING_REQUIRED; }

The problem here is that there is no current stack location inside this completion routine! Consequently, IoMarkIrpPending modifies a random piece of storage. Besides, it s fundamentally silly to worry about setting a flag that IoCompleteRequest will never inspect: you re returning STATUS_MORE_PRO CESSING_REQUIRED, which is going to cause IoCompleteRequest to immediately return to its own caller without doing another single thing with your IRP.

Avoid both of these problems by remembering not to call IoMarkIrpPending from a completion routine that returns STATUS_MORE_PRO CESSING_REQUIRED.

Bad Idea # 4 Always Pend the IRP

Here the programmer gives up trying to understand and just always pends the IRP. This strategy avoids needing to do anything special in the completion routine.

NTSTATUS TopDriverDispatchSomething(PDEVICE_OBJECT fido, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceExtension; IoMarkIrpPending(Irp); IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp, TopDriverCompletionRoutine, ...); IoCallDriver(pdx->LowerDeviceObject, Irp); return STATUS_PENDING; } NTSTATUS TopDriverCompletionRoutine(PDEVICE_OBJECT fido, PIRP Irp, ...) {  return STATUS_SUCCESS; }

This strategy isn t so much bad as inefficient. If SL_PENDING_RETURNED is set in the topmost stack location, IoCompleteRequest schedules a special kernel APC to do the work in the context of the originating thread. Generally speaking, if a dispatch routine posts an IRP, the IRP will end up being completed in some other thread. An APC is needed to get back into the original context in order to do some buffer copying. But scheduling an APC is relatively expensive, and it would be nice to avoid the overhead if you re still in the original thread. Thus, if your dispatch routine doesn t actually return STATUS_PENDING, you shouldn t mark your stack frame pending.

But nothing really awful will happen if you implement this bad idea, in the sense that the system will keep working normally. Note also that Microsoft might someday change the way completion cleanup happens, so don t write your driver on the assumption that an APC is always going to occur.

A Plug and Play Complication

The PnP Manager might conceivably decide to unload your driver before one of your completion routines has a chance to return to the I/O Manager. Anyone who sends you an IRP is supposed to prevent this unhappy occurrence by making sure you can t be unloaded until you ve finished handling that IRP. When you create an IRP, however, you have to protect yourself. Part of the protection involves a so-called remove lock object, discussed in Chapter 6, which gates PnP removal until drivers under you finish handling all outstanding IRPs. Another part of the protection is the following function, available in XP and later releases of Windows:

IoSetCompletionRoutineEx(DeviceObject, Irp, CompletionRoutine, context, InvokeOnSuccess, InvokeOnError, InvokeOnCancel);

NOTE
The DDK documentation for IoSetCompletionRoutineEx suggests that it s useful only for non-PnP drivers. As discussed here, however, on many occasions a PnP driver might need to use this function to achieve full protection from early unloading.

The DeviceObject parameter is a pointer to your own device object. IoSetCompletionRoutineEx takes an extra reference to this object just before calling your completion routine, and it releases the reference when your completion routine returns. The extra reference pins the device object and, more important, your driver, in memory. But because this function doesn t exist in Windows versions prior to XP, you need to consider carefully whether you want to go to the trouble of calling MmGetSystemRoutineAddress (and loading a Windows 98/Me implementation of the same function) to dynamically link to this routine if it happens to be available. It seems to me that there are five discrete situations to consider:

Situation 1: Synchronous Subsidiary IRP

The first situation to consider occurs when you create a synchronous IRP to help you process an IRP that someone else has sent you. You intend to complete the main IRP after the subsidiary IRP completes.

You wouldn t ordinarily use a completion routine with a synchronous IRP, but you might want to if you were going to implement the safe cancel logic discussed later in this chapter. If you follow that example, your completion routine will safely return before you completely finish handling the subsidiary IRP and, therefore, comfortably before you complete the main IRP. The sender of the main IRP is keeping you in memory until then. Consequently, you won t need to use IoSetCompletionRoutineEx.

Situation 2: Asynchronous Subsidiary IRP

In this situation, you use an asynchronous subsidiary IRP to help you implement a main IRP that someone sends you. You complete the main IRP in the completion routine that you re obliged to install for the subsidiary IRP.

Here you should use IoSetCompletionRoutineEx if it s available because the main IRP sender s protection expires as soon as you complete the main IRP. Your completion routine still has to return to the I/O Manager and therefore needs the protection offered by this new routine.

Situation 3: IRP Issued from Your Own System Thread

The third situation in our analysis of completion routines occurs when a system thread you ve created (see Chapter 14 for a discussion of system threads) installs completion routines for IRPs it sends to other drivers. If you create a truly asynchronous IRP in this situation, use IoSetCompletionRoutineEx to install the obligatory completion routine and make sure that your driver can t unload before the completion routine is actually called. You could, for example, claim an IO_REMOVE_LOCK that you release in the completion routine. If you use scenario 8 from the cookbook at the end of this chapter to send a nominally asynchronous IRP in a synchronous way, however, or if you use synchronous IRPs in the first place, there s no particular reason to use IoSetCompletionRoutineEx because you ll presumably wait for these IRPs to finish before calling PsTerminateSystemThread to end the thread. Some other function in your driver will be waiting for the thread to terminate before allowing the operating system to finally unload your driver. This combination of protections makes it safe to use an ordinary completion routine.

Situation 4: IRP Issued from a Work Item

Here I hope you ll be using IoAllocateWorkItem and IoQueueWorkItem, which protect your driver from being unloaded until the work item callback routine returns. As in the previous situation, you ll want to use IoSetCompletionRoutineEx if you issue an asynchronous IRP and don t wait (as in scenario 8) for it to finish. Otherwise, you don t need the new routine unless you somehow return before the IRP completes, which would be against all the rules for IRP handling and not just the rules for completion routines.

Situation 5: Synchronous or Asynchronous IRP for Some Other Purpose

Maybe you have some reason for issuing a synchronous IRP that is not in aid of an IRP that someone else has sent you and is not issued from the context of your own system thread or a work item. I confess that I can t think of a circumstance in which you d actually want to do this, but I think you d basically be toast if you tried. Protecting your completion routine, if any, probably helps a bit, but there s no bulletproof way for you to guarantee that you ll still be there when IoCallDriver returns. If you think of a way, you ll simply move the problem to after you do whatever it is you think of, at which point there has to be at least a return instruction that will get executed without protection from outside your driver.

So don t do this.



Programming the Microsoft Windows Driver Model
Programming the Microsoft Windows Driver Model
ISBN: 0735618038
EAN: 2147483647
Year: 2003
Pages: 119
Authors: Walter Oney

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net