Thursday, January 6, 2011

About IRP_MJ_CREATE and minifilter design considerations - Part IV

It is pretty common for a miniflter to attempt to redirect an open to a file to a different file. For example, when the user is trying to open "c:\temp\foo.txt" they would instead end up opening "d:\bar.txt". By far the easiest way to achieve this behavior is by using STATUS_REPARSE. There are some disadvantages to this method as compared to other methods (that I plan to discuss in a future post in this series) but it is widely used nevertheless because it is quite simple.

As described in one of the previous posts, ObpLookupObjectName is the OB function that is responsible for resolving a name to an actual OB object. Because the OB namespace design requires symbolic links, there needed to be a mechanism to implement this functionality and STATUS_REPARSE happens to be that mechanism. The whole OB symbolic link resolution code is encapsulated into just one function, ObpLookupObjectName. The contract is that when the object found has a parse routine (symbolic link objects, file objects and device objects do), the OB manager will call that function with a pointer to a UNICODE_STRING describing the path. If the parse procedure returns STATUS_REPARSE then the function must restart the lookup with the new name supplied in the UNICODE_STRING.

This is a pretty clean mechanism and it is fairly easy to use by things plugging into the OB namespace, like file systems and file system filters. In the latest WDK there is a minifilter sample that is an example of how a minifilter can use STATUS_REPARSE to redirect the create to a file. Here are the steps in the function SimRepPreCreate(which are similar to most other filters doing this):
  1. Eliminate cases where we don't want to reparse (paging files and volume opens)
  2. Get the name of the file that the user is trying to open (please note that this is a preCreate callback and the request is for the opened name, which is much faster to get in preCreate than the normalized name)
  3. Replace the name in the FILE_OBJECT (allocate a new buffer for the UNICODE_STRING if needed)

This is a list of things worth mentioning about using STATUS_REPARSE as a redirection mechanism:
  • ObpLookupObjectName will use the name that is returned in the FILE_OBJECT->FileName as a completely new name. This means that a filter (or a file system) must return a full path to the new file, complete with a device path (since this is an OB name after all). This can be seen in the the SimRepPreCreate function where the new name is built. However, SimRep reparses a path to a different path on the same volume, so the device name is the same. If the new path needs to be on a different device, the name in the FILE_OBJECT can either be something like "\Device\HarddiskVolume1\bar.txt" or even "\??\D:\bar.txt". This last path with the device name written as "\??\D:" works because ObpLookupObjectName restarts the lookup before the point where it resolves the "\??\" shortcut.
  • Sometimes the FILE_OBEJCT contains a RelatedFileObject member and the name in the FILE_OBJECT is relative to that RelatedFileObject. If STATUS_REPARSE is returned then the IO manager will simply ignore the RelatedFileObject from that point on and assume that the path that was returned in the FILE_OBJECT is a full path. This also simplifies things for the filter writer since it means that they don't need to care about RelatedFileObjects at all when returning STATUS_REPARSE, the path is always a full path.
  • It is possible to specify a DEVICE_OBJECT hint when calling IoCreateFileSpecifyDeviceObjectHint or IoCreateFileEx (so only kernel mode callers). When this happens the device specified is stored in the OPEN_PACKET and it is evaluated in IopParseDevice (by calling nt!IopCheckTopDeviceHint). This will fail if the path returned in the STATUS_REPARSE points to a different device. Moreover, the IRP_MJ_CREATE will be sent to the device specified in the device hint, so the file name must be a name that is meaningful at that layer, which might be different from the name at the top of the file system stack on that volume. This isn't generally a problem since the filter must be below that device in order to even see the request so it can know what the file system namespace looks like below the hint device level.
  • Another request that comes up a lot is how to track a request that a filter reparsed. For example, if my filter returns STATUS_REPARSE, I might not want to process that request when it comes down again (assuming that I reparse to another place that my filter filters as well). This is pretty complicated to do because all this happens before a stream is opened in the file system so stream-based contexts (like FltMgr contexts) will not work. In fact, in order for a filter to be able to track a create they must find a variable with the following properties:
    1. The variable must be accessible to the filter (this is pretty obvious but important nevertheless)…
    2. The variable must persist (keep either its value or its address the same) for the same call to IopCreateFile.
    3. The variable must be unique enough so that there is no chance of confusion between two IRP_MJ_CREATEs that happen at the same time.
    4. The variable must be changed (freed or released or it must get a new value) at the end of the IopCreateFile scope, so that new calls to IopCreateFile will not get the same value (otherwise a filter might record the variable value, return STATUS_REPARSE and then it might see a completely unrelated future create with the same value and assume it is the reparse it's been waiting for all along).
    5. The variable must be torn down cleanly even if the filter never receives it back, because there are no guarantees that after returning STATUS_REPARSE there will actually be another IRP_MJ_CREATE (maybe there was a device hint and the reparse was for a different stack, or maybe the maximum number of reparses was hit and so on).

    So it is easy to see that most variables that a filter has access to won't work:
    • the IRP doesn't work because it isn't persistent (it is freed at the end of the IopParseDevice call, so subsequent calls to IopParseDevice will likely get a new IRP)
    • the FILE_OBJECT doesn't work because its scope is also the IopParseDevice call.
    • the OPEN_PACKET would be nice, but it might be the same between different calls, and besides a filter doesn't have access to it anyway.
    • the thread will be the same, but it is not unique enough. There may a new completely unrelated create sent down on this same thread.
    • Finally, allocating some filter structure and sticking a pointer to it into an unused field someplace won't work because if there is never a new IRP sent to the filter then the structure will be leaked.

    So for Vista Microsoft decided to do something about this and introduced a bunch of new calls and a couple of new structures. In the new model, in the OPEN_PACKET there is a new structure nt!_IO_DRIVER_CREATE_CONTEXT:
    1: kd> dt nt!_OPEN_PACKET DriverCreateContext.
       +0x05c DriverCreateContext  :
          +0x000 Size                 : Int2B
          +0x004 ExtraCreateParameter : Ptr32 _ECP_LIST
          +0x008 DeviceObjectHint     : Ptr32 Void
          +0x00c TxnParameters        : Ptr32 _TXN_PARAMETER_BLOCK
    1: kd> dt nt!_ECP_LIST
       +0x000 Signature        : Uint4B
       +0x004 Flags            : Uint4B
       +0x008 EcpList          : _LIST_ENTRY

    In this structure there is another structure, the _ECP_LIST, which stores an unlimited number of other structures called ECPs (extra create parameters). These structures (in fact the list containing the structure) can be passed in as a parameter to IoCreateFileEx and, more importantly for our case, can be added by filters (both legacy and minifilters) to an existing IRP_MJ_CREATE request (there are two largely similar sets of APIs, FsRtl and Flt, with functions such as FsRtlAllocateExtraCreateParameter and FltAllocateExtraCreateParameter, respectively). The guarantee is that these ECPs will be passed to all IRP_MJ_CREATE IRPs associated with a call to IoCreateFile. They are guaranteed to be unique because each ECP is identified by a GUID and are also guaranteed to be torn down along with the OPEN_PACKET, at the end of the create operation.
    This mechanism allows the filter (legacy or mini) to allocate a new structure, associate it with an IRP_MJ_CREATE for which it wants to return STATUS_REPARSE, and then be able to know for any subsequent IRP_MJ_CREATE if it is related to this create it has already processed.
    Incidentally, another good use for this mechanism is to send some additional information with a create request to a filter or file system. A scenario where this might be useful is when a more complex product that also has a filter component (like an anti-virus product) wants to open a file but would like to tell the filter that this create originated from the product so perhaps the usual rules (for example scanning the file) might not apply.
    There two downsides to this mechanism. It is only available to kernel mode callers (you cannot pass in an ECP or an ECP list to NtCreateFile) and it is not available for XP...