Thursday, July 21, 2011

Using IoRegisterFsRegistrationChangeMountAware

I've already talked about how file system filters attach to volumes. However, there is one thing I didn't mention in the context of that discussion. There is a race that can happen in that path that can have unpleasant side-effects for filters.

I'll start with steps involved in a legacy filter attaching to a volume. This is discussed in more detail in another couple of posts on this blog so I'll skip over some steps and focus only on the ones that are relevant to the problem at hand:

  1. Legacy filter calls IoRegisterFsRegistrationChange().
  2. The notification callback gets called for a file system (initially all the registered ones and then new ones when they register).
  3. In the notification callback the legacy filter attaches to the file system control device objects so that it can receive the IRP_MJ_FILE_SYSTEM_CONTROL with the IRP_MN_MOUNT_VOLUME minor code request whenever the file system is asked to mount a new volume (and thus the filter will be notified of all new mounted volumes).
  4. Also in the notification callback the legacy filter walks over the list of devices that the file system has already created, which are all file system volume device objects (VDOs) for all the volumes mounted by that file system, and attaches to each of them (this takes care of the existing volumes).

Now let's take a look at the steps that the IO manager takes when trying to mount a volume (again these are just the relevant ones to the problem):

  1. Start at the head of the list of registered file system and get a reference to the CDO for that file system.
  2. Prepare an IRP_MJ_FILE_SYSTEM_CONTROL with the IRP_MN_MOUNT_VOLUME minor code IRP that will be sent to that CDO.
  3. Send the IRP and wait for it to complete.
  4. If the file system couldn't mount the volume then get the next entry in the list of registered file systems and reference the CDO for that file system and then go to step 2.

It is important to note that both the list of registered file systems and the list of drivers to be notified about the arrival of new file systems (drivers that called IoRegisterFsRegistrationChange()) are protected by the same lock. Incidentally, this lock (though private to the OS) is available in the debugger as nt!IopDatabaseResource:

0: kd> x nt!IopDatabaseResource
8299e860 nt!IopDatabaseResource = <no type information>
0: kd> !locks 8299e860 

Resource @ nt!IopDatabaseResource (0x8299e860)    Available
1 total locks

In step 3 of the volume mount path, where the IRP is sent to the CDO, the IO manager releases the IopDatabaseResource before sending the IRP down to the file system and then once the IRP completes it reacquires it. After all, holding a lock across a call to a driver is to be avoided whenever possible. However, this opens a very small window in which things can go wrong. If a volume mount is in progress and let's say that the IO manager has prepared the mount IRP and has just released the IopDatabaseResource and then the thread is preempted and on a different thread a filter calls IoRegisterFsRegistrationChange(), gets the list of registered file systems, attaches a device on the CDO for each file system and then it enumerates all the VDOs then the problem is the file system filter will completely miss the volume that is about to be mounted because there is no VDO for it yet (since the IRP_MN_MOUNT_VOLUME IRP hasn't reached the file system yet and so no VDO was created) and the device it has attached to the CDO will also not see the IRP_MN_MOUNT_VOLUME request because when the IO manager referenced the top device for the CDO the filter wasn't there yet and so the IRP will go to the device right below the filter.

The result of all this is that the filter will completely miss a mounted volume and will not attach to it. Since this requires that a filter calls IoRegisterFsRegistrationChange() exactly at the time when a volume is mounted, it is a very narrow window. This window can be avoided by using IoRegisterFsRegistrationChangeMountAware() instead of IoRegisterFsRegistrationChange(), where the IO manager synchronizes volume mounts with calls to IoRegisterFsRegistrationChangeMountAware().

Of course, this discussion is really only relevant to legacy filters, minifilters don't have to deal with all this since they never register with the IO manager directly.