The Windows NT operating system has always included the ability for one device driver to “filter” the Device Objects created by another driver. While the actual filtering mechanism has changed very little since NT was first introduced, the advent of Windows 2000 and WDM has greatly changed the architecture of device filtering. In this article, we’ll explore some of the most important changes in device filters in Windows 2000. We’ll start by describing what device filter drivers are, and briefly describe how they were implemented in NT V4. Next, we’ll talk about Win2K, and how filters for device drivers get installed and instantiated. Then we’ll discuss the difference between upper and lower filters, and why those lower filters aren’t nearly as useless as they sound when you first hear about them.
We’re going to stick to discussing how to filter devices. So, what we will explicitly not discuss in this article is how file system filtering is accomplished in Windows 2000. We’ll leave that topic for a whole different article, perhaps even a series of them.
Filtering Basics - Before Win2K
Back in the days of Windows NT V4, device driver filtering was a pretty straightforward process. A driver created some Device Objects and gave each one a name. If another driver wanted to intercept all the I/O requests that went to that first device driver it needed to “filter” the Device Objects created by the first driver. To do this, the filtering driver would create a Device Object of its own for each Device Object to be filtered. The filtering driver than “attached” the Device Object that it created to the Device Object to be filtered. This is typically done by calling IoAttachDevice(), which takes as arguments the name of the Device Object to be filtered and a pointer to the Device Object that will be used to perform the filtering.
It’s really only confusing to write about. As an example, consider an NT V4 filter driver that has the job of counting the writes that are sent to SCSI disk devices. This driver decides to filter the SCSI port device to carry out its mission. The filter driver “knows” the SCSI port driver creates Device Objects named “ScsiPort0”, “ScsiPort1”, etc. So, in order to intercept all the I/O requests sent to the SCSI adapter (so he can determine if the request represents a write is to a disk unit, and count it if it is), the filter driver creates one (usually unnamed) Device Object of his own for each Device Object the SCSI port driver creates. The filter driver then attaches one of the Device Objects it created to each of the SCSI Port Device Objects. Thus, to filter the I/O requests destined for the device “ScsiPort0”, a filter driver would create his own, unnamed, Device Object, and attach it to the Device Object named “ScsiPort0” as follows:
NULL, // Pointer to name
Pretty easy, right? There are some disadvantages to this scheme, however. The most obvious disadvantage is that the filter driver has to have predefined knowledge of the names of the Device Objects it wants to filter. Suppose you need to filter all the Device Objects that represent SCSI Devices in the system. And, suppose one of your customers installs some OEM adapters that use a (non-miniport) SCSI port driver that names its first Device Object “Joe_Bloggs_Slightly_Used_SCSI_Adapters_Pty_Ltd_Port_A”?
The second complexity of the NT V4 scheme is that you must carefully control the load-order of the filter driver. The filter driver must only be started after the driver being filtered. Otherwise, when the filter driver starts, the Device Objects it wants to filter won’t yet exist, and the call to IoAttachDevice() will fail.
Well, if Windows 2000 didn’t completely replace NT’s I/O subsystem, it at least shook its foundations. Devices can arrive and leave at almost any time. Device Objects names are no longer pre-defined and static. Lots of new port devices exist. And there are plenty of changes that affect device filter drivers.
I’m not going to describe too much detail about how devices get enumerated, or drivers get loaded and initialized. I’ve talked about this a bunch of times in previous articles. I’ll assume you already know the basics of how device drivers on Win2K work. If you don’t, take our “Writing Kernel Mode Drivers For Windows 2000/WDM” seminar and I’ll tell you all about it then.
Having said that, check out Figure 1, as you recall the arrangement of the Win2K device stack. Some bus driver (let’s say the PCI driver for the sake of convenience) finds a SCSI adapter on the bus, and creates a Physical Device Object (PDO) that represents that adapter. The PCI bus driver reports the creation of this PDO to the PnP Manager. As a result, the PnP Manager loads the SCSI adapter driver. The SCSI adapter driver is then called at its AddDevice() entry point where it is passed a pointer to the PDO created by the bus driver. The SCSI adapter driver then creates his Functional Device Object (FDO) and (still in its AddDevice() entry point) attaches that FDO to the underlying PDO.
Figure 1 – Win2K Device Stack
Next, the SCSI adapter driver acts like a bus driver (because it is, in fact, a bus driver – they don’t call it a SCSI bus for nothin’) and enumerates the devices on the SCSI bus. For each device the SCSI adapter driver finds on the bus, it creates a PDO, and reports the existence of the PDOs it creates to the PnP Manager.
As a result of being informed about the creation of these new PDOs, the PnP Manager loads the SCSI disk class driver. The SCSI disk class driver is called it at its AddDevice() entry point once for each PDO that represents a disk device (these PDOs having been created by the SCSI adapter driver to represent devices on its bus). Within its AddDevice() routine, the SCSI disk class driver creates an FDO that represents the functional instance of a disk device, and attaches that FDO to the underlying PDO passed to it.
The process continues, as the disk class driver acts as a bus driver and enumerates disk partitions for each disk, etc. And so it goes, on up the stack.
Eventually, once the stack is fully set-up, somebody will issue a disk request. The IRP that represents this request will be passed-down the device stack from driver to driver. Typically (though not always) the device to which a higher-level driver passes its IRPs is the underlying (PDO) device to which its FDO is attached. For example, the disk class driver will be called with an IRP at one of its dispatch entry points. It will process this IRP by reading parameters from the current I/O stack location in the IRP, setting up the next I/O stack location, and passing the IRP down to a specific SCSI disk PDO.
Back To Filtering
The good news (if you can call it that) is that device filter drivers in Win2K get loaded, initialized, and started almost exactly the same way as any other Win2K device driver. When the driver is first loaded, its DriverEntry() entry point is called. At this point the filter driver does the typical driver-wide initialization activities, like exporting its other entry point by filling in its Driver Object. When a device is found to filter, the filter driver’s AddDevice() entry point is called with a pointer to a Device Object below it. So, where a typical hardware driver would be passed a pointer to an underlying PDO at its AddDevice() entry point, a filter driver is a passed a pointer to an underlying device to filter. When the device being filtered gets its resources, the filter driver gets an IRP_MJ_PNP, IRP_MN_START_DEVICE IRP.
The rest of a filter device’s operations are similarly straight-forward. When the filter driver is called with an IRP, it does whatever it wants with or to that IRP – Just as it did in NT V4 – and then passes it on (assuming it wants to) to the underlying driver.
To continue our example of the filter driver that counts writes to SCSI disk devices, this filter driver would determine if any request it receives is a write operation (ignoring, for the moment, whether or not the request is being sent to a disk), if the request is for a write the driver counts it, sets up the next IRP stack location and then passes the request on to the driver below it using IoCallDriver(). This is not rocket science we’re talking about here. It’s all pretty mundane stuff.
There is one concept about Win2K device filtering has confused more than one experienced NT driver writer, however. In Win2K, a filter driver can be either an upper filter or a lower filter. An upper filter receives requests before the device it filters receives them. A lower filter receives requests after the device being filters receives them, assuming the filtered device passes them down.
The concept of a lower filter has occasioned more than a few guffaws from NT V4 cognoscenti: “Gee, given a device stack like ‘DeviceA, DeviceB, DeviceC’ if I want to filter all the IRPs going to DeviceC, I wonder if I should write an upper filter of DeviceC or a lower filter of DeviceB? Maybe we should have a design review!” Guffaw twitter giggle. “As if it makes any difference!” Nudge nudge, wink wink, aren’t we smart, say no more!
Well, lads, I’m afraid you’ve just stuffed your Nike’s into your mouth about up to the knee. The only problem with the above snicker-soaked soliloquy is that it’s totally wrong, as a result of being NT V4 centric. It doesn’t take into account the Win2K driver model, and the difference between FDOs and PDOs. So, while the speaker may be an NT V4 god, it’s pretty clear that he/she doesn’t really understand how Win2K works.
Filters are specified in Win2K in relation to FDOs, not in relation to PDOs. Thus (check out Figure 1 again) it is almost never the case that the lower filter of one device, and the upper filter of the device the immediately follows it in the device stack, are equivalent.
Let’s return once again to the example of wanting to filter all the SCSI disk devices. To accomplish this, we would want our driver to be a lower filter of the disk class driver. That places our Device Objects directly below the FDOs created by the disk class driver and above the PDOs created by the SCSI adapter driver for any disk type devices. At this location, we will receive any I/O requests destined for SCSI disk type devices, after the disk class driver has processed those requests. We will not receive requests for other SCSI device types (CD-ROM, tape, or the like). Also, note that being a lower filter of the disk class driver most decidedly does not make our device an upper filter of the SCSI adapter driver.
I’ve found that the best way (well, for me it’s also the only way) to figure out where you want your filter driver to load is to sit down and carefully diagram the part of the device stack you’re filtering. Use squares for the PDOs and circles for the FDOs, or whatever other distinctive geometric shapes turn you on. Check your concept of how the device stack looks against several real systems (OSR’s newly re-written and highly revised DeviceTree utility, available for free download from the OSR web site, actually shows the device enumeration tree and the relationship of PDOs and FDOs in the system – There’s nothing better with which to check out your ideas). When you’re sure you know how the PDOs and FDOs stack up, you’ll know precisely where you want your filter installed.
Getting it Installed
Filter driver installation is another major differences between NT V4 and Win2K. After you’ve created the usual “service” entry in the registry (HKLM\System\CCS\Services\<your-driver-name-here>), you’ll need to add your driver’s name to the UpperFilters or LowerFilters value for the device you want to filter.
In Win2K, you must choose whether you want to filter one or more specific instances of a device, or an entire class of devices. This determines where you make your UpperFilters or LowerFilters Registry entry. To filter an entire class of device, which includes those devices presently installed and those which may be installed in the future by the user, place the name of your driver in a REG_MULTI_SZ value named UpperFilters or LowerFilters under the Registry key:
Where “<class-guid>” is the setup class of the device you want to filter. For example, continuing our example again, to be a lower filter of any disk devices, you would create a REG_MULTI_SZ value named LowerFilters set equal to your driver name, and place that entry in:
This is done programmatically using the Registry API. In fact, check out the above key in any Win2K system. You’ll see that there are already two drivers listed as UpperFilters for this class. These are PartMgr (the Partition Manager) and DiskPerf (the Disk Performance Filter).
Alternatively, you may choose to filter only specific device instances. To do this, put the name of your driver in a REG_MULTI_SZ value named UpperFilters or LowerFilters under the key:
This is done programmatically using the function SetupDiSetDeviceRegistryProperty() for property SPDRP_UPPERFILTERS or SPDRP_LOWERFILTERS. The DDK program AddFilter adds filters for specific device instances (only). Check out that program for an example of how to use the (lovely) SetupDiXxxx interface. (You’ll find it in \ntddk\src\storage\filters\addfilter).
Filter Exploration and Caveats
So, that’s the skinny on device filters in Win2K. If you want to do more filter exploration, and possibly add filters interactively to your system for debugging or testing, check out our FilterMan utility, available for free download from the OSR web site.
As for caveats, whenever you’re writing a filter driver, make certain that you pay special attention to the issue of I/O completion, especially if you utilize a completion routine in your IRPs. Remember the all-important “if (Irp->PendingReturned) IoMarkIrpPending();” If you don’t know what I’m talking about, check out The NT Insider back issues for the article on I/O Completion for a refresher.