Thursday, February 11, 2010

Issuing IO in minifilters: Part 2 – Flt vs. Zw

Sorry about the frequency of my posts, i’m been really swamped with the IFS Plugfest preparations.

Anyway, let’s get down to business. So now the way the create path works should be clear. The basic idea is that FltMgr has some targeting information (in which it stores the minifilters below which altitude should see the operation). In the create path the information is stored in an ECP (or, pre-Vista, an EA).

After the create operation has completed successfully, the targeting information is taken from the ECP and moved to a FILE_OBJECT context (private to filter manager). This allows filter manager to always figure out where IO to a file object should be targeted. This is very important. It means that once a filter opens a file with FltCreateFile, filter manager will always target the IO properly, at the minifilter below the one that created the file.

Another piece of the puzzle that I should mention is that IO manager uses a similar mechanism to the one filter manager uses to remember the device that was specified as a hint when a FILE_OBJECT was created. This information is used in IoGetRelatedDeviceObject to figure out which device the IO should be targeted at.

Finally, I need to address one very important concept: where a minifilter is sitting on the IO path. There are a couple of cases that are best described by example:

  1. A minifilter, being a device driver after all, can register for some notifications such as IoRegisterShutdownNotification or PsSetLoadImageNotifyRoutine. When these callbacks are called, the minifilter is not on the IO path at all. It should behave like any other driver in the system. So if it needs to do stuff like write something into a log it should issue IO to the top of the stack, using APIs like ZwCreateFile, ZwWriteFile and so on.
  2. Expanding on the example above, a minifilter can filter some volume (like the system volume) and write something (like a log) to another volume. In this case, while the minifilter is on the IO path on the volume it filters, it is not on the IO path for the volume it wants to write to. So, like in the case above, it might issue IO to the top of the stack on that other volume using APIs like ZwCreateFile, ZwWriteFile. This is good because it doesn’t require the minifilter to attach an instance to that volume. The two device stacks are pretty much unrelated.
  3. However, there are cases where a minifilter wants to write to the same volume. As we’ve already established that reentering the IO stack at the top is evil and the wrongdoers should be sent to Azkaban, the minifilter writer has no choice but to issue the IO below itself on the same stack. This is where the minifilter has two options:
    1. Use the user’s FILE_OBJECT. I use the term “user’s FILE_OBJECT” to refer to the FILE_OBJECT in FltObjects->FileObject, which might in fact be a FILE_OBJECT created by a filter above, not necessarily coming from the user. Anyway, the only way to do this is to call APIs that take an instance, so that filter manager can target it properly: FltWriteFile, FltQueryDirectoryFile and so on. Alternatively, feel free to allocate a CALLBACK_DATA using FltAllocateCallbackData, set up the parameters and then call FltPerformSynchronousIo. Filter manager will show the IO to the minifilters below that instance and then, if none of them complete the IO, it will allocate an IRP and send the request below. IO manager does not get involved in this at all (i mean, filter manager will call IoAllocateIrp and IoCallDriver, but the targeting logic in IO manager will not be used).
    2. Call FltCreateFile and get its own FILE_OBJECT. Once this create completes successfully, the minifilter can use Flt APIs or Zw APIs. The reason Zw APIs work well is because IO manager will use IoGetRelatedDeviceObject which will send the IO directly to filter manager’s device and then filter manager will find the targeting information associated with the FILE_OBJECT and will send it to the proper instance.
  4. Finally, there can be a case where a minifilter might decide to issue IO below its instance on a different volume (so the minifilter is injecting IO into the IO path on a different volume). Since using the user’s FILE_OBJECT is clearly not an option, it will need to create a new FILE_OBJECT for that second volume. However, to target below its instance it needs to call FltCreateFile (which takes an instance). However, from that point on, it can call either the Zw APIs or the Flt APIs on that file object and the operations will be targeted correctly.

Of course, there are many ways to implement something in a minifilter and both the IO model and filter manager allow for a lot of flexibility. So the list below should be treated more as a guideline and not like strict rules. Finally, here are the guidelines:

  1. Anytime you find yourself trying to call an API that requires an instance and you don’t have one, don’t try to hack something up. You’re probably not supposed to call that API from in that context.
  2. If you want to use the user’s FILE_OBJECT, you must use send the IO below yourself and you must use Flt APIs. If there isn’t one you can build your own with FltAllocateCallbackData and FltPerformSynchronousIo. However, using Zw APIs in this context will cause reentrancy.
  3. If you have your own FILE_OBJECT (meaning it was created with FltCreateFile), you can use either Flt APIs or Zw APIs. You don’t have to stick with one set, you can mix & match, depending on which provides more value.
  4. If you want to issue IO on a different volume, you can either use ZwCreateFile or FltCreateFile, depending on your design (an important factor in the decision is whether you have an instance on that volume). However, once the FILE_OBJECT and handle are created, the logic in the rule above applies.
  5. If you are completely outside the IO stack in some cases, you might be better off sending IO to the top of the stack (in those cases). What I have in mind here is a minifilter that does a bunch of other stuff like registering for PsSetLoadImageNotifyRoutine.
  6. If you need to issue IO to a different volume and you don’t have an instance to call FltCreateFile, it’s probably not a good idea to attach to that volume just to have an instance to pass into FltCreateFile. ZwCreateFile might be what you need. This is not a rule, you can create an instance just to inject IO in a different stack, but think hard about it before you do. It might not be necessary.

I hope this makes sense, but feel free to post any questions.