Thursday, November 17, 2011

Controlling the Load Order of File System Filters

In this post I'd like to talk about the factors that contribute to the loading order of file system filters. Of course, if all the filters on a system are minifilters then the load order is completely determined by their altitudes. But as it happens there are still some legacy filters out there and so one does occasionally have to deal with order inversions.
Before we go any further I'd like to add some links to some documentation that describes this. For minifilters altitudes there is the Load Order Groups and Altitudes for Minifilter Drivers MSDN page. For how the load order of regular drivers is calculated there is KB article 115486 on How To Control Device Driver Load Order. Also, for a refresher on how file system attach in general I'll refer you to my blog (Part1 and Part2) and to my post on the FLTP_FRAME structure.
It is useful to quickly go over how legacy file system filters typically attach:
  1. When the driver is loaded it calls IoRegisterFsRegistrationChange() to find the file system control device objects (CDOs).
  2. Then the driver attaches to each CDO and enumerates any existing volumes (VDOs) and attaches to them.
  3. For any mount request that arrives on the CDO the legacy file system filter can attach right in the mount path.
However, please note that it's possible (though not very common) that the file system filter has a user mode component that tells the filter to attach to some specific volume and as such it's possible that the legacy filter attaches to the volume out-of-order (or rather in no particular order). As you can expect it's impossible to predict or control the load order in this case because the filter will simply attach on top of whatever happens to be the top of the stack at that time.
Filter Manager is a legacy filter and it follows the usual legacy filter steps, but there is an added twist. FltMgr can attach to the same volume multiple times (each attachment is called a frame) and each such attachment follows the same steps as if it were a complete new filter (FltMgr attaches to the CDOs for all the file systems, enumerates volumes and attaches to them and so on). The frame right on top of the file system is frame 0 and the one on top of it is frame 1 and so on. The decision that a new frame is required is made when a new filter is registered and it is based on the following factors:
  • whether the altitude for the filter is higher than the highest altitude in the top frame. Each frame has a an bottom altitude and a top altitude and any filter with the altitude in that range belongs to that frame. On my Win7 machine when FltMgr creates frame0 it sets a bottom altitude of 0 and a top altitude of 49999 (though I'm not sure why or what are the guarantees around this top altitude; this post also indicates that things used to be different at some point). Naturally if the altitude already fits in one of the existing frames then the filter will be placed in that frame at the right place.
  • whether one or more legacy filters have attached on top of FltMgr. If no legacy filters have attached then FltMgr simply changes the altitude of the top frame to the altitude of the filter and registers the filter with that frame.
  • OS version. On XP, if the filter can't fit in the existing frames and there is a new legacy filter attached then FltMgr simply creates a new frame that has the bottom altitude right above the one of the previous top frame. In Vista and newer Oses the behavior is a bit different. If the altitude for the new filter is higher that the upper altitude of the topmost frame and a legacy filter has attached then FltMgr tries to identify the type of filter the legacy filter is by looking at the LoadOrderGroup and based on that it generates a fake altitude for the legacy filter and then if the top frame's upper altitude is below that fake altitude for the legacy filter then it adjusts the top frame's upper altitude to be right up to the legacy filter's fake altitude (legacy filters can only attach on top of the topmost frame and so only that frame's upper altitude changes). At this point FltMgr checks whether the filter will now fit in the top frame (which might happen since its altitude range has been extended) and if the new filter still doesn't fit there it will finally create a new frame.
Since DriverEntry is where most legacy filters attach to CDOs and most minifilters call FltRegisterFilter() from, the order in which events happen can generally be inferred just by looking at the load order group for each filter (legacy and mini) and by following the rules. Just to illustrate this let's say that we have two minifilters, MF1 (altitude 134999 which should make it a virtualization filter) and MF2 (altitude 324999 which makes it an anti-virus filter) and a legacy encryption filter, LF1. All the filters are BOOT start drivers. Ideally all of these would have their appropriate Load Order Group and things would work just fine. However I'd like to show a couple of scenarios where things can go wrong. For the scenarios please assume that all the drivers that I'm not specifically calling out are loading in their appropriate group.
  1. Let's say that MF1 discovers that there is no "FSFilter Virtualization" group on XP and decides to change its Load Order Group to the next group, the FSFilter Encryption group. Now what will happen is that either LF1 or MF1 can be started first (depending most likely on the order on which they were installed on the system). Let's say we are on XP and LF1 loads first. When MF1 loads and calls FltRegisterFilter() FltMgr will see that it has frame 0 with a range of 0-49999 and since LF1 is loaded and this is XP FltMgr will create Frame1 with the range 49999-134999 and load MF1 into that frame. The net result here is that MF1 is layered above LF1. However, all of Frame1 is on top of LF1 and so all the minifilters in the groups that fall into that range (FSFilter Copy Protection, FSFilter Security Enhancer, FSFilter Open File, FSFilter Physical Quota Management, FSFilter Virtualization and so on) will be above LF1 which might lead to issues in the long run. Now, on Vista and newer OSes the behavior for this scenario will be different. FltMgr will figure out that LF1 is an encryption filter and it will extend the range of frame0 to 0-149999 so that it covers the encryption range and so everything will be layered correctly. In my opinion it would be better if MF1 would select a load order group that is below the FSFilter Virtualization group for XP, FSFilter Physical Quota Management, which would guarantee that the minifilter loads before any legacy encryption filters and thus avoid the altitude inversion.
  2. Another possible scenario is where MF2 wants to load as early as possible and so it sets the LoadOrderGroup to FSFilter Bottom so that it loads and attaches really early on. In this case FltMgr will extend frame0 from 0-49999 to 0-324999 and load the minifilter in frame0. Then, when it loads LF1 it will attach it on top of frame0 and so now all the minifilters in frame0 will see only encrypted file data flowing through. As you can expect this will likely lead to problems at some point, either for MF2 or for some other filter that might be added to the system at a later time.
There really isn't much more I can say of the subject, all of it is fairly well documented except for how FltMgr does the frame altitude adjustment for Vista+, which isn't very complicated. I'll wrap things up with a couple of things I think filters should be aware of.
  • Minifilters still need to set an appropriate LoadOrderGroup, they can't just rely on the altitude mechanism because of interaction with legacy filters.
  • Legacy filters must use an appropriate LoadOrderGroup as well and they must also call IoRegisterFsRegistrationChange() (or IoRegisterFsRegistrationChangeMountAware() where available) otherwise FltMgr will not become aware of the legacy filter and will keep adding minifilters in existing top frame, leading to very interesting bugs.
  • Even though it's allowed that minifilters call FltRegisterFilter() at some later time (as opposed to registering directly from DriverEntry), it's generally better to call FltRegisterFilter() from DriverEntry which will associate the minifilter with the appropriate frame and perform the any altitude adjustment and should reduce interop issues with legacy filters.
  • Once a frame is no longer the top frame (i.e. after a new frame is created) its altitude range can no longer change at all. Only the top frame can change its altitude range, and only the upper altitude can change.
  • The upper altitude range for a frame can never decrease, it only increases.