Programming the Microsoft Windows Driver Model
Mechanics of a Filter Driver
In this section, I ll describe the mechanics of building a filter driver. As I ve said several times, a filter driver is just another kind of WDM driver that has a Driver Entry routine, an AddDevice routine, dispatch functions for PnP and Power IRPs, and so on. The devil, as usual, is in the details.
On the CD The FILTER sample in the companion content illustrates the points discussed in this section.
The DriverEntry Routine
The DriverEntry routine for a filter driver is similar to that for a function driver. The major difference is that a filter driver must install dispatch routines for every type of IRP, not just for the types of IRP it expects to handle:
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { DriverObject->DriverUnload = DriverUnload; DriverObject->DriverExtension->AddDevice = AddDevice; for (int i = 0; i < arraysize(DriverObject->MajorFunction); ++i) DriverObject->MajorFunction[i] = DispatchAny; DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower; DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp; return STATUS_SUCCESS; }
A filter driver has a DriverUnload and an AddDevice function just as any other driver does. I filled the major function table with the address of a routine named DispatchAny that would pass any random request down the stack. I specified dispatch routines for power and PnP requests.
The reason that a filter driver has to handle every conceivable type of IRP has to do with the order in which driver AddDevice functions get called vis- -vis DriverEntry. In general, a filter driver has to support all the same IRP types that the driver immediately underneath it supports. If a filter were to leave a particular MajorFunction table entry in its default state, IRPs of that type would get failed with STATUS_INVALID_DEVICE_REQUEST. (The I/O Manager includes a default dispatch function that simply completes a request with this status. The driver object initially comes to you with all the MajorFunction table entries pointing to that default routine.) But you won t know until AddDevice time which device object or objects are underneath you. You can investigate the dispatch table for each lower device driver inside AddDevice and plug in the needed dispatch pointers in your own MajorFunction table, but remember that you might be in multiple device stacks, so you might get multiple AddDevice calls. It s easier just to declare support for all IRPs at DriverEntry time.
The AddDevice Routine
Filter drivers have AddDevice functions that get called for each appropriate piece of hardware. You ll be calling IoCreateDevice to create an unnamed device object and IoAttachDeviceToDeviceStack to plug in to the driver stack. In addition, you ll need to copy a few settings from the device object underneath you:
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo) { PDEVICE_OBJECT fido; NTSTATUS status = IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), NULL, GetDeviceTypeToUse(pdo), 0, FALSE, &fido); if (!NT_SUCCESS(status)) return status; PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceExtension; do { pdx->DeviceObject = fido; pdx->Pdo = pdo; PDEVICE_OBJECT fdo = IoAttachDeviceToDeviceStack(fido, pdo); pdx->LowerDeviceObject = fdo; fido->Flags = fdo->Flags & (DO_DIRECT_IO DO_BUFFERED_IO DO_POWER_PAGABLE); fido->Flags &= ~DO_DEVICE_INITIALIZING; } while (FALSE); if (!NT_SUCCESS(status)) IoDeleteDevice(fido); return status; }
The parts that are different from a function driver are shown in boldface. Basically, we re using a peculiar method to determine the device type, and we re propagating a few flag bits from the next device object beneath us.
GetDeviceTypeToUse is a local function that determines the device type of the device object immediately under ours. We haven t yet called IoAttachDevice ToDeviceStack, so we don t have our regular LowerDeviceObject pointer. GetDeviceTypeToUse uses IoGetAttachedDeviceReference to get a pointer to the device object that s currently at the top of the stack rooted in our PDO, and it returns that device object s type. The reason we do this in the first place is that if we happen to be filtering a disk storage device object, we must have the correct type code in our call to IoCreateDevice so that the I/O Manager will create an auxiliary data structure known as a Volume Parameters Block (VPB). Without a VPB on every device object in the stack, some Windows 2000 file system drivers might crash later on.
We specify 0 for the device object characteristics. The PnP Manager will propagate any crucial characteristics flags up and down the device stack automatically. It would be wrong for a filter driver to force the FILE_FLAG_SECURE_OPEN flag, which applies to the whole driver stack, except for the purpose of fixing a bug in a function driver that forgets to set this flag.
We copy the buffering flags from the lower device object because the I/O Manager bases some of its decisions on what it sees in the topmost device object. In particular, whether a read or write IRP gets a memory descriptor list or a system buffer depends on what the top object s DO_DIRECT_IO and DO_BUFFERED_IO flags are. The reason a function driver must set one or the other of these flags at AddDevice time and can t change its mind later should now be clear: a filter driver will copy the flags at AddDevice time and won t have any way to know that a lower driver has changed them.
We copy the DO_POWER_PAGABLE flag from the lower device object to satisfy an obscure restriction imposed by the Power Manager. Refer to the sidebar for an explanation of the restriction. We will deal with another aspect of the same problem in our IRP_MJ_PNP dispatch routine. We don t need to propagate the DO_POWER_INRUSH flag because the Power Manager needs that flag set in only one device object.
The DO_POWER_PAGABLE Flag
Drivers must actively manage the DO_POWER_PAGABLE flag to accommodate some quirks in the Windows 2000 Power Manager. If this flag is set in a device object, the Power Manager will send IRP_MN_SET_POWER and IRP_MN_QUERY_POWER requests to the corresponding driver at PASSIVE_LEVEL. If the flag is clear, the Power Manager sends those IRPs at DISPATCH_LEVEL. (IRP_MN_WAIT_WAKE and IRP_MN_POWER_SEQUENCE requests are always sent at PASSIVE_LEVEL.)PoCallDriver acts as a sort of interrupt request level (IRQL) transformer for SET_POWER and QUERY_POWER requests. A driver might forward these IRPs at either PASSIVE_LEVEL or DISPATCH_LEVEL, depending on the IRQL at which the driver itself received the IRP and on whether it s forwarding the IRP as part of an I/O completion routine. If necessary, PoCallDriver will raise the IRQL or schedule a work item to call the next driver at the correct IRQL.
In Windows 2000, however, the Power Manager nevertheless objects (by bugchecking) if it finds a nonpaged device object layered on top of a paged device object when it s building internal lists in preparation for power operations. The Driver Verifier in all systems after and including Windows 2000 also checks for this condition. Because of the rule, you need to make sure at all times that your device object has DO_POWER_PAGABLE set if the driver under you does, and you need to help the driver above you obey the rule too. The first aspect of obeying the rule is to set the flag the same as the lower device object at AddDevice time.
We don t need to copy the SectorSize or AlignmentRequirement members of the lower device object IoAttachDeviceToDeviceStack will do that automatically. We don t need to copy the Characteristics flags because the PnP Manager does that automatically after the device stack is completely built and after it applies overrides from the registry.
There s ordinarily no need for a FiDO to have its own name. If the function driver names its device object and creates a symbolic link, or if the function driver registers a device interface for its device object, an application will be able to open a handle for the device. Every IRP sent to the device gets sent first to the topmost FiDO driver, regardless of whether that FiDO has its own name. Further on in this chapter, I ll discuss how to create an extra named device object to allow applications to access your filter in the middle of a driver stack.
The DispatchAny Function
You write a filter driver in the first place because you want to modify the behavior of a device in some way. Therefore, you ll have dispatch functions that do something with some of the IRPs that come your way. But you ll be passing most of the IRPs down the stack, and you pretty much know how to do this already:
NTSTATUS DispatchAny(PDEVICE_OBJECT fido, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceExtension; NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp); if (!NT_SUCCESS(status)) return CompleteRequest(Irp, status, 0); IoSkipCurrentIrpStackLocation(Irp); status = IoCallDriver(pdx->LowerDeviceObject, Irp); IoReleaseRemoveLock(&pdx->RemoveLock, Irp); return status; }
NOTE
You should follow this guideline when you program a filter driver: First, do no harm. In other words, don t cause drivers above or below you to fail because you perturbed anything at all in their environment or in the flow of IRPs.
DispatchAny uses the remove lock in an attempt to partially fulfill our responsibility to keep the lower driver in memory. As discussed in Chapter 6, however, there s a small hole in the protection we re trying to provide. Our protection of the lower driver will expire as soon as we release the lock at the end of DispatchAny. If the lower driver, or any driver underneath it, returns STATUS_PENDING from the dispatch routine, we re going to release the lock too soon. To provide totally bulletproof protection, we would need to install a completion routine (using IoSetCompletionRoutineEx if it s available) that would release the remove lock.
Installing a completion routine for every IRP in every filter driver isn t an acceptable solution to the early-unload problem, however, because doing so would greatly increase the cost of handling every IRP just to guard against a low-probability race condition. Furthermore, many thousands of filter drivers already in the field don t go to these heroic lengths. Consequently, Microsoft is going to have to find a more general solution to the problem. You could even make the case that a DispatchAny routine in a filter driver might as well not bother with the remove lock at all since it provides only limited protection at some slight cost and since most IRPs that flow through a filter driver are handle based in the first place. As discussed in Chapter 6, handle-based IRPs are inherently safe because Windows XP won t even send an IRP_MN_REMOVE_DEVICE to a device stack while anyone has an open handle.
The DispatchPower Routine
The dispatch routine for IRP_MJ_POWER in a filter driver is straightforward and (nearly) trivial:
NTSTATUS DispatchPower(PDEVICE_OBJECT fido, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceObject; PoStartNextPowerIrp(Irp); NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp); if (!NT_SUCCESS(status)) return CompleteRequest(Irp, status, 0); IoSkipCurrentIrpStackLocation(Irp); status = PoCallDriver(pdx->LowerDeviceObject, Irp); IoReleaseRemoveLock(&pdx->RemoveLock, Irp); return status; }
The only remarkable thing about this routine is that in contrast with every other DispatchPower routine you ve ever seen, this one is actually simple to code.
The DispatchPnp Routine
The dispatch routine for IRP_MJ_PNP in a filter driver has several special cases:
NTSTATUS DispatchPnp(PDEVICE_OBJECT fido, PIRP Irp) { PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); ULONG fcn = stack->MinorFunction; NTSTATUS status; PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fido->DeviceExtension; status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp); if (!NT_SUCCESS(status)) return CompleteRequest(Irp, status, 0); if (fcn == IRP_MN_DEVICE_USAGE_NOTIFICATION) { if (!fido->AttachedDevice (fido->AttachedDevice->Flags & DO_POWER_PAGABLE)) fido->Flags = DO_POWER_PAGABLE; IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE) UsageNotificationCompletionRoutine, (PVOID) pdx, TRUE, TRUE, TRUE); return IoCallDriver(pdx->LowerDeviceObject, Irp); } if (fcn == IRP_MN_START_DEVICE) { IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE) StartDeviceCompletionRoutine, (PVOID) pdx, TRUE, TRUE, TRUE); return IoCallDriver(pdx->LowerDeviceObject, Irp); } if (fcn == IRP_MN_REMOVE_DEVICE) { IoSkipCurrentIrpStackLocation(Irp); status = IoCallDriver(pdx->LowerDeviceObject, Irp); IoReleaseRemoveLockAndWait(&pdx->RemoveLock, Irp); RemoveDevice(fido); return status; } IoSkipCurrentIrpStackLocation(Irp); status = IoCallDriver(pdx->LowerDeviceObject, Irp); IoReleaseRemoveLock(&pdx->RemoveLock, Irp); return status; }
IRP_MN_DEVICE_USAGE_NOTIFICATION
Recall from Chapter 6 that the usage notification informs a function driver that a disk device contains or doesn t contain a paging file, a dump file, or a hibernation file. In response to a usage notification, the function driver might change the setting of its DO_POWER_PAGABLE flag. We may need to alter our setting of that flag in sympathy.
As the IRP travels down the stack, we set DO_POWER_PAGABLE if we re at the top of the PnP stack or if the driver above us has set this flag. It s not normally safe to reference the AttachedDevice field because we don t have access to the internal spin lock that protects the device object stack. The reference is safe in this context, though, because the PnP Manager won t be changing the stack while the usage notification IRP is outstanding.
NOTE
If another driver were to attach to our device stack while a usage notification was traveling down the stack, there is some slight possibility of allowing a nonpaged driver to be layered above a paged driver. Microsoft s own filter drivers don t worry about this possibility, so you and I can probably follow suit.
As the IRP travels back up the stack, our completion routine propagates the flag setting from the lower driver so as to obey the rule that we don t have a nonpaged handler layered on top of a paged handler.
NTSTATUS UsageNotificationCompletionRoutine( PDEVICE_OBJECT fido, PIRP Irp, PDEVICE_EXTENSION pdx) { if (Irp->PendingReturned) IoMarkIrpPending(Irp); if (!(pdx->LowerDeviceObject->Flags & DO_POWER_PAGABLE)) fido->Flags &= ~DO_POWER_PAGABLE; IoReleaseRemoveLock(&pdx->RemoveLock, Irp); return STATUS_SUCCESS; }
For this code to work right, the driver at the top of the stack must set DO_POWER_PAGABLE on the way down so that we ll have set it ourselves in the dispatch routine. We ll leave it set if the lower driver sets it. We ll clear it if the lower driver clears it.
IRP_MN_START_DEVICE
We hook IRP_MN_START_DEVICE so we can propagate the FILE_REMOVABLE_ MEDIA flag. This flag doesn t get set in AddDevice (because the function driver can t talk to the device then) but must be set correctly in the topmost device object of a storage stack. The completion routine is as follows:
NTSTATUS StartDeviceCompletionRoutine(PDEVICE_OBJECT fdo, PIRP Irp, PDEVICE_EXTENSION pdx) { if (Irp->PendingReturned) IoMarkIrpPending(Irp); if (pdx->LowerDeviceObject->Characteristics & FILE_REMOVABLE_MEDIA) fido->Characteristics = FILE_REMOVABLE_MEDIA; IoReleaseRemoveLock(&pdx->RemoveLock, Irp); return STATUS_SUCCESS; }
IRP_MN_REMOVE_DEVICE
We handle IRP_MN_REMOVE_DEVICE specially because this is where we do the RemoveDevice processing that calls IoDetachDevice and IoDeleteDevice. The reason this is so much simpler than in a function driver is that we don t have queues to abort or I/O resources to release.