Thursday, December 15, 2011

Debugging Minifilters: Using !fltkd.filter

Ever since I started this blog I had planned to go over all the fields that the !fltkd extension can show for the various FltMgr structures and talk about each of them. I'm going to start this with the !fltkd.filter command. Most commands take additional flags that control which information is shown. For this post I'll just focus on the default output without any flags and I'll cover the flags in a different post.

Before I begin I'd like to mention that the !fltkd command in general is heavily dependent on the public symbols for fltmgr.sys and so it doesn't work at all without the right symbols. Moreover, the extension itself and the symbols as well have some bugs and in some cases it can't display things correctly because some of the structures that it expects are not in the public symbols at all or at least they are not under the name the extension expects to find them. So you will occasionally see output from the debugger saying something along the lines of "Your debugger is not using the correct symbols … Type referenced: " followed by a type name. I've removed them from the output in the blog post and we'll work with what we have. Please note that if you want to follow along it's best to use a Win7 machine since the symbols were most complete in that release (though still not 100% fixed).

1: kd> !fltkd.filters

Filter List: 92cddd1c "Frame 0" 
   FLT_FILTER: 94144008 "luafv" "135000"
      FLT_INSTANCE: 94146008 "luafv" "135000"
   FLT_FILTER: 92cdda58 "FileInfo" "45000"
      FLT_INSTANCE: 930e7730 "FileInfo" "45000"
      FLT_INSTANCE: 93448dc8 "FileInfo" "45000"
      FLT_INSTANCE: 9357c340 "FileInfo" "45000"
      FLT_INSTANCE: 93584560 "FileInfo" "45000"
      FLT_INSTANCE: 9359a948 "FileInfo" "45000"
1: kd> !fltkd.filter 94144008 

FLT_FILTER: 94144008 "luafv" "135000"
   FLT_OBJECT: 94144008  [02000000] Filter
      RundownRef               : 0x00000008 (4)
      PointerCount             : 0x00000001 
      PrimaryLink              : [92cdda64-92cddd1c] 
   Frame                    : 92cddcc0 "Frame 0" 
   Flags                    : [00000006] FilteringInitiated NameProvider
   DriverObject             : 941427a8 
   FilterLink               : [92cdda64-92cddd1c] 
   PreVolumeMount           : 81f980cc  luafv!LuafvPreRedirect 
   PostVolumeMount          : 00000000  (null) 
   FilterUnload             : 00000000  (null) 
   InstanceSetup            : 81fa462b  luafv!LuafvInstanceSetup 
   InstanceQueryTeardown    : 00000000  (null) 
   InstanceTeardownStart    : 00000000  (null) 
   InstanceTeardownComplete : 00000000  (null) 
   ActiveOpens              : (941440dc)  mCount=1 
   Communication Port List  : (94144108)  mCount=0 
   Client Port List         : (94144134)  mCount=0 
   VerifierExtension        : 00000000 
   Operations               : 94144164 
   OldDriverUnload          : 00000000  (null) 
   SupportedContexts        : (941440a0)
      VolumeContexts           : (941440a0)
      InstanceContexts         : (941440a0)
      FileContexts             : (941440a0)
      StreamContexts           : (941440a0)
      StreamHandleContexts     : (941440a0)
      TransactionContext       : (941440a0)
   InstanceList             : (94144038)
      FLT_INSTANCE: 94146008 "luafv" "135000"

I'll just go over each field and discuss its meaning.
  • FLT_OBJECT: 94144008 [02000000] Filter - some of the main FltMgr objects (instance, volume and filter) begin with a standard structure that identifies the object type and contains some common synchronization primitives. You can see the structure by doing a "dt fltmgr!_FLT_OBJECT". The FLT_OBJECT is the first member of the structure and as such the address of the FLT_OBJECT is the same as the address of the bigger structure, which is the FLT_FILTER in our case.
  • RundownRef : 0x00000008 (4) - this is a rundown reference primitive that I've mentioned in my post here. The actual reference count is the value in parentheses. This value is incremented and decremented whenever someone calls FltObjectReference() and, respectively, FltObjectDereference(). This allows the filter to wait for other components to release their reference before tearing down (since the rundown reference will enter a special state that doesn't allow more references to be taken once the teardown process starts; this is different from a regular reference count where it's possible that the refcount never drops to 0)..
  • PointerCount : 0x00000001 - this is the actual reference count that controls when the memory is freed. There is no direct way filters can influence this (like FltReferenceObject), but I've seen communication ports increment this count.
  • PrimaryLink : [92cdda64-92cddd1c] - this is a link into a list of filters for the frame. Please remember that a filter is not aware of anything outside the frame and so the list of filters is a per-frame concept.
  • Frame : 92cddcc0 "Frame 0" - this is obviously a pointer to the frame the filter belongs to.
  • Flags : [00000006] FilteringInitiated NameProvider - obviously these are flags. Possible values include (as a general trick it's pretty easy to generate this list by changing the value of the flags in the structure and asking !fltkd.filter to display the filter again):
    • UnloadInProgress - the minifilter is unloading
    • FilteringInitiated - the minifilter has started filtering by calling FltStartFiltering().
    • NameProvider - the minifilter is a name provider. I think FltMgr sets this flag based on whether the minifilter registers the name provider callbacks.
  • DriverObject : 941427a8 - obviously the DRIVER_OBJECT for the filter.
  • FilterLink : [92cdda64-92cddd1c] - this is the same list as the PrimaryLink member of the FLT_OBJECT structure above.
  • PreVolumeMount : 81f980cc luafv!LuafvPreRedirect - this is actually the preOp callback for the IRP_MJ_MOUNT_VOLUME operation (that I've mentioned in my post How File System Filters Attach to Volumes - Part II). This looks like a strange place to have an operation callback but it makes sense because when this callback is called there is no instance associated with the volume (since the volume isn't mounted yet) and so the pointer couldn't have been called through the usual instance mechanism.
  • PostVolumeMount : 00000000 (null) - this is the postOp callback for the IRP_MJ_MOUNT_VOLUME operation.
  • FilterUnload : 00000000 (null) - this is the Unload callback (the FLT_REGISTRATION->FilterUnloadCallback member).
  • InstanceSetup : 81fa462b luafv!LuafvInstanceSetup - this is the FLT_REGISTRATION->InstanceSetupCallback member.
  • InstanceQueryTeardown : 00000000 (null) - this is the FLT_REGISTRATION->InstanceQueryTeardownCallback member.
  • InstanceTeardownStart : 00000000 (null) - this is the FLT_REGISTRATION->InstanceTeardownStartCallback member.
  • InstanceTeardownComplete : 00000000 (null) - this is the FLT_REGISTRATION->InstanceTeardownCompleteCallback member.
  • ActiveOpens : (941440dc) mCount=1 - the value in parentheses is a pointer to an internal FltMgr structure that is simply a doubly linked list protected by a mutex (you can see in the debugger by doing a dt fltmgr!_FLT_MUTEX_LIST_HEAD). For more information on this field please check out my post on Tracking a minifilter's ActiveOpens files.
  • Communication Port List : (94144108) mCount=0 - this is a list of opened communication ports for this filter (the same FLT_MUTEX_LIST_HEAD structure as mentioned above).
  • Client Port List : (94144134) mCount=0 - this is a list of connected communication ports.
  • VerifierExtension : 00000000 - this is the a structure that is used when DriverVerifier is enabled for this minifilter. I will go into more detail on this structure in a future post.
  • Operations : 94144164 - as mentioned in my post Debugging Minifilters: Finding the Callbacks this is actually a pointer to the _FLT_OPERATION_REGISTRATION structure that the filter used when it registered with FltMgr.
  • OldDriverUnload : 00000000 (null) - this is the routine that the driver registered as an unload routine. When a driver registers as a minifilter FltMgr needs to know when the driver is being unloaded (because it is possible to unload a minifilter like any other driver) and so FltMgr replaces the _DRIVER_OBJECT->DriverUnload member function with its own function so that it can detect that someone is trying to unload the driver and so that FltMgr can start the teardown process. Well, the original DriverUnload function is stored in the FLT_FILTER structure in this member.
  • SupportedContexts : (941440a0) - this is an array of information related to the context registration. I will discuss this in more depth in a future post.
  • InstanceList : (94144038) - this is a pointer to another FltMgr internal structure, the fltmgr!_FLT_RESOURCE_LIST_HEAD, which is a list protected by an ERESOURCE (as opposed to the fltmgr!_FLT_MUTEX_LIST_HEAD which was protected by a mutex). The entries in this list are the instances.

That's it for now. Next week I plan to discuss some of the !fltkd.filter flags and their output.

Thursday, December 8, 2011

Debugging Minifilters: Finding the Callbacks

When debugging interop issues I often need to be able to set breakpoints on a certain filter's dispatch function for a certain operation. In most cases the other filter is written by a 3rd party and I have no symbols for it at all and so it's not easy to look at function names and guess what the appropriate function to set the breakpoint on is. So in this post I would like to show how to find the appropriate routines for each operation for both legacy filters and minifilters. Because i'm using Microsoft filters for which symbols are available you can see all the function names, but of course the same principle applies even when you don't have symbols just that the names won't look so pretty.
When dealing with legacy filters the approach to get the callbacks is pretty well-known and it relies on looking at the fields in the DRIVER_OBJECT:
0: kd> !drvobj sr
Driver object (82332290) is for:
 \FileSystem\sr
Driver Extension List: (id , addr)

Device Object list:
8207d830  8213add0  8235e020  8238e248
8238e030  
0: kd> dt 82332290 nt!_DRIVER_OBJECT
   +0x000 Type             : 0n4
   +0x002 Size             : 0n168
   +0x004 DeviceObject     : 0x8207d830 _DEVICE_OBJECT
   +0x008 Flags            : 0x12
   +0x00c DriverStart      : 0xf8489000 Void
   +0x010 DriverSize       : 0x11f00
   +0x014 DriverSection    : 0x823eb998 Void
   +0x018 DriverExtension  : 0x82332338 _DRIVER_EXTENSION
   +0x01c DriverName       : _UNICODE_STRING "\FileSystem\sr"
   +0x024 HardwareDatabase : 0x8067d260 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
   +0x028 FastIoDispatch   : 0xf848b4c0 _FAST_IO_DISPATCH
   +0x02c DriverInit       : 0xf8498fd4     long  sr!GsDriverEntry+0
   +0x030 DriverStartIo    : (null) 
   +0x034 DriverUnload     : (null) 
   +0x038 MajorFunction    : [28] 0xf848e726     long  sr!SrCreate+0
0: kd> dps 82332290+0x38 L0n28
823322c8  f848e726 sr!SrCreate
823322cc  f8489428 sr!SrPassThrough
823322d0  f8489428 sr!SrPassThrough
823322d4  f8489428 sr!SrPassThrough
823322d8  f8489320 sr!SrWrite
…
82332330  f8489428 sr!SrPassThrough
82332334  f848dd52 sr!SrPnp
Of course, legacy filters also have a couple of other sets of entrypoints, the FastIO routines and the FS_FILTER_CALLBACKS, which we might need to set breakpoints on:
0: kd> dt 0xf848b4c0 _FAST_IO_DISPATCH
nt!_FAST_IO_DISPATCH
   +0x000 SizeOfFastIoDispatch : 0x70
   +0x004 FastIoCheckIfPossible : 0xf8490914     unsigned char  sr!SrFastIoCheckIfPossible+0
   +0x008 FastIoRead       : 0xf8490962     unsigned char  sr!SrFastIoRead+0
   +0x00c FastIoWrite      : 0xf84909b0     unsigned char  sr!SrFastIoWrite+0
...
   +0x060 FastIoQueryOpen  : 0xf8490f30     unsigned char  sr!SrFastIoQueryOpen+0
   +0x064 ReleaseForModWrite : (null) 
   +0x068 AcquireForCcFlush : (null) 
   +0x06c ReleaseForCcFlush : (null) 

0: kd> dt 0x82332338 nt!_DRIVER_EXTENSION
   +0x000 DriverObject     : 0x82332290 _DRIVER_OBJECT
   +0x004 AddDevice        : (null) 
   +0x008 Count            : 0
   +0x00c ServiceKeyName   : _UNICODE_STRING "sr"
   +0x014 ClientDriverExtension : (null) 
   +0x018 FsFilterCallbacks : 0x8236d238 _FS_FILTER_CALLBACKS
0: kd> dt 0x8236d238 _FS_FILTER_CALLBACKS
nt!_FS_FILTER_CALLBACKS
   +0x000 SizeOfFsFilterCallbacks : 0x38
   +0x004 Reserved         : 0
   +0x008 PreAcquireForSectionSynchronization : 0xf8490c30     long  sr!SrPreAcquireForSectionSynchronization+0
   +0x00c PostAcquireForSectionSynchronization : (null) 
...
   +0x030 PreReleaseForModifiedPageWriter : (null) 
   +0x034 PostReleaseForModifiedPageWriter : (null) 
Things are similar for minifilters. There is a !fltkd command that tries to make things easier, but in my oppinion it could do a better job at helping out. For one, even though the callbacks are defined on a per-filter basis, the !fltkd.filter command that lists the contents of the filter structure doesn't actually show the registered callbacks, and instead one must use the the !fltkd.instance command on one of the instances of the filter , command that has a special flag that can be used to see the callbacks:
1: kd> !fltkd.filters

Filter List: 92cddd1c "Frame 0" 
   FLT_FILTER: 94144008 "luafv" "135000"
      FLT_INSTANCE: 94146008 "luafv" "135000"
   FLT_FILTER: 92cdda58 "FileInfo" "45000"
      FLT_INSTANCE: 930e7730 "FileInfo" "45000"
      FLT_INSTANCE: 93448dc8 "FileInfo" "45000"
      FLT_INSTANCE: 9357c340 "FileInfo" "45000"
      FLT_INSTANCE: 93584560 "FileInfo" "45000"
      FLT_INSTANCE: 9359a948 "FileInfo" "45000"
1: kd> !fltkd.filter 94144008 

FLT_FILTER: 94144008 "luafv" "135000"
   FLT_OBJECT: 94144008  [02000000] Filter
      RundownRef               : 0x00000008 (4)
      PointerCount             : 0x00000001 
      PrimaryLink              : [92cdda64-92cddd1c] 
   Frame                    : 92cddcc0 "Frame 0" 
   Flags                    : [00000006] FilteringInitiated NameProvider
   DriverObject             : 941427a8 
   FilterLink               : [92cdda64-92cddd1c] 
   PreVolumeMount           : 81f980cc  luafv!LuafvPreRedirect 
   PostVolumeMount          : 00000000  (null) 
   FilterUnload             : 00000000  (null) 
   InstanceSetup            : 81fa462b  luafv!LuafvInstanceSetup 
   InstanceQueryTeardown    : 00000000  (null) 
   InstanceTeardownStart    : 00000000  (null) 
   InstanceTeardownComplete : 00000000  (null) 
   ActiveOpens              : (941440dc)  mCount=1 
   Communication Port List  : (94144108)  mCount=0 
   Client Port List         : (94144134)  mCount=0 
   VerifierExtension        : 00000000 
   Operations               : 94144164 
   OldDriverUnload          : 00000000  (null) 
   SupportedContexts        : (941440a0)
      VolumeContexts           : (941440a0)
      InstanceContexts         : (941440a4)
         ALLOCATE_CONTEXT_NODE: 94144a20 "luafv" [01] LookasideList (size=608)
      FileContexts             : (941440a8)
      StreamContexts           : (941440ac)
      StreamHandleContexts     : (941440b0)
         ALLOCATE_CONTEXT_NODE: 94144ae8 "luafv" [01] LookasideList (size=20)
      TransactionContext       : (941440b4)
   InstanceList             : (94144038)
      FLT_INSTANCE: 94146008 "luafv" "135000"
1: kd> !fltkd.instance 94146008 4

FLT_INSTANCE: 94146008 "luafv" "135000"
   CallbackNodes            : (94146054)
       NORMALIZE_NAME_COMPONENT (-22)
         CALLBACK_NODE: 94146584  Inst:(94146008,"luafv","\Device\HarddiskVolume2") "luafv" "135000"
       GENERATE_FILE_NAME (-21)
         CALLBACK_NODE: 9414656c  Inst:(94146008,"luafv","\Device\HarddiskVolume2") "luafv" "135000"
       MDL_WRITE_COMPLETE (-18)
         CALLBACK_NODE: 9414611c  Inst:(94146008,"luafv","\Device\HarddiskVolume2") "luafv" "135000"
       PREPARE_MDL_WRITE (-17)
...
       ACQUIRE_FOR_SECTION_SYNC (-1)
         CALLBACK_NODE: 941462b4  Inst:(94146008,"luafv","\Device\HarddiskVolume2") "luafv" "135000"
       CREATE (0)
         CALLBACK_NODE: 941462cc  Inst:(94146008,"luafv","\Device\HarddiskVolume2") "luafv" "135000"
       CREATE_NAMED_PIPE (1)
         CALLBACK_NODE: 941462e4  Inst:(94146008,"luafv","\Device\HarddiskVolume2") "luafv" "135000"
...
       SET_QUOTA (26)
         CALLBACK_NODE: 9414653c  Inst:(94146008,"luafv","\Device\HarddiskVolume2") "luafv" "135000"
       PNP (27)
         CALLBACK_NODE: 94146554  Inst:(94146008,"luafv","\Device\HarddiskVolume2") "luafv" "135000"
However, the above mentioned debugger function returns pointers to a CALLBACK_NODE structure from which one can figure out what the actual function is. Please note that the structure contains unions and so a lot of the members actually point to the same function. It generally is pretty clear which member should be used depending on the function for which we got the CALLBACK_NODE. For this example I took the IRP_MJ_CREATE callback and the NORMALIZE_NAME_COMPONENT callback:
1: kd> dt 941462cc  fltmgr!_CALLBACK_NODE
   +0x000 CallbackLinks    : _LIST_ENTRY [ 0x93448edc - 0x9343757c ]
   +0x008 Instance         : 0x94146008 _FLT_INSTANCE
   +0x00c PreOperation     : 0x81f9f263     _FLT_PREOP_CALLBACK_STATUS  luafv!LuafvPreCreate+0
   +0x010 PostOperation    : 0x81fa24e8     _FLT_POSTOP_CALLBACK_STATUS  luafv!LuafvPostCreate+0
   +0x00c GenerateFileName : 0x81f9f263     long  luafv!LuafvPreCreate+0
   +0x00c NormalizeNameComponent : 0x81f9f263     long  luafv!LuafvPreCreate+0
   +0x00c NormalizeNameComponentEx : 0x81f9f263     long  luafv!LuafvPreCreate+0
   +0x010 NormalizeContextCleanup : 0x81fa24e8     void  luafv!LuafvPostCreate+0
   +0x014 Flags            : 0 (No matching name)
1: kd> dt 94146584  fltmgr!_CALLBACK_NODE
   +0x000 CallbackLinks    : _LIST_ENTRY [ 0x934374cc - 0x934374cc ]
   +0x008 Instance         : 0x94146008 _FLT_INSTANCE
   +0x00c PreOperation     : 0x81fa09b2     _FLT_PREOP_CALLBACK_STATUS  luafv!LuafvNormalizeNameComponentEx+0
   +0x010 PostOperation    : (null) 
   +0x00c GenerateFileName : 0x81fa09b2     long  luafv!LuafvNormalizeNameComponentEx+0
   +0x00c NormalizeNameComponent : 0x81fa09b2     long  luafv!LuafvNormalizeNameComponentEx+0
   +0x00c NormalizeNameComponentEx : 0x81fa09b2     long  luafv!LuafvNormalizeNameComponentEx+0
   +0x010 NormalizeContextCleanup : (null) 
   +0x014 Flags            : 4 ( CBNFL_USE_NAME_CALLBACK_EX )
Personally I found this approach somewhat cumbersome to use and so what I've done is to list the array of callback directly from the filter object. Please note that the array ends with a record for the IRP_MJ_OPERATION_END pseudo-operation (which is just an array terminator) which has the value of 0x80. This is actually the array of FLT_OPERATION_REGISTRATION structures that is passed to FltRegisterFilter() function in the FLT_REGISTRATION.OperationRegistration field. As such this array does not include the name generation callbacks.
: kd> dt -oca60 94144164 _FLT_OPERATION_REGISTRATION
fltmgr!_FLT_OPERATION_REGISTRATION
[0] @ 94144164 MajorFunction 0xec ''  Flags 0  PreOperation 0x81f980cc  _FLT_PREOP_CALLBACK_STATUS  luafv!LuafvPreRedirect+0  PostOperation (null)   Reserved1 (null)   
[1] @ 94144178 MajorFunction 0xed ''  Flags 0  PreOperation 0x81f980cc  _FLT_PREOP_CALLBACK_STATUS  luafv!LuafvPreRedirect+0  PostOperation (null)   Reserved1 (null)   
[2] @ 9414418c MajorFunction 0xee ''  Flags 0  PreOperation 0x81f98005  _FLT_PREOP_CALLBACK_STATUS  luafv!LuafvPreWrite+0  PostOperation (null)   Reserved1 (null)   
[3] @ 941441a0 MajorFunction 0xef ''  Flags 0  PreOperation 0x81f980cc  _FLT_PREOP_CALLBACK_STATUS  luafv!LuafvPreRedirect+0  PostOperation (null)   Reserved1 (null)   
…
[45] @ 941444e8 MajorFunction 0x19 ''  Flags 0  PreOperation 0x81f980cc  _FLT_PREOP_CALLBACK_STATUS  luafv!LuafvPreRedirect+0  PostOperation (null)   Reserved1 (null)   
[46] @ 941444fc MajorFunction 0x1a ''  Flags 0  PreOperation 0x81f980cc  _FLT_PREOP_CALLBACK_STATUS  luafv!LuafvPreRedirect+0  PostOperation (null)   Reserved1 (null)   
[47] @ 94144510 MajorFunction 0x1b ''  Flags 0  PreOperation 0x81fa3330  _FLT_PREOP_CALLBACK_STATUS  luafv!LuafvPrePnp+0  PostOperation (null)   Reserved1 (null)   
[48] @ 94144524 MajorFunction 0x80 ''  Flags 0  PreOperation (null)   PostOperation (null)   Reserved1 (null)   
...

Thursday, December 1, 2011

Name Normalization in Win8

It's interesting to note that one of the FltMgr features that a lot of minifilters use is the ability to have FltMgr generate normalized paths. This is not something trivial to implement and a lot of the code in FltMgr is dedicated to generating these names. It's also fairly expensive so FltMgr implements a name cache. However file systems don't implement any mechanism to get this information, even though looking at the Win7 WDK we can see some references that indicate that this has been in the works (search for FileNormalizedNameInformation; there are references to it both in wdm.h and ntddk.h):

typedef enum _FILE_INFORMATION_CLASS {
    FileDirectoryInformation         = 1,
    FileFullDirectoryInformation,   // 2
 ...
    FileNormalizedNameInformation,           // 48 <- this seems to be the information class one can use to request the name..
 ...
} FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS;

//
// This is also used for FileNormalizedNameInformation <- this indicates that the _FILE_NAME_INFORMATION structure can also be used for normalized names
//

typedef struct _FILE_NAME_INFORMATION {
    ULONG FileNameLength;
    WCHAR FileName[1];
} FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION;

Well, with the arrival of Win8 things are about to change and file systems and file system filters now have a way to query the file system directly for a normalized name. We can see some of the work that needs to happen by looking at the Win8 FastFat source, but the changes look pretty minimal. Also, there are no changes that I can see in the WDK headers from the Win7 WDK.

_Requires_lock_held_(_Global_critical_region_)    
NTSTATUS
FatCommonQueryInformation (
    IN PIRP_CONTEXT IrpContext,
    IN PIRP Irp
    )
{
...
            case FileNormalizedNameInformation:  <- FastFat will now support this request

                FatQueryNameInfo( IrpContext, Fcb, Ccb, TRUE, Buffer, &Length ); <- we can see a new boolean parameter for FatQueryNameInfo
                break;
}
...

VOID
FatQueryNameInfo (
...
    IN BOOLEAN Normalized,  <- new argument to tell FatQueryNameInfo whether it should return a normalized name or not
...
    )

/*++

Routine Description:

    This routine performs the query name information function for fat.

Arguments:

 ...
    Normalized - if true the caller wants a normalized name (w/out short names).
        This means we're servicing a FileNormalizedNameInformation query.

 ...

Return Value:

    None

--*/

{

 ...
    if (!Normalized &&
        (Fcb->LongName.Unicode.Name.Unicode.Buffer != NULL)) {

        if ((Ccb != NULL) &&
            FlagOn(Ccb->Flags, CCB_FLAG_OPENED_BY_SHORTNAME)) {

            TrimLength = Fcb->FinalNameLength;
        }
    }

 …
}

So now that we know that FastFat implements this, the next logical question is whether NTFS implements it as well and a small program I wrote confirms that it does and that it does indeed return a normalized path like one would expect.

The implications for file system developers are pretty clear, this is one more information class they need to implement and they can use FastFat as a sample of how that can be implemented. One thing I would add is that file systems might want to be extra careful about the performance of the implementation since it's likely that FltMgr's name generation and normalization code will use this information class and so it might be called pretty frequently.

Finally I'd like to talk about the implications about file system filter developers. There are a couple of scenarios that I think could be impacted by this.

  • Name providers must implement this information class. Actually all the filters that currently implement the FileNameInformation class should implement this new class as well (and if they do that they're most likely name providers.. I can't think of a case where a filter would need to implement that class and not be a name provider).
  • Filters that call FltGetFileNameInformation(..,FLT_FILE_NAME_NORMALIZED, ..) in preCreate might not see a significant performance improvement since in preCreate the file isn't opened and so FltMgr can't use this information class and while it might be able to use it for some parts of the path, a lot of the overhead is still there. So querying the normalized name in preCreate is likely to be just as bad as ever.
  • Filters that call FltGetFileNameInformation(..,FLT_FILE_NAME_NORMALIZED, ..) for open files might see some performance improvements since FltMgr should be able to leverage this information class a lot. Please note that I haven't verified that FltMgr actually does use this class, but it would make a lot of sense to use it.
  • Legacy filters would probably benefit the most from this new class but I don't expect that a lot of the legacy filters that are still around are in active development.

Thursday, November 24, 2011

Investigating an SR.sys Issue

This week I've looked at an interop issue with SR.sys and I want to share the results and the investigation. The filter I was looking at (MyFilter) is an SFO type filter which completes certain IRP_MJ_CREATEs for specific FILE_OBJECTs and then implements all the requests for them. As usual great care must be taken that none of the FILE_OBJECTs that MyFilter owns (SFOs) are ever seen below my filter (in other filters or in the file system) or those filters or the file system might think they own the FILE_OBJECTs and start walking their private structures (like the FCB or CCB) and get very confused about things and then either end with a bugcheck in the good case or data corruption in the bad case.
The problem I was investigating was exactly one of these, where somehow one of my FILE_OBJECTs ended up on the lower file system, NTFS. This is what the stack looked like:
f511ee9c f840cedc f511ef88 82035578 f511eed8 Ntfs!NtfsDecodeFileObject+0x37
f511ef10 f840b49c f511ef88 81fd5900 81cfb770 Ntfs!NtfsCommonQueryInformation+0x56
f511ef74 f840b4d5 f511ef88 81fd5900 00000001 Ntfs!NtfsFsdDispatchSwitch+0x12a
f511f098 804ef18f 81cfb770 81fd5900 81fd5900 Ntfs!NtfsFsdDispatchWait+0x1c
f511f0a8 f849852d 81d1fc10 f511f16e f511f15c nt!IopfCallDriver+0x31
f511f0d4 f849282c 81cfb770 82035578 f511f16e sr!SrQueryInformationFile+0x99
f511f100 f8492f33 00000034 82035578 f511f15c sr!SrpGetFileName+0x32
f511f270 f84936d1 81d1fc10 f511f2e0 f511f2d7 sr!SrpExpandPathOfFileName+0x19f
f511f290 f8493873 81d1fc10 823896a0 f511f2e0 sr!SrpGetFileNameFromFileObject+0xe7
f511f3f4 f848e8c2 81d1fc10 823896a0 00000000 sr!SrFileAlreadyExists+0x5f
f511f44c 804ef18f 81d1fc10 00000002 82130c98 sr!SrCreate+0x19c
f511f45c f84ab6c3 823896a0 82130ca8 823ca2e0 nt!IopfCallDriver+0x31
f511f48c 804ef18f 81e32020 82130c98 82130c98 fltMgr!FltpCreate+0x1d9
f511f49c 805831fa 81ccdb68 81fddfd4 f511f634 nt!IopfCallDriver+0x31
f511f57c 805bf444 81ccdb80 00000000 81fddf30 nt!IopParseDevice+0xa12
f511f5f4 805bb9d0 00000000 f511f634 00000640 nt!ObpLookupObjectName+0x53c
f511f648 80576033 00000000 00000000 36039c00 nt!ObOpenObjectByName+0xea
f511f6c4 80576a20 f511f848 00100002 f511f850 nt!IopCreateFile+0x407
f511f70c f84ad5b9 f511f848 00100002 f511f850 nt!IoCreateFileSpecifyDeviceObjectHint+0x52
f511f7b8 f84ada28 81d2d2d0 81d31008 f511f848 fltMgr!FltCreateFileEx+0x113
f511f7fc f500ea00 81d2d2d0 81d31008 f511f848 fltMgr!FltCreateFile+0x36
f511f868 f500ec87 81fc0994 81d31008 00000000 myfilter!MyCreateFile+0x100
Looking at the stack one thing looks really strange: FltMgr is calling into SR which is calling into NTFS. This indicates that SR is somehow loaded between FltMgr and NTFS. So I decided to see that the file system stack looks like:
1: kd> !fltkd.volumes

Volume List: 820210a0 "Frame 0" 
   FLT_VOLUME: 820ad668 "\Device\WebDavRedirector"
   FLT_VOLUME: 81d01168 "\Device\LanmanRedirector"
   FLT_VOLUME: 82005b48 "\Device\HGFS"
   FLT_VOLUME: 81ccbc18 "\Device\VhdDisk00000003"
      FLT_INSTANCE: 81d31008 "MyFilter Default Instance" "137000"
   FLT_VOLUME: 81d01c18 "\Device\VhdDisk00000002"
   FLT_VOLUME: 81cc8c18 "\Device\VhdDisk00000001"
   FLT_VOLUME: 8207fbd8 "\Device\HarddiskDmVolumes\PhysicalDmVolumes\BlockVolume1"
   FLT_VOLUME: 81e9a8a0 "\Device\HarddiskVolume2"
   FLT_VOLUME: 81d2e008 "\Device\HarddiskVolume1"
1: kd> !fltkd.volume 81ccbc18 

FLT_VOLUME: 81ccbc18 "\Device\VhdDisk00000003"
   FLT_OBJECT: 81ccbc18  [04000000] Volume
      RundownRef               : 0x00000004 (2)
      PointerCount             : 0x00000001 
      PrimaryLink              : [81d01c24-82005b54] 
   Frame                    : 82021000 "Frame 0" 
   Flags                    : [00000004] SetupNotifyCalled
   FileSystemType           : [00000002] FLT_FSTYPE_NTFS
   VolumeLink               : [81d01c24-82005b54] 
   DeviceObject             : 81e32020 
   DiskDeviceObject         : 81ccdb80 
   VolumeInNextFrame        : 00000000 
   Guid                     : "" 
   CDODeviceName            : "\Ntfs" 
   CDODriverName            : "\FileSystem\Ntfs" 
   Callbacks                : (81ccbca8)
   ContextLock              : (81ccbe38)
   VolumeContexts           : (81ccbe70)  Count=0
   StreamListCtrls          : (81ccbe74)  rCount=56 
   NameCacheCtrl            : (81ccbeb8)
   InstanceList             : (81ccbc64)
      FLT_INSTANCE: 81d31008 "MyFilter Default Instance" "137000"
1: kd> !devstack 81e32020 
  !DevObj   !DrvObj            !DevExt   ObjectName
> 81e32020  \FileSystem\FltMgr 81e320d8  
  81d1fb58  \FileSystem\sr     81d1fc10  
  81cfb770  \FileSystem\Ntfs   81cfb828 
So as you can see SR is indeed loaded between NTFS and FltMgr's frame0. As I mentioned in my post on how the file system stack is layered, in XP FltMgr doesn't create frame0 immediately and instead it waits for the first minifilter to register before it creates it. Since on my system there is no other minifilter FltMgr never created frame0 until I manually loaded my filter. However, since SR is a boot start driver by the time FltMgr initialized frame0 SR was already attached and FltMgr had no option but to attach on top of it.
Now, the FILE_OBJECT that NTFS chokes on is 82035578 and it is indeed an SFO. Looking on the stack we can see that it first appears when SR calls SrpGetFileName. There were two possibilities. Either I had leaked my SFO below my filter (either in this operation or at some point in the past) and SR got it and was using it or SR got their own FILE_OBJECT from my filter by issuing a request above my filter (most likely to the top of the stack). So I decided to see what the function that calls SrpGetFileName does (the function is rather long so I trimmed it a bit but it's still quite long so i tried to highlight things):
1: kd> uf sr!SrpExpandPathOfFileName
sr!SrpExpandPathOfFileName:
f8492d94 8bff            mov     edi,edi
f8492d96 55              push    ebp
…
f8492e3a 56              push    esi
f8492e3b 6800080000      push    800h
f8492e40 56              push    esi
f8492e41 56              push    esi
f8492e42 8385d4feffff02  add     dword ptr [ebp-12Ch],2
f8492e49 56              push    esi
f8492e4a 56              push    esi
f8492e4b 6821400000      push    4021h
f8492e50 6a01            push    1
f8492e52 6a03            push    3
f8492e54 6880000000      push    80h
f8492e59 56              push    esi
f8492e5a 8d85b0feffff    lea     eax,[ebp-150h]
f8492e60 50              push    eax
f8492e61 8d85b8feffff    lea     eax,[ebp-148h]
f8492e67 50              push    eax
f8492e68 6800001000      push    100000h
f8492e6d 8d85d8feffff    lea     eax,[ebp-128h]
f8492e73 50              push    eax
f8492e74 c785b8feffff18000000 mov dword ptr [ebp-148h],18h
f8492e7e 89b5bcfeffff    mov     dword ptr [ebp-144h],esi
f8492e84 c785c4feffff00020000 mov dword ptr [ebp-13Ch],200h
f8492e8e 899dc0feffff    mov     dword ptr [ebp-140h],ebx
f8492e94 89b5c8feffff    mov     dword ptr [ebp-138h],esi
f8492e9a 89b5ccfeffff    mov     dword ptr [ebp-134h],esi
f8492ea0 e8296bffff      call    sr!IoCreateFileSpecifyDeviceObjectHint (f84899ce)
f8492ea5 3bc6            cmp     eax,esi
f8492ea7 8985e8feffff    mov     dword ptr [ebp-118h],eax
f8492ead 7d08            jge     sr!SrpExpandPathOfFileName+0x123 (f8492eb7)

sr!SrpExpandPathOfFileName+0x11b:
f8492eaf c60701          mov     byte ptr [edi],1
f8492eb2 e9c0010000      jmp     sr!SrpExpandPathOfFileName+0x2e3 (f8493077)

sr!SrpExpandPathOfFileName+0x123:
f8492eb7 56              push    esi
f8492eb8 8d85dcfeffff    lea     eax,[ebp-124h]
f8492ebe 50              push    eax
f8492ebf a1389b48f8      mov     eax,dword ptr [sr!_imp__IoFileObjectType (f8489b38)]
f8492ec4 56              push    esi
f8492ec5 ff30            push    dword ptr [eax]
f8492ec7 56              push    esi
f8492ec8 ffb5d8feffff    push    dword ptr [ebp-128h]
f8492ece ff15349b48f8    call    dword ptr [sr!_imp__ObReferenceObjectByHandle (f8489b34)]
f8492ed4 3bc6            cmp     eax,esi
f8492ed6 8985e8feffff    mov     dword ptr [ebp-118h],eax
f8492edc 0f8c95010000    jl      sr!SrpExpandPathOfFileName+0x2e3 (f8493077)

sr!SrpExpandPathOfFileName+0x14e:
f8492ee2 8b85e0feffff    mov     eax,dword ptr [ebp-120h]
f8492ee8 ff7048          push    dword ptr [eax+48h]
f8492eeb ff15149c48f8    call    dword ptr [sr!_imp__IoGetAttachedDevice (f8489c14)]
f8492ef1 ffb5dcfeffff    push    dword ptr [ebp-124h]
f8492ef7 8bf8            mov     edi,eax
f8492ef9 ff15309b48f8    call    dword ptr [sr!_imp__IoGetRelatedDeviceObject (f8489b30)]
f8492eff 3bc7            cmp     eax,edi
f8492f01 7418            je      sr!SrpExpandPathOfFileName+0x187 (f8492f1b)

sr!SrpExpandPathOfFileName+0x16f:
f8492f03 8b85d0feffff    mov     eax,dword ptr [ebp-130h]
f8492f09 c785e8feffffd40000c0 mov dword ptr [ebp-118h],0C00000D4h
f8492f13 c60001          mov     byte ptr [eax],1
f8492f16 e95c010000      jmp     sr!SrpExpandPathOfFileName+0x2e3 (f8493077)

sr!SrpExpandPathOfFileName+0x187:
f8492f1b 8d85ecfeffff    lea     eax,[ebp-114h]
f8492f21 50              push    eax
f8492f22 ffb5dcfeffff    push    dword ptr [ebp-124h]
f8492f28 ffb5e0feffff    push    dword ptr [ebp-120h]
f8492f2e e8c7f8ffff      call    sr!SrpGetFileName (f84927fa)
…
So looking at the function we can see that SR is calling IoCreateFileSpecifyDeviceObjectHint and then gets the FILE_OBJECT associated with the handle it has (by calling ObReferenceObjectByHandle). From this call we can infer that the handle is stored in the local variable @ebp-128h and that the FILE_OBJECT is stored in the variable @ebp-124h. Then SR compares the device of the FILE_OBJECT with the device it is attached to and if they don't match it fails with status 0C00000D4h (STATUS_NOT_SAME_DEVICE; also please note that the status gets put into @ebp-118h). Then the function calls SrpGetFileName with the FILE_OBJECT it got from the call to IoCreateFileSpecifyDeviceObjectHint. Since all these are stored in local variables we can get the value of EBP for that function from the ChildEBP column (0xf511f270) and see which FILE_OBJECT they got:
1: kd> !error 0C00000D4h
Error code: (NTSTATUS) 0xc00000d4 (3221225684) - {Incorrect Volume}  The target file of a rename request is located on a different device than the source of the rename request.
1: kd> dp f511f270-0x128
f511f148  800005f4 82035578 81d1fc10 0000001e
f511f158  00000000 00fe0000 f511f16e 000000fe
f511f168  00000000 01000000 f511f0a8 f511f858
f511f178  f511f32c f83ea75b e113f858 ffffffff
f511f188  f83e5b5c e1cbada0 f511f858 81de4290
f511f198  00000000 00000000 f511f118 f511f1bc
f511f1a8  f511f32c f83ea75b f511f1c8 f511f2dc
f511f1b8  804e1ec4 f511f1fc f511f1d8 81cfb850
1: kd> !handle 800005f4 

PROCESS 81f43020  SessionId: 0  Cid: 0714    Peb: 7ffde000  ParentCid: 03ec
    DirBase: 02b40320  ObjectTable: e1518008  HandleCount:  32.
    Image: ifstest.exe

Kernel handle table at e1004000 with 345 entries in use

800005f4: Object: 82035578  GrantedAccess: 00100000 Entry: e1004be8
Object: 82035578  Type: (823aead0) File
    ObjectHeader: 82035560 (old version)
        HandleCount: 1  PointerCount: 2
        Directory Object: 00000000  Name: \opcreatg\ {VhdDisk00000003}
So as we can see SR actually got one my SFOs by sending a create to the top of the stack and then sent it directly to NTFS in a query. This was puzzling because SR is actually using IoCreateFileSpecifyDeviceObjectHint which is exactly what I expected it would use to target the IRP_MJ_CREATE appropriately. However, when looking at the call we can see that the DeviceObject member is passed in as "push esi" and it's hard to track exactly what a given register's value was at the time of the call without carefully analyzing the code. In this case however it seems we got lucky because a lot of other parameters are set up using the "push esi" instruction which means that either SR called IoCreateFileSpecifyDeviceObjectHint with a lot of the parameters set to the DEVICE_OBJECT or that it passed in NULL for the DEVICE_OBJECT. Looking at where ESI is initialized (which is not in the chunk of code I pasted here) we can see that indeed ESI is set to 0 and so now we know what is going on:
  1. SR calls IoCreateFileSpecifyDeviceObjectHint and sends a request to the top of the stack.
  2. SR takes the handle and resolves it to a FILE_OBJECT
  3. SR compares the DEVICE_OBJECT for the FILE_OBJECT it just got with the DEVICE_OBJECT it is attached to and if they are different it fails with STATUS_NOT_SAME_DEVICE.
  4. Finally SR uses the FILE_OBJECT in a call that is targeted below itself and thus my SFO reaches NTFS.
I'm not exactly sure why SR does this. It looks like the code was written so that it used IoCreateFileSpecifyDeviceObjectHint but then the DeviceObject was passed in as NULL which effectively changes it to IoCreateFile. But then the code itself checks whether the device for the FILE_OBJECT it gets is the same as the one it's attached on, which is something IoCreateFileSpecifyDeviceObjectHint would have done if used with a DeviceObject parameter. Anyway the problem is that SR sent requests to two different points on the stack, the IRP_MJ_CREATE to the top of the stack and then subsequent requests below itself on the stack and thus it runs into trouble when things change between the top of the stack and the altitude where SR is located. Had SR sent the IRP_MJ_CREATE below itself (which is the right behavior from a layering perspective) or the subsequent requests to the top of the stack (which is actually still wrong because it could lead to infinite loops and such) then it would have avoided this problem.

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.

Thursday, November 10, 2011

Filters And IRP_MJ_QUERY_INFORMATION


IRP_MJ_QUERY_INFORMATION is a request that file system filters must interact with quite frequently, either to process it or to issue a query to get some information from the underlying file system. The semantics are fairly simple and fairly well documented but still there are some implementation details that might make things interesting for a filter.
Looking at the documentation for IRP_MJ_QUERY_INFORMATION the following phrase stands out:


Although the FileAccessInformation, FileAlignmentInformation, and FileModeInformation information types can also be passed as a parameter to ZwQueryInformationFile, this information is file-system-independent. Thus ZwQueryInformationFile supplies this information directly, without sending an IRP_MJ_QUERY_INFORMATION request to the file system.


What this means is that the IO manager can extract the information from some other place, and considering this information can be requested on a per-handle basis, it's pretty clear that the information must come from the FILE_OBJECT or the handle information that the IO manager keeps in its handle tables. And indeed, if we look at the information classes that are singled out (FileAccessInformation, FileAlignmentInformation, and FileModeInformation) we can see where the information might come from in each case:

  • FileAccessInformation - the access rights for each handle are managed by the IO manager internally and they are not visible on the FILE_OBJECT or even stored in the file system. So even if the IO manager were to send an IRP, the file system itself wouldn't be able to answer the request because it just doesn't have that information. I should mentioned that this isn't necessarily true for remote file systems since the remote file system must perform access checks for the requests it receives over the wire anyway.
  • FileAlignmentInformation - this alignment is not something required by the file system anyway. This is related to the storage device on top of which the file system is mounted and so the IO manager could get it by querying the storage device. However, that's not really necessary since each DEVICE_OBJECT has an AlignmentRequirement member (again, this might not be true for remote file systems).
  • FileModeInformation - this information comes from the FILE_OBJECT and it's pretty transparent how it maps to the various FILE_OBJECT flags.

Frankly I expected to see another information class on the list, the FilePositionInformation. I thought the current pointer is maintained in the FILE_OBJECT->CurrentByteOffset and so the IO manager could just get it from there and not bother to send a request into the file system (after all the information must be stored on a per-FILE_OBJECT basis and so it can't be in the SCB or anything like that).
Anyway, it's interesting to see what the FastFat file system does for these requests. So looking at \src\filesys\fastfat\Win7\fileinfo.c we can see that for the three information classes mentioned in the documentation the IRP would be failed with STATUS_INVALID_PARAMETER. I was also curious to see what FastFat does for FilePositionInformation and while FastFat doesn't fail the request, it does what I thought it would do, which is to return the FILE_OBJECT->CurrentByteOffset value.
So far it's all pretty clear and it's not really problematic for filters since in most cases they don't really care about these information classes anyway and there is no chance to get an IRP_MJ_QUERY_INFORMATION request for any of them from the IO manager. However, there is a twist here. The FileAllInformation class includes all the four information classes mentioned above (FileAccessInformation, FileAlignmentInformation, FileModeInformation and FilePositionInformation) and is actually sent in the form of an IRP. So how does the file system get that information ?
Looking again at the FastFat implementation we can see the code fragment that is used to implement the FileAllInformation call:
            case FileAllInformation:

                //
                //  For the all information class we'll typecast a local
                //  pointer to the output buffer and then call the
                //  individual routines to fill in the buffer.
                //

                AllInfo = Buffer;
                Length -= (sizeof(FILE_ACCESS_INFORMATION)
                           + sizeof(FILE_MODE_INFORMATION)
                           + sizeof(FILE_ALIGNMENT_INFORMATION));

                FatQueryBasicInfo( IrpContext, Fcb, FileObject, &AllInfo->BasicInformation, &Length );
                FatQueryStandardInfo( IrpContext, Fcb, &AllInfo->StandardInformation, &Length );
                FatQueryInternalInfo( IrpContext, Fcb, &AllInfo->InternalInformation, &Length );
                FatQueryEaInfo( IrpContext, Fcb, &AllInfo->EaInformation, &Length );
                FatQueryPositionInfo( IrpContext, FileObject, &AllInfo->PositionInformation, &Length );
                FatQueryNameInfo( IrpContext, Fcb, Ccb, &AllInfo->NameInformation, &Length );

                break;
So as we can see FastFat doesn't even attempt to return the data for those information classes mentioned in the documentation. So how are they populated ? My first guess was that the IO manager populates them after the request completes and before returning the buffer to the caller. But when I tried to validate my assumption by setting a write breakpoint on the location in the buffer where the FILE_ACCESS_INFORMATION structure is the breakpoint never got hit in the path I expected it to.. After some more investigation I realized that by the time my filter got the request, the FILE_ACCESS_INFORMATION was already populated:

1: kd> kn
 # ChildEBP RetAddr  
00 a625eb6c 96016aeb myfilter!PreQueryInformation+0x29c
01 a625ebd8 960199f0 fltmgr!FltpPerformPreCallbacks+0x34d
02 a625ebf0 96019f01 fltmgr!FltpPassThroughInternal+0x40
03 a625ec14 9601a3ba fltmgr!FltpPassThrough+0x203
04 a625ec44 828884bc fltmgr!FltpDispatch+0xb4
05 a625ec5c 82aa8f24 nt!IofCallDriver+0x63
06 a625ed18 8288f44a nt!NtQueryInformationFile+0x779

1: kd> ?? ((PFILE_ALL_INFORMATION)Data->Iopb->Parameters.QueryFileInformation.InfoBuffer)->AccessInformation
struct _FILE_ACCESS_INFORMATION
   +0x000 AccessFlags      : 0x120089
So the way NtQueryInformationFile works for FileAllInformation is by populating the buffer with the information it has access to before sending it to the file system and then the file system fills in the rest. With this in mind there are a couple of things that filters must be careful about:

  • When processing a FileAllInformation request the filter must be careful not to overwrite the information that was already written by the IO manager. So don't call RtlZeroMemory() for that buffer or reuse it for some other purpose. Also, if completing an FileAllInformation query from assembling bits from some other sources (other queries into an underlying file system or some such) the filter must be careful about how it copies the data into the user's buffer. I've see cases where in response to a FileAllInformation request the filter allocated its own buffer, sent its request using FltQueryInformationFile() and then copied the resulting buffer over the user's buffer and that is broken. This is because:
  • FltQueryInformationFile() is not meant to be identical to ZwQueryInformationFile(). It is simply a wrapper over allocating an IRP and sending the request to the file system, so some (all ?) of the requests that would be completed by the IO manager without sending an IRP will just fail for FltQueryInformationFile().
  • Filters that implement more of the file system functionality need to behave more like a file system so for example a filter that owns its own FILE_OBJECTs must make sure to keep the CurrentByteOffset updated since the FilePositionInformation request might be completed above them by the IO manager or some other filter that will simply look in the FILE_OBJECT.
Finally, I wanted to mention one particular documentation page on MSDN that I find very useful when dealing with Information classes, the page for FileInformation Classes. I have a hard time remembering which ones are only for set and which ones are query-only and which are both and how they are handled and this page helps a lot. Please note however that this page is written with remote file systems in mind and so some of the information isn't exactly the same for local file systems. Still I find it quite useful whenever I have to deal with this topic.

Thursday, November 3, 2011

Byte Range Locks and IRP_MJ_CLEANUP


Byte range locks are not a complicated concept but there are some interesting implementation details that might make life hard for a filter. I ran into this a couple of days ago when I was tracking down some IFS tests failures related to locking (in particular the UnlockRangeOnCloseTest test from the FileLocking group).
Byte range locks are documented fairly well, at least when compared with other concepts. There is the Lock 'Em Up - Byte Range Locking OSR article and an MSDN page on Locking and Unlocking Byte Ranges in Files. However, for this discussion, the relevant feature is described in the user mode API for locking files, LockFileEx(). This is the quote:


If a process terminates with a portion of a file locked or closes a file that has outstanding locks, the locks are unlocked by the operating system. However, the time it takes for the operating system to unlock these locks depends upon available system resources. Therefore, it is recommended that your process explicitly unlock all files it has locked when it terminates. If this is not done, access to these files may be denied if the operating system has not yet unlocked them.


So what this means is that a process doesn't necessarily have to release all its locks on a file before closing the handle it has and the OS will release all the locks on its behalf (though this is not the recommended way of doing things). There is an interesting aspect here that is worth noting. In fact, whenever the documentation says that something happens automatically for a handle when its closed I immediately think about what happens about handles in different processes that point to the same object. For example, what happens when a file is opened with handle A (HA) in process A and then process A creates process B in such a way that process B inherits the handle from process A (HB). Both HA and HB point to the same FILE_OBJECT and when the first handle is closed nothing particularly interesting happens for the file system (the IRP_MJ_CLEANUP only gets sent when the last handle to a FILE_OBJECT is closed). For the rest of this post let's assume that HA is closed first and then HB is closed and the closing of the HB handle is the one that prompts the IO manager to send the IRP_MJ_CLEANUP call.
So now let's look at what happens in FastFat to handle this case. Looking at the code that processes IRP_MJ_CLEANUP (in \src\filesys\fastfat\Win7\cleanup.c) we find this block of code:
            //
            //  Unlock all outstanding file locks.
            //

            (VOID) FsRtlFastUnlockAll( &Fcb->Specific.Fcb.FileLock,
                                       FileObject,
                                       IoGetRequestorProcess( Irp ),
                                       NULL );
There are two interesting things to note about this call.

  • First we can see that a process is passed in (and this is the process associated with the IRP which FastFat gets from IoGetRequestorProcess()). Moreover, the process is a mandatory parameter, as we can see from the declaration for FsRtlFastUnlockAll():
    NTSTATUS FsRtlFastUnlockAll(
      __in      PFILE_LOCK FileLock,
      __in      PFILE_OBJECT FileObject,
      __in      PEPROCESS ProcessId,
      __in_opt  PVOID Context
    );
    
    The documentation clearly states that the locks that are released are specific to a process and so during IRP_MJ_CLEANUP FastFat will automatically close the handles associated with the handle on which the IRP_MJ_CLEANUP call came. For our example, handle HB. But what about the locks acquired on handle HA ? Are they going to be left behind ?
  • The second interesting thing to note is that the FILE_LOCK structure is a private member of the FCB, not part of the FSRTL_ADVANCED_FCB_HEADER. So the IO manager can't know where that structure is located without specific knowledge about each file system and as such it can't call FsRtlFastUnlockAll by itself.

Searching for FsRtlFastUnlockAll() in the FastFat source we find that there is another place where it is called, in the FatFastUnlockAll() function (in \src\filesys\fastfat\Win7\lockctrl.c). As the name suggests, FatFastUnlockAll() is a fast IO callback for FastFat and it really doesn't do much else than release all the byte range locks associated with the calling process. This looks like a good mechanism to have the IO manager call the file system to instruct it to release all the locks when a handle is closed. However, there was still one puzzling aspect. FastIO is supposed to be optional so what happens if a filter fails the FastIO or a file system doesn't implement it at all ? I expected there would be an IRP equivalent for this FastIO but there is no other place in the code where FsRtlFastUnlockAll() is called. Well, in fact there is an IRP equivalent for the FastIO but it is not explicitly processed by the FastFat file system. Instead all the lock processing associated with the IRP_MJ_LOCK_CONTROL IRP is handled inside FatCommonLockControl(), which simply calls FsRtlProcessFileLock() and lets the FsRtl package handle it.
Finally, now that we know how the IO manager calls the file system to tell it to release the locks associated with a process, there is one more twist. Does the IO manager call an unlock all every time a handle is closed ? Or, if not, how does it know when to do it ? Clearly it doesn't need to do it for the last handle (since the file system's IRP_MJ_CLEANUP routine will do it) but what about the other handles ? It turns out that there is an optimization here. Whenever the IO manager issues a byte range lock request to the file system it sets the FILE_OBJECT->LockOperation boolean to TRUE. Then, whenever it is closing a handle, if FILE_OBJECT->LockOperation is set it knows that it must notify the file system to release any potential locks. Please note that this flag appears to never be cleared (i.e. even if a process locks and then unlocks all the ranges so that there are no locks to release when closing the handle) so don't be surprised if you receive this in your filter even when there are no locked ranges.
So to summarize things, this is the logic involved here:

  • On every lock operation the IO manager sets FILE_OBJECT->LockOperation. It is worth mentioning that LockOperation is never actually used by the file system (at least not that I've seen in any file system I've looked at).
  • When a handle is closed, if the FILE_OBJECT->LockOperation is set then the IO manager knows there were some locks taken on the FILE_OBJECT and so it must release them. So the IO manager will issue the IRP_MJ_LOCK_CONTROL IRP with the IRP_MN_UNLOCK_ALL minor function (or it will call the FastIO equivalent) to tell the file system to release all the locks. However, this is not necessary if this is the last handle for the FILE_OBJECT because the IO manager will issue the IRP_MJ_CLEANUP IRP in that case and the file system will release all the locks for that process anyway.
  • When a file system processes the IRP_MJ_CLEANUP IRP must also release all the byte range locks for the FILE_OBJECT for that process.

Ok, so now let's look at some of the problems that filters might introduce or might run into:

  • A filter that acquires locks on a FILE_OBJECT without going through the IO manager (i.e. without calling ZwLockFile() but by issuing their own IO (IRP or FLT_CALLBACK_DATA)) should also set the FILE_OBJECT->LockOperation flag so that the IO manager knows locks have been taken on that file because otherwise it'll be really complicated to release the locks at the right time.
  • A filter that duplicates a handle for a FILE_OBJECT might also change the behavior a bit depending on when it closes the handle. If for example if closes the handle after the user has closed his handle then the IRP_MJ_CLEANUP IRP will be sent for their close and not the user's close. Now, the IO manager should handle this properly and frankly I don't see any problem with it off the top of my head, but it's something to keep in mind.
  • When a filter calls ZwClose (or FltClose) for a handle they've opened the IoGetRequestorProcess() call for the IRP_MJ_CLEANUP IRP will return the system process, so the file system will release all byte range locks on the FILE_OBJECT in the system process. This might be broken if, for example, there are two handles, H1 and H2 for the same FILE_OBJECT in the system process and a lock was taken on handle H1 but then the filter closes H2 and the IO manager finds FILE_OBJECT->LockOperation set and it tells the file system to release all the locks in the system process for that FILE_OBJECT and thus it releases the byte range lock that H1 had.
  • Also, there are some filters that open their own handles to certain files and then they forward some requests that arrive on other files to the files they've opened (for example some back-up filter might forward all IRP_MJ_WRITE for each file (foo.txt) requests to another file (foo.txt.bak)). Also Shadow File Object type filters will often exhibit the same behavior. Now, if they ever forward a byte range lock request to the file they've opened (by doing something like changing the TargetFileObject) then when they close their file that close will most likely not be in the same process as the process that requested the byte range lock originally and so some ranges of the file they've opened might remain locked. In this case the filter might need to call IRP_MJ_LOCK_CONTROL with IRP_MN_UNLOCK_ALL itself from the process context where the forwarded lock request originated.

Finally, there is one more thing I'd like to say. There are no Flt functions equivalents for ZwLockFile or ZwUnlockFile. A filter that wants to lock files on the file system below must issue their own requests. However, there are some Flt special functions for byte range locks (like FltProcessFileLock()) but they are meant for filters that implement byte range locks for some FILE_OBJECTs (like a file system would). For example FltProcessFileLock() should be called where a file system would call the FsRtlProcessFileLock() function. However, since the FsRtlProcessFileLock() requires an IRP parameter FltMgr had to implement a wrapper function that takes a FLT_CALLBACK_DATA structure instead of that IRP. This is not the case for all the FsRtlXxxLock() functions because not all of them take an IRP parameter (for example FsRtlFastUnlockAll() doesn't take an IRP and there is no Flt equivalent and instead a filter that implements file locks simply calls FsRtlFastUnlockAll() directly). Basically a filter that implements file locks must mix calls to FsRtl functions with calls to Flt functions.

Thursday, October 27, 2011

TargetInstance Redirection Problems for FastIO on WinXP

Frankly I don't expect this is a problem that many people will run into but I'd like to show some of the debugging that led me to figure out the problem and what the implications are. I've already explained how using TargetInstance might help filters and some of the issues associated with it in my post on File IO Redirection Between Volumes Using FltMgr and I also have a post on Handling IRP_MJ_NETWORK_QUERY_OPEN in a Minifilter and I encourage you to revisit those posts if you need a refresher. In the post on handling IRP_MJ_NETWORK_QUERY_OPEN my suggestion was to return STATUS_FLT_DISALLOW_FAST_IO if you don't want to deal with all the weird semantics it introduces. However, there is a small performance overhead associated with failing IRP_MJ_NETWORK_QUERY_OPEN in this manner so while I was chasing down some performance issues I decided to actually implement this path. The filter I was working on was a pretty classic design, returning STATUS_REPARSE to redirect IRP_MJ_CREATEs to a different volume. Also, let's use the simplifying assumption that the file name was exactly the same between the two volumes. This meant that in IRP_MJ_NETWORK_QUERY_OPEN I should be able to just redirect the request to a different volume by changing the TargetInstance to the instance associated with the other volume and the request would then follow down that path and get the attributes for the file on the other volume. And since there is no handle open as a result of this operation I didn't have to worry about subsequent operations and such.
I'll post some pseudocode because there is just too much infrastructure to set things up properly in the passthrough sample. There is an instance context that I use to figure out if we need to redirect requests and where to redirect them (if there is no context attached then I don't redirect anything):
typedef struct _MY_INSTANCE_CONTEXT {

    PFLT_INSTANCE InstanceToRedirectTo;

} MY_INSTANCE_CONTEXT, *PMY_INSTANCE_CONTEXT;
And the following piece of code I've added to PtPreOperationPassThrough:
        if (!NT_SUCCESS(status)) {

            PT_DBG_PRINT( PTDBG_TRACE_OPERATION_STATUS,
                          ("PassThrough!PtPreOperationPassThrough: FltRequestOperationStatusCallback Failed, status=%08x\n",
                           status) );
        }
    }

    if (Data->Iopb->MajorFunction == IRP_MJ_NETWORK_QUERY_OPEN) {

        status = FltGetInstanceContext( FltObjects->Instance, &instanceContext );

        if (NT_SUCCESS(status)) {

            //
            // send this request to the instance we want it to go to and we must
            // mark the FLT_CALLBACK_DATA dirty.
            //

            Data->Iopb->TargetInstance = instanceContext->InstanceToRedirectTo;

            FltSetCallbackDataDirty( Data );

            //
            // we'll release this in the postOp callback.
            //
            
            *CompletionContext = (PVOID)instanceContext;
        } else {

            if (status == STATUS_NOT_FOUND) {

                //
                // this isn't an instance for which we want to redirect this 
                // operation, send the request down and don't care about
                // the postOp Callback.
                //

                return FLT_PREOP_SUCCESS_NO_CALLBACK;

            } else {

                //
                // some other error. we can either fail the request here or
                // we can just return STATUS_FLT_DISALLOW_FAST_IO and we'll
                // get another shot at it on the IRP_MJ_CREATE path. 
                //

                return STATUS_FLT_DISALLOW_FAST_IO ;
            }
        }
    }

    return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}
Again, this is very simplified to only show how to set the TargetInstance but there are a couple of things I'd like to point out. Because of how FastIO works (each driver calls the next driver passing parameters on the stack) FastIO doesn't have the problem described in my post on File IO Redirection Between Volumes Using FltMgr because there is no IRP and there are no IO_STACK_LOCATIONs (it is possible though to run out of thread stack but that can also be worked around). Also, in terms of referencing, please note that I'm keeping a reference to the instance context from preOp to postOp callback which in turn keeps the instance pointed by instanceContext->InstanceToRedirectTo around (though of course there are multiple different ways to achieve the same result).
So anyway, the code I have works fine in Win7 (after I disabled LUAFV because LUAFV always fails IRP_MJ_NETWORK_QUERY_OPEN with IRP_MJ_NETWORK_QUERY_OPEN; if you're wondering why I went through all the trouble because LUAFV will be running on all Vista and Win7 machines anyway then let me remind you that server SKUs don't have LUAFV running so there are machines out there running the Win7 kernel without LUAFV in the picture so my code might actually help them; also as you expect performance is a much bigger concern for servers). However, on WinXP SP3 I kept getting STATUS_OBJECT_NAME_NOT_FOUND (and the other statuses that indicate that the file isn't there) but the file was definitely present. Having tested that Win7 worked I started to wonder whether there was something different in WinXP that I needed to worry about. So I decided to see whether the request makes it to the right volume after all:
1: kd> kn L5 
 # ChildEBP RetAddr  
00 f53f293c f8477888 myfilter!PreNetworkQueryOpen // this is my preOp callback
01 f53f299c f84791a7 fltMgr!FltpPerformPreCallbacks+0x2d4 // this calls the preOp callbacks
02 f53f29b4 f8485c7a fltMgr!FltpPassThroughFastIo+0x3b // this is FLtMgr's function to process FastIO operations
03 f53f29f8 f83d6f70 fltMgr!FltpFastIoQueryOpen+0xf4 // FltMgr's FastIO callback for this operation
04 f53f2a18 805830fe sr!SrFastIoQueryOpen+0x40 // SR is issuing the request
1: kd> ?? Data // we need the address of the FLT_CALLBACK_DATA
struct _FLT_CALLBACK_DATA * 0x81b49684
   +0x000 Flags            : 2
...
1: kd> dt 0x81b49684 fltmgr!_FLT_CALLBACK_DATA Iopb->TargetInstance // See what is the instance the request was originally going to 
   +0x008 Iopb                 : 
      +0x00c TargetInstance       : 0x820d3008 _FLT_INSTANCE
1: kd> dt 0x820d3008 fltmgr!_FLT_INSTANCE Volume // get the volume from the instance 
   +0x018 Volume : 0x8237e5c0 _FLT_VOLUME
1: kd> dt  0x8237e5c0 fltmgr!_FLT_VOLUME DeviceObject // get FltMgr's DEVICE_OBJECT from the volume
   +0x01c DeviceObject : 0x823dac70 _DEVICE_OBJECT
1: kd> !devstack 0x823dac70 // see what's the bottom DEVICE_OBJECT for this volume. 
  !DevObj   !DrvObj            !DevExt   ObjectName
  823637a8  \FileSystem\sr     82363860  
> 823dac70  \FileSystem\FltMgr 823dad28  
  822fe020  \FileSystem\Ntfs   822fe0d8  // so we have NTFS on the bottom
1: kd> bp /t @$thread f8477888 // ok, now let's step out of my preOp callback on this thread and see what we change the instance to 
1: kd> bl
 0 e f8477888     0001 (0001) fltMgr!FltpPerformPreCallbacks+0x2d4
     Match thread data 81a3cbe8

1: kd> g
Breakpoint 0 hit
fltMgr!FltpPerformPreCallbacks+0x2d4:
f8477888 83f802          cmp     eax,2
1: kd> bc 0
1: kd> dt 0x81b49684 fltmgr!_FLT_CALLBACK_DATA Iopb->TargetInstance // it's the same FLT_CALLBACK_DATA but the instance should be different
   +0x008 Iopb                 : 
      +0x00c TargetInstance       : 0x820d9008 _FLT_INSTANCE
1: kd> dt  0x820d9008 fltmgr!_FLT_INSTANCE Volume // get the volume for the new instance
   +0x018 Volume : 0x820ebae0 _FLT_VOLUME
1: kd> dt 0x820ebae0 fltmgr!_FLT_VOLUME DeviceObject // get the DEVICE_OBJECT for the volume
   +0x01c DeviceObject : 0x820ebee8 _DEVICE_OBJECT
1: kd> !devstack 0x820ebee8 // see what's the bottom DEVICE_OBJECT… again, NTFS… 
  !DevObj   !DrvObj            !DevExt   ObjectName
  820eb020  \FileSystem\sr     820eb0d8  
> 820ebee8  \FileSystem\FltMgr 820ebfa0  
  820ea020  \FileSystem\Ntfs   820ea0d8  
1: kd> bp /t @$thread Ntfs!NtfsNetworkOpenCreate // ok, put a break on NTFS's function that processes this FastIO on this thread 
1: kd> g
Breakpoint 0 hit
Ntfs!NtfsNetworkOpenCreate:
f834ffb8 6878010000      push    178h
1: kd> bc 0
1: kd> kb L5 // show us the stack with parameters so we can see which device the request was actually sent to.
ChildEBP RetAddr  Args to Child              
f53f2968 f84790e8 81a6c380 f53f2c00 822fe020 Ntfs!NtfsNetworkOpenCreate // what do you know, it's the original DEVICE_OBJECT: 822fe020
f53f2988 f84791e4 000000f2 00000000 81b496c0 fltMgr!FltpPerformFastIoCall+0x300
f53f29b4 f8485c7a 003f29d8 823637a8 81a6c510 fltMgr!FltpPassThroughFastIo+0x78
f53f29f8 f83d6f70 81a6c380 f53f2c00 823dac70 fltMgr!FltpFastIoQueryOpen+0xf4
f53f2a18 805830fe 81a6c380 f53f2c00 823637a8 sr!SrFastIoQueryOpen+0x40
So what I did was to get the FLT_CALLBACK_DATA at the beginning of my callback and from that extract the file system's DEVICE_OBJECT on which the original request was sent. Then I let my callback run and I checked what the new stack instance was and got the file system's DEVICE_OBJECT on that stack. Then I simply let the request go until it hit the file system (NTFS on both volumes in this case) and then on the stack I can see which DEVICE_OBJECT the request was actually sent to. And, as I suspected, the request was sent on the original DEVICE_OBJECT and not the DEVICE_OBJECT for the instance I switched to. But why ? What should I have changed to make the request go where I wanted ? With some stepping through the code and reading a bunch of assembly I got to this part:
1: kd> u fltMgr!FltpPassThroughFastIo+0x55 L0xE
fltMgr!FltpPassThroughFastIo+0x55:
f84791c1 8b0f            mov     ecx,dword ptr [edi] // what is EDI
f84791c3 8b4664          mov     eax,dword ptr [esi+64h] // what is ESI ?
f84791c6 8d5e68          lea     ebx,[esi+68h]
f84791c9 53              push    ebx
f84791ca ff711c          push    dword ptr [ecx+1Ch]
f84791cd 8d4810          lea     ecx,[eax+10h]
f84791d0 ff7640          push    dword ptr [esi+40h]
f84791d3 51              push    ecx
f84791d4 33c9            xor     ecx,ecx
f84791d6 8a4805          mov     cl,byte ptr [eax+5]
f84791d9 0fb64004        movzx   eax,byte ptr [eax+4]
f84791dd 51              push    ecx
f84791de 50              push    eax
f84791df e804fcffff      call    fltMgr!FltpPerformFastIoCall (f8478de8)
1: kd> !pool @esi 2
Pool page 81b49628 region is Nonpaged pool
*81b49620 size:  108 previous size:   18  (Allocated) *FMic
  Pooltag FMic : IRP_CTRL structure, Binary : fltmgr.sys
1: kd> r @edi
edi=f53f29d8 // this is an address on the current stack
1: kd> dp f53f29d8
f53f29d8  8237e5c0 00000000 81b49628 ffffffff // so this structure has a pointer to the FLT_VOLUME and IRP_CTRL.. Must be the IRP_CALL_CTRL
f53f29e8  00000000 00000000 000001b4 0000493e
1: kd> dt @edi fltmgr!_IRP_CALL_CTRL
   +0x000 Volume           : 0x8237e5c0 _FLT_VOLUME
   +0x004 Irp              : (null) 
   +0x008 IrpCtrl          : 0x81b49628 _IRP_CTRL
   +0x00c StartingCallbackNode : 0xffffffff _CALLBACK_NODE
   +0x010 OperationStatusCallbackListHead : _SINGLE_LIST_ENTRY
   +0x014 Flags            : 0 (No matching name)
1: kd> dt fltmgr!_FLT_VOLUME
   +0x000 Base             : _FLT_OBJECT
   +0x014 Flags            : _FLT_VOLUME_FLAGS
   +0x018 FileSystemType   : _FLT_FILESYSTEM_TYPE
   +0x01c DeviceObject     : Ptr32 _DEVICE_OBJECT
….
So as you can see it looks like FltMgr picks the DEVICE_OBJECT from the IRP_CALL_CTRL->Volume structure. Let's see what happens in Win7:
0: kd> u fltmgr!FltpPassThroughFastIo+0x5a L0xD
fltmgr!FltpPassThroughFastIo+0x5a:
96019198 8b4668          mov     eax,dword ptr [esi+68h] // offset 0x68 where we had 0x64 in XP
9601919b 8d5e6c          lea     ebx,[esi+6Ch] // offset 0x6C where we had 0x68 in XP… did the IRP_CTRL change ?
9601919e 832300          and     dword ptr [ebx],0
960191a1 53              push    ebx
960191a2 ff763c          push    dword ptr [esi+3Ch] // and then there is a push for IRP_CTRL+0x3c instead of IRP_CALL_CTRL->Volume+0x1c..
960191a5 8d4810          lea     ecx,[eax+10h]
960191a8 ff7640          push    dword ptr [esi+40h]
960191ab 51              push    ecx
960191ac 0fb64805        movzx   ecx,byte ptr [eax+5]
960191b0 0fb64004        movzx   eax,byte ptr [eax+4]
960191b4 51              push    ecx
960191b5 50              push    eax
960191b6 e803fcffff      call    fltmgr!FltpPerformFastIoCall (96018dbe)
1: kd> dt fltmgr!_IRP_CTRL
   +0x000 Type             : _FLT_TYPE
   +0x004 Flags            : _IRP_CTRL_FLAGS
   +0x008 MajorFunction    : UChar
   +0x009 Reserved0        : UChar
   +0x00a CompletionStackLength : UChar
   +0x00b NextCompletion   : UChar
   +0x00c CompletionStack  : Ptr32 _COMPLETION_NODE
   +0x010 SyncEvent        : _KEVENT
   +0x020 Irp              : Ptr32 _IRP
   +0x020 FsFilterData     : Ptr32 _FS_FILTER_CALLBACK_DATA
   +0x024 AsyncCompletionRoutine : Ptr32     void 
   +0x028 AsyncCompletionContext : Ptr32 Void
   +0x02c InitiatingInstance : Ptr32 _FLT_INSTANCE
   +0x030 PendingCallbackNode : Ptr32 _CALLBACK_NODE
   +0x030 StartingCallbackNode : Ptr32 _CALLBACK_NODE
   +0x034 preOp            : __unnamed
   +0x034 postOp           : __unnamed
   +0x038 PostCompletionRoutine : Ptr32     void 
   +0x03c DeviceObject     : Ptr32 _DEVICE_OBJECT // so we get the DEVICE_OBJECT from the IRP_CTRL
...
Ok, so what's going on is that in Win7 it looks like the DEVICE_OBJECT is taken from the IRP_CTRL (which is the internal FltMgr structure that hosts the FLT_CALLBACK_DATA), which makes sense since we change the TargetInstance in the FLT_CALLBACK_DATA. In XP the DEVICE_OBJECT is taken from the IRP_CALL_CTRL->Volume and I haven't been able to find any code path that updates the IRP_CALL_CTRL. So based on this I've decided that this is an XP bug and that I can't really work around it for WinXP (since there is no way to update the FLT_VOLUME inside the IRP_CALL_CTRL which are both undocumented btw..). So I've updated my code so that in WinXP it always returns STATUS_FLT_DISALLOW_FAST_IO.
Finally, there is one more aspect to discuss. It looks like FltpPassThroughFastIo is a generic handler for all FastIO routines and as such this problem might actually be affecting all FastIO in WinXP and not only IRP_MJ_NETWORK_QUERY_OPEN, so if you see that TargetInstance redirection isn't working then it might be this issue.

Thursday, October 20, 2011

Testing a Minifilter on More Filesystems: UDF and ExFAT

In this post I want to show a neat trick that allows testing of filters on other file systems with very little overhead. This is important because very often file system filters end up very dependent on the semantics of some specific file system only because it's very easy to test on just that one. For example, most filters are tested with NTFS and occasionally FAT. However, the world of windows file systems is larger and in most cases just a simple test is enough to expose bigger issues.

I'd also like to mention the little known fact that Alternate Data Streams (ADS) and hardlinks are not only available on NTFS but on UDF as well, which makes UDF quite useful for testing these features if a filter uses them. Though UDF is meant for optical media I'll show you how you can set up a local virtual volume very easily.

I'm going to use the VHD support in the OS, which means this works on Win7 (and newer OSes). I found virtual disks to be very useful for testing and they're easy to setup and automate. What follows is code that creates a dynamic VHD:

C:\Users\Me>diskpart
Microsoft DiskPart version 6.1.7600
Copyright (C) 1999-2008 Microsoft Corporation.
On computer: xxxxxxx

DISKPART> create vdisk file=c:\testUDFS.vhd maximum=10000 type=expandable

  100 percent completed

DiskPart successfully created the virtual disk file.

DISKPART> attach vdisk

  100 percent completed

DiskPart successfully attached the virtual disk file.

Now that we have a VHD, the next step is to partition and format it:

DISKPART> create partition primary

DiskPart succeeded in creating the specified partition.

DISKPART> format fs=UDF label="UDFSVol" quick

  100 percent completed

DiskPart successfully formatted the volume.

DISKPART> assign

DiskPart successfully assigned the drive letter or mount point.

DISKPART> exit

Leaving DiskPart...

And that's all it takes, you now have the 10GB writable UDFS volume that can be used for testing filters in unusual setups:

C:\Users\Me>fsutil fsinfo volumeinfo I:
Volume Name : UDFSVol
Volume Serial Number : 0x96b7dca5
Max Component Length : 254
File System Name : UDF
Supports Case-sensitive filenames
Preserves Case of filenames
Supports Unicode in filenames
Supports Named Streams
Supports Hard Links

Finally, exactly the same approach can be used to create an NTFS volume or an ExFAT volume, all that needs to change is the line to format the disk:

DISKPART> format fs=UDF label="UDFSVol" quick
or
DISKPART> format fs=ExFAT label="ExFATVol" quick
or
DISKPART> format fs=NTFS label="NTFSVol" quick