Thursday, March 15, 2012

Volume Names

In this post I want to talk about something that's not directly related to file system filters but that I've spent a lot of time fighting with. I'm specifically talking about volume names and the reason this is important to me is because these days I work on virtualization filters and in some cases when creating virtual files I need to make them feel the same as regular files on a real volume and the way some applications (both kernel mode and user mode) handle volume names is downright broken.
The most important point I want to make is that a volume name is NOT a drive letter. I read a lot of articles and attend a lot of presentations where volumes are identified by "drive letter" which, while is useful as a way to express a concept because everyone is familiar with drive letters, is actually wrong. Drive letters are a DOS concept, the NT concept is the volume name (and it looks like this '\\?\Volume{0d5759d1-429c-11df-8e0f-806e6f6e6963}'). Easiest way to see this is to use the "mountvol.exe" command line tool. This difference is very clearly expressed in the mountmgr.h file (%DDKPATH%\inc\ddk\mountmgr.h) where there are macros like 'MOUNTMGR_IS_VOLUME_NAME(s)' and 'MOUNTMGR_IS_DRIVE_LETTER':


//
// Macro that defines what a "drive letter" mount point is.  This macro can
// be used to scan the result from QUERY_POINTS to discover which mount points
// are find "drive letter" mount points.
//

#define MOUNTMGR_IS_DRIVE_LETTER(s) (   \
    (s)->Length == 28 &&                \
    (s)->Buffer[0] == '\\' &&           \
    (s)->Buffer[1] == 'D' &&            \
    (s)->Buffer[2] == 'o' &&            \
    (s)->Buffer[3] == 's' &&            \
    (s)->Buffer[4] == 'D' &&            \
    (s)->Buffer[5] == 'e' &&            \
    (s)->Buffer[6] == 'v' &&            \
    (s)->Buffer[7] == 'i' &&            \
    (s)->Buffer[8] == 'c' &&            \
    (s)->Buffer[9] == 'e' &&            \
    (s)->Buffer[10] == 's' &&           \
    (s)->Buffer[11] == '\\' &&          \
    (s)->Buffer[12] >= 'A' &&           \
    (s)->Buffer[12] <= 'Z' &&           \
    (s)->Buffer[13] == ':')

//
// Macro that defines what a "volume name" mount point is.  This macro can
// be used to scan the result from QUERY_POINTS to discover which mount points
// are "volume name" mount points.
//

#define MOUNTMGR_IS_VOLUME_NAME(s) (                                          \
     ((s)->Length == 96 || ((s)->Length == 98 && (s)->Buffer[48] == '\\')) && \
     (s)->Buffer[0] == '\\' &&                                                \
     ((s)->Buffer[1] == '?' || (s)->Buffer[1] == '\\') &&                     \
     (s)->Buffer[2] == '?' &&                                                 \
     (s)->Buffer[3] == '\\' &&                                                \
     (s)->Buffer[4] == 'V' &&                                                 \
     (s)->Buffer[5] == 'o' &&                                                 \
     (s)->Buffer[6] == 'l' &&                                                 \
     (s)->Buffer[7] == 'u' &&                                                 \
     (s)->Buffer[8] == 'm' &&                                                 \
     (s)->Buffer[9] == 'e' &&                                                 \
     (s)->Buffer[10] == '{' &&                                                \
     (s)->Buffer[19] == '-' &&                                                \
     (s)->Buffer[24] == '-' &&                                                \
     (s)->Buffer[29] == '-' &&                                                \
     (s)->Buffer[34] == '-' &&                                                \
     (s)->Buffer[47] == '}'                                                   \
    )
So unless you're writing applications that are specific to DOS, please stop thinking in terms of "drive letters" and instead think of "volume names", especially when writing articles and presentations. There are many volume user mode APIs that are very well documented (see the page Volume Management Functions in MSDN) and that should be used. Also, as a developer, never write a function that takes a parameter a volume as a "char" and instead always use mount points or volume names (which is a string). There is also a page on Naming a Volume which discusses some of the use cases and the available APIs.
As I mentioned in my previous post on Problems with STATUS_REPARSE - Part II, a lot of the times the problems come from user mode apps trying to build a path to a file and they expect to get a drive letter as the volume, which is just wrong. Even the MSDN example Obtaining a File Name From a File Handle falls into this trap by using drive letters all over:


…
        if (GetLogicalDriveStrings(BUFSIZE-1, szTemp)) 
        {
          TCHAR szName[MAX_PATH];
          TCHAR szDrive[3] = TEXT(" :");   <- this is wrong…
          BOOL bFound = FALSE;
          TCHAR* p = szTemp;

          do 
          {
            // Copy the drive letter to the template string
            *szDrive = *p;

            // Look up each device name
            if (QueryDosDevice(szDrive, szName, MAX_PATH))  <- this is wrong...
            {
              size_t uNameLen = _tcslen(szName);

              if (uNameLen < MAX_PATH) 
              {
                bFound = _tcsnicmp(pszFilename, szName, uNameLen) == 0
                         && *(pszFilename + uNameLen) == _T('\\');

                if (bFound) 
                {
                  // Reconstruct pszFilename using szTempFile
                  // Replace device path with DOS path
                  TCHAR szTempFile[MAX_PATH];
                  StringCchPrintf(szTempFile,
                            MAX_PATH,
                            TEXT("%s%s"),
                            szDrive,
                            pszFilename+uNameLen);
                  StringCchCopyN(pszFilename, MAX_PATH+1, szTempFile, _tcslen(szTempFile));
...
I've always wondered, as a windows developer, does it not bother people that they're calling functions like "QueryDosDevice" ? What does DOS have to do with anything ? Step into the 21st century already!
Anyway, the best way to do this is to call GetFinalPathNameByHandle() and use the VOLUME_NAME_GUID flag to get used to using volume names. Unfortunately this is only available in Vista and newer OSes and so for XP one could still use the technique described in Obtaining a File Name From a File Handle but there is something that needs to be changed. The problem is that the volume APIs don't seem to have a way to convert a volume device name ('\Device\HarddiskVolume2') to a volume GUID name. In fact, none of the volume APIs offer an easy way to work with volume device names. The one way I've been able to do this in the general case was to use the MountMgr APIs directly. I don't have any user mode code that shows exactly what need to be done but I'll show the kernel mode code piece that queries the MountMgr:

#define MY_MOUNTMGR_MOUNT_POINT_TAG = 'mMyM'

typedef enum _MY_MOUNTMGR_BUFFER_TYPE {

    //
    // we'll query the MOUNTMGR using one of the three keys it supports..
    //

    MY_MOUNTMGR_SYMLINK = ' myS',
    MY_MOUNTMGR_UNIQUE_ID = 'DIUU',
    MY_MOUNTMGR_DEVICE = ' veD',
        
} MY_MOUNTMGR_BUFFER_TYPE, *PMY_MOUNTMGR_BUFFER_TYPE;


NTSTATUS
MyQueryMountMgr(
    __in PVOID Buffer,
    __in USHORT BufferLength,
    __in MY_MOUNTMGR_BUFFER_TYPE BufferType,  
    __out PMOUNTMGR_MOUNT_POINTS * MountPoints  
    )
/*++

Routine Description:

    Call MountMgr to get a names of a volume when knowing one of the
    other names.

Arguments:

    Buffer - the buffer that we want to send MountMgr to allow it to identify 
             the volume we're talking about. 

    BufferLength - the length of that buffer

    BufferType - the type of information that the buffer describes.

    MountPoints - this is a buffer that is allocated inside this function that 
                  the caller must free which is the list of mount points that
                  MountMgr returned... if it's NULL then no buffer is returned..
                  This is NOT the standard convention (the caller should supply 
                  the buffer) but it saves time.

Return Value:

    an appropriate NTSTATUS value

--*/
{
    NTSTATUS status = STATUS_SUCCESS;

    PMOUNTMGR_MOUNT_POINT mountMgrKey = NULL;
    ULONG mountMgrKeyLength = 0;

    PIRP irp = NULL;

    UNICODE_STRING mountMgrName;
    PFILE_OBJECT mountMgrFileObject = NULL;
    PDEVICE_OBJECT mountMgrDeviceObject = NULL;

    IO_STATUS_BLOCK ioStatus;
    KEVENT ioEvent;

    PMOUNTMGR_MOUNT_POINTS mountMgrMountPoints = NULL;
    ULONG mountMgrMountPointsLength = 0;
    
    PAGED_CODE();

    __try{

        KeInitializeEvent( &ioEvent, NotificationEvent, FALSE);

        //
        // first try to set up the buffer for the name..
        //
        
        mountMgrKeyLength = sizeof(MOUNTMGR_MOUNT_POINT);
        mountMgrKeyLength += BufferLength;


        mountMgrKey = ExAllocatePoolWithTag( PagedPool,
                                             mountMgrKeyLength,
                                             MY_MOUNTMGR_MOUNT_POINT_TAG );

        if (mountMgrKey == NULL) {

            status = STATUS_INSUFFICIENT_RESOURCES;
            __leave;
        }

        //
        // populate the structure..
        //

        RtlZeroMemory( mountMgrKey, mountMgrKeyLength);

        switch(BufferType) {

            case MY_MOUNTMGR_DEVICE:

                mountMgrKey->DeviceNameLength = BufferLength;
                mountMgrKey->DeviceNameOffset = sizeof(MOUNTMGR_MOUNT_POINT);
                break;

            case MY_MOUNTMGR_UNIQUE_ID:

                mountMgrKey->UniqueIdLength= BufferLength;
                mountMgrKey->UniqueIdOffset = sizeof(MOUNTMGR_MOUNT_POINT);
                break;

            case MY_MOUNTMGR_SYMLINK:

                mountMgrKey->SymbolicLinkNameLength= BufferLength;
                mountMgrKey->SymbolicLinkNameOffset= sizeof(MOUNTMGR_MOUNT_POINT);
                break;

            default:

                status = STATUS_INVALID_PARAMETER;
                __leave;
                break;
        }

        RtlCopyMemory( Add2Ptr(mountMgrKey, sizeof(MOUNTMGR_MOUNT_POINT)),
                       Buffer,
                       BufferLength );

        //
        // now we need a reference to MountMgr
        //

        RtlInitUnicodeString(&mountMgrName, MOUNTMGR_DEVICE_NAME);
        
        status = IoGetDeviceObjectPointer( &mountMgrName,
                                           FILE_READ_ATTRIBUTES, 
                                           &mountMgrFileObject, 
                                           &mountMgrDeviceObject);
        
        if (!NT_SUCCESS(status)) {
        
            __leave;
        }

        mountMgrMountPointsLength = sizeof(MOUNTMGR_MOUNT_POINTS);

        status = STATUS_BUFFER_OVERFLOW;

        while(status == STATUS_BUFFER_OVERFLOW) {

            NT_ASSERT(mountMgrMountPoints == NULL);

            mountMgrMountPoints = ExAllocatePoolWithTag( PagedPool,
                                                         mountMgrMountPointsLength,
                                                         MY_MOUNTMGR_MOUNT_POINT_TAG );

            if (mountMgrMountPoints == NULL) {

                status = STATUS_INSUFFICIENT_RESOURCES;
                __leave;
            }

            irp = IoBuildDeviceIoControlRequest( IOCTL_MOUNTMGR_QUERY_POINTS,
                                                 mountMgrDeviceObject, 
                                                 mountMgrKey, 
                                                 mountMgrKeyLength, 
                                                 mountMgrMountPoints, 
                                                 mountMgrMountPointsLength, 
                                                 FALSE, 
                                                 &ioEvent, 
                                                 &ioStatus);

            if (irp == NULL) {

                status = STATUS_INSUFFICIENT_RESOURCES;
                __leave;
            }
        
            status = IoCallDriver( mountMgrDeviceObject, irp );
            
            if (status == STATUS_PENDING) {
            
                status = KeWaitForSingleObject( &ioEvent,
                                                Executive,
                                                KernelMode,
                                                FALSE,
                                                NULL );
            
                status = ioStatus.Status;
            }

            switch (status) {

                case STATUS_BUFFER_OVERFLOW:

                    //
                    // we need a bigger buffer, the Size should tell us how big.
                    // assert that it's more that we previously had...
                    //

                    NT_ASSERT(mountMgrMountPointsLength < mountMgrMountPoints->Size);

                    mountMgrMountPointsLength = mountMgrMountPoints->Size;

                    ExFreePoolWithTag( mountMgrMountPoints, MY_MOUNTMGR_MOUNT_POINT_TAG );
                    mountMgrMountPoints = NULL;
                    
                    break;

                case STATUS_OBJECT_NAME_NOT_FOUND:

                    //
                    // it is possible that the IOCTL doesn't find anything, this
                    // is not a problem...
                    //
                    
                    break;

                case STATUS_SUCCESS:

                    //
                    // we got the links back, all is good... for the delete case
                    // it's possible we'll get called multiple times..
                    //

                    NT_ASSERT((mountMgrMountPoints->NumberOfMountPoints != 0) ||
                               (MountMgrIoctl == IOCTL_MOUNTMGR_DELETE_POINTS));

                    break;

                default:

                    NT_ASSERT(!"why are we here ? investigate...");
                    break;
                        
            }

        }

    }
    __finally{

        if (mountMgrKey != NULL) {

            ExFreePoolWithTag( mountMgrKey, MY_MOUNTMGR_MOUNT_POINT_TAG );
        }

        if (mountMgrFileObject != NULL) {

            ObDereferenceObject( mountMgrFileObject );
        }

    }
    
    //
    // if we have some mount points and we were successful and the caller
    // gave us a pointer, then set it in that pointer. Otherwise, free it..
    //

    if (NT_SUCCESS(status) &&
        MountPoints != NULL) {

        *MountPoints = mountMgrMountPoints;
        
    }
     
    return status;
}
Something very similar can be done in user mode (though it would be a lot simpler), where instead of IoGetDeviceObjectPointer() one would have to open MOUNTMGR_DOS_DEVICE_NAME and get a handle to the MountMgr device and also the call to IoBuildDeviceIoControlRequest would be replaced with DeviceIoControl.

Thursday, March 8, 2012

Name Provider Changes

I just wanted to add a couple of things to my previous post on what a passthrough nameprovider should look like. There are two changes I'd like to make:
  1. The code I have posted for the PtGenerateFileNameCallback() function has an assert that it shouldn't receive a generate request for a normalized name. However, there are some changes in Win8 where the file system can now generate directly a normalized name (see my previous post on Name Normalization in Win8) and as such FltMgr has been changed and it can also now request a normalized name from in the generate name callback. So that assert needs to be changed to include a normalized name.
  2. Also in the code for PtGenerateFileNameCallback() I have omitted to add a very important flag, the FLT_FILE_NAME_DO_NOT_CACHE flag. This is a very important flag because it can lead to problems with FltMgr's name cache. It isn't really necessary for my example because the name above my filter is the same as the name below my filter, but it is very important for filters that change the name of a file. I'll explain this issue in more detail below.
So FltMgr's name cache is simply a FltMgr specific context associated with the stream (or with the FILE_OBJECT depending on some factors) where FltMgr remembers the name for the object. Because before Win8 building a name was such an expensive operation FltMgr wanted to cache the name whenever it was generated in response to a filter request (I mean that FltMgr never actually went and populated the name by itself) and also FltMgr tried really hard not to lose any such name.
First I'd like to point out that FLtMgr caches each name at multiple levels, for each name provider. So if there is no name provider on the stack there is only one entry, for the name at the file system level. If there is a name provider then there are two entries in the cache, one at the name provider's level and one at the file system's level. And so on.
The problem with using stream contexts (or stream handle contexts) is that you can't know what the actual context is until in postCreate. However, it is not uncommon for filters to query for the name during preCreate, which means that FltMgr can't look it up in the cache and so it must generate it every time (which is why querying for names in preCreate is a pretty big performance hit). Moreover, once the name is generated FltMgr can't even cache because it doesn't have the stream. So in this case FltMgr passes the name from preCreate to postCreate in a fashion similar to what a minifilter would do (using the CompletionContext parameter of the preCreate callback). However, since there is no such parameter FltMgr uses the IRP to send some information from preCreate to postCreate, including the name. Once the IRP_MJ_CREATE operation is complete and FltMgr knows what the stream is, it tries to save the name it generated during preCreate into the name cache.
This is where FLT_FILE_NAME_DO_NOT_CACHE comes in. Let's say we have a minifilter that changes the name for a file from C:\foo.txt to C:\bar.txt (so C:\foo.txt is the name seen above the minifilter and C:\bar.txt is the name seen at the file system's level). If the name provider minifilter simply passes the request down to the file system then the file system will see a request for C:\foo.txt. In this case this means that it will be look at the file system level as "C:\foo.txt". Once the name is generated FltMgr will cache it and associate it with the IRP_MJ_CREATE operation. However, once the actual IRP_MJ_CREATE is completed since the C:\foo.txt name is now associated with the IRP FltMgr will cache the name at the file system level as "C:\foo.txt" instead of the "C:\bar.txt" that it should be. This is why name providers must always set the FLT_FILE_NAME_DO_NOT_CACHE flag when making their own requests to the file system, to tell the file system not to cache those names.
So this is the new & improved code (changes in RED):


 NTSTATUS PtGenerateFileNameCallback(  
   __in   PFLT_INSTANCE Instance,  
   __in   PFILE_OBJECT FileObject,  
   __in_opt PFLT_CALLBACK_DATA CallbackData,  
   __in   FLT_FILE_NAME_OPTIONS NameOptions,  
   __out   PBOOLEAN CacheFileNameInformation,  
   __out   PFLT_NAME_CONTROL FileName  
   )  
 {  
   NTSTATUS status = STATUS_SUCCESS;  
   PFLT_FILE_NAME_INFORMATION belowFileName = NULL;  
   PT_DBG_PRINT( PTDBG_TRACE_ROUTINES,  
          ("PassThrough!PtGenerateFileNameCallback: Entered\n") );  
   __try {  

     //
     //  We expect to only get requests for opened, short and normalized names.
     //  If we get something else, fail. 
     //

     if (!FlagOn( NameOptions, FLT_FILE_NAME_OPENED ) && 
 !FlagOn( NameOptions, FLT_FILE_NAME_SHORT ) &&
 !FlagOn( NameOptions, FLT_FILE_NAME_NORMALIZED )) {

         ASSERT(!"we have a received a request for an unknown format. investigate!");

         return STATUS_NOT_SUPPORTED ;
     }

     //  
     // First we need to get the file name. We're going to call   
     // FltGetFileNameInformation below us to get the file name from FltMgr.   
     // However, it is possible that we're called by our own minifilter for   
     // the name so in order to avoid an infinite loop we must make sure to   
     // remove the flag that tells FltMgr to query this same minifilter.   
     //  
     ClearFlag( NameOptions, FLT_FILE_NAME_REQUEST_FROM_CURRENT_PROVIDER );  
     SetFlag( NameOptions, FLT_FILE_NAME_DO_NOT_CACHE );
     //  
     // this will be called for FltGetFileNameInformationUnsafe as well and  
     // in that case we don't have a CallbackData, which changes how we call   
     // into FltMgr.  
     //  
     if (CallbackData == NULL) {  
       //  
       // This must be a call from FltGetFileNameInformationUnsafe.  
       // However, in order to call FltGetFileNameInformationUnsafe the   
       // caller MUST have an open file (assert).  
       //  
       ASSERT( FileObject->FsContext != NULL );  
       status = FltGetFileNameInformationUnsafe( FileObject,  
                            Instance,  
                            NameOptions,  
                            &belowFileName );   
       if (!NT_SUCCESS(status)) {  
         __leave;  
       }                              
     } else {  
       //  
       // We have a callback data, we can just call FltMgr.  
       //  
       status = FltGetFileNameInformation( CallbackData,  
                         NameOptions,  
                         &belowFileName );   
       if (!NT_SUCCESS(status)) {  
         __leave;  
       }                              
     }  
     //  
     // At this point we have a name for the file (the opened name) that   
     // we'd like to return to the caller. We must make sure we have enough   
     // buffer to return the name or we must grow the buffer. This is easy   
     // when using the right FltMgr API.  
     //  
     status = FltCheckAndGrowNameControl( FileName, belowFileName->Name.Length );  
     if (!NT_SUCCESS(status)) {  
       __leave;  
     }  
     //  
     // There is enough buffer, copy the name from our local variable into  
     // the caller provided buffer.  
     //  
     RtlCopyUnicodeString( &FileName->Name, &belowFileName->Name );   
     //  
     // And finally tell the user they can cache this name.  
     //  
     *CacheFileNameInformation = TRUE;  
   } __finally {  
     if ( belowFileName != NULL) {  
       FltReleaseFileNameInformation( belowFileName );        
     }  
   }  
   return status;  
 }  

Thursday, March 1, 2012

Win8 Bugcheck 0x1a_61946

I was going to skip posting this week but at plugfest last week I've run into an issue that I believe might impact users testing with Win8 and I wanted to explain what the problem is and how it can be avoided.

This is what the bugcheck might look like:

1: kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

MEMORY_MANAGEMENT (1a)
    # Any other values for parameter 1 must be individually examined.
Arguments:
Arg1: 00061946, The subtype of the bugcheck.
Arg2: d0427500
Arg3: 000214ce
Arg4: 00000000

Debugging Details:
------------------


BUGCHECK_STR:  0x1a_61946

DEFAULT_BUCKET_ID:  VISTA_DRIVER_FAULT

PROCESS_NAME:  System

CURRENT_IRQL:  2

LAST_CONTROL_TRANSFER:  from 8217aa43 to 82151d70

STACK_TEXT:  
8989f2ec 8217aa43 00000003 07cae677 0000001a nt!RtlpBreakWithStatusInstruction
8989f33c 8217a088 00000003 80bf4138 8989f744 nt!KiBugCheckDebugBreak+0x1c
8989f718 821509da 0000001a 00061946 d0427500 nt!KeBugCheck2+0x594
8989f73c 82150911 0000001a 00061946 d0427500 nt!KiBugCheck2+0xc6
8989f75c 821f0e08 0000001a 00061946 d0427500 nt!KeBugCheckEx+0x19
8989f79c 820b9b14 8989f7c0 000000c4 d0427500 nt! ?? ::FNODOBFM::`string'+0x1dc6f
8989f800 82518ec8 d0427500 00000000 00000001 nt!MmProbeAndLockPages+0x134
8989f820 8281fb38 d0427500 00000000 00000001 nt!VerifierMmProbeAndLockPages+0x7b
8989f860 8282553d d53fc4f8 d5601e28 00000001 Ntfs!NtfsLockUserBuffer+0x4c
8989f880 828177eb 00000001 00000000 d53fc4f8 Ntfs!NtfsPrePostIrpInternal+0xa1
8989f8a0 82824d68 00000001 8dcd8e00 db5e1c9f Ntfs!NtfsPostRequest+0x21
8989f90c 8281d01e d53fc4f8 d5601e28 c00000d8 Ntfs!NtfsProcessException+0x2b4
8989f988 82500e6b 88cac018 d5601e28 d5601c6c Ntfs!NtfsFsdRead+0x376
8989f9b0 82090047 8278b0ee d5601c68 8989fa14 nt!IovCallDriver+0x2f3
8989f9c0 8278b0ee d5601e28 88ca6ba8 d421e6c0 nt!IofCallDriver+0x72
8989fa14 8278c432 8989fa38 00000000 00000000 FLTMGR!FltpLegacyProcessingAfterPreCallbacksCompleted+0x25b
8989fa50 82500e6b 88ca6ba8 d5601e28 88ccf120 FLTMGR!FltpDispatch+0xca
8989fa78 82090047 822d9c57 d5601e28 8989fadc nt!IovCallDriver+0x2f3
8989fa88 822d9c57 d5601e28 d5602000 d5601e30 nt!IofCallDriver+0x72
8989fadc 822d3b37 88ca6ba8 00000001 00000000 nt!IopSynchronousServiceTail+0x10a
8989fb74 821cabec 88ca6ba8 80002e70 00000000 nt!NtReadFile+0x3f7
8989fb74 8214f1f1 88ca6ba8 80002e70 00000000 nt!KiFastCallEntry+0x12c
8989fc10 82780b85 80002038 80002e70 00000000 nt!ZwReadFile+0x11
8989fc94 82771fa9 8989fcc8 94c0f000 00001000 mydriver!MyDriverReadFile+0x28f 

STACK_COMMAND:  kb

FOLLOWUP_IP: 
nt! ?? ::FNODOBFM::`string'+1dc6f
821f0e08 8bd0            mov     edx,eax

SYMBOL_STACK_INDEX:  5

SYMBOL_NAME:  nt! ?? ::FNODOBFM::`string'+1dc6f

FOLLOWUP_NAME:  MachineOwner

MODULE_NAME: nt

IMAGE_NAME:  ntkrpamp.exe

DEBUG_FLR_IMAGE_TIMESTAMP:  4f23bd74

FAILURE_BUCKET_ID:  0x1a_61946_VRF_nt!_??_::FNODOBFM::_string_+1dc6f

BUCKET_ID:  0x1a_61946_VRF_nt!_??_::FNODOBFM::_string_+1dc6f

Followup: MachineOwner
---------

Now, what MyDriver was doing was to take an MDL that was used for a paging read and it created a system VA for it (by calling MmGetSystemAddressForMdlSafe()) and then it was populating some parts of it by calling ZwReadFile to read into that buffer. The problem is that when calling ZwReadFile for the address the pages will be marked dirty. However, the pages were supposed to be populated from a paging read and so it makes no sense that they are marked "dirty" in the paging read path. There are a couple of different reasons where this is problematic:

  • If the pages are marked dirty it means that MM will try to save their contents (since it believe the page contains data that hasn't been saved to disk yet - that's what a dirty page means). So if MM is running out of physical pages it will try to create more space by reusing some of the existing physical pages. For pages that aren't dirty MM can simply discard them because it knows it can get the contents back later (through another paging read). However, if the pages are marked dirty it must save the contents somewhere (in this case in the page file) and fault them in from the page file later. This is slow (MM must first save the contents to the page file though it doesn’t need to) and it can lead to problems because space in the page file hasn't been allocated for this page so MM might run out of space later and bugcheck.
  • this can be problematic for image pages (for exes, dlls) because any fixups that might normally be applied on the fly when paging them in from the file system will become "permanent" because the data is now dirty and so it might lead to crashes later on when the page is used in a different process.

Anyway, this is clearly bad so let's discuss what a driver should do about this:

  • If a file system filter (or any other type of driver) has an MDL and it wants to populate it with data from a file (or from multiple files) (i.e. if it needs to issue one or more reads to get the data) then it must issue an IRP_MJ_READ request using the same MDL that was passed in (please note that it might be necessary to split the MDL by using IoBuildPartialMdl()). Minifilters can use FltReadFileEx() to read data directy into the MDL (or MDLs). Legacy filters or other types of drivers must NOT use ZwReadFile and instead they must issue an IRP_MJ_READ with the partial MDLs.
  • If a file system (or file system filter) wants to populate the MDL or parts of the MDL by writing into it directly (for example an encryption filter that will write the decrypted data into the user's buffer directly; another example would be a filter that completely owns a virtual file for which it supplies all the contents without trying to read them from disk at all) it can call MmGetSystemAddressForMdlSafe() to get a system VA and then the filter can write directly into that buffer. However, the filter must not create a new MDL for any part of that buffer. If a filter must use a MDL for all or some part of the buffer it should build a partial MDL.

Please note that this has always been the correct way of doing things, but developers have been taking the easy way out and getting lucky until now. Checked builds for previous OS releases used assert when this was detected. One additional benefit of doing things the right way is that performance should be slightly better as well.

Just to point out what this might look like for a user, please take a look at this VirtualBox ticket https://www.virtualbox.org/ticket/10290 which illustrates this issue.

Thursday, February 23, 2012

IFS Plugfest

Hi everyone. Sorry, I've been meaning to post this earlier but I didn't get a chance. I'm currently at the IFS Plugfest event so there will be no post this week. I'll also be away next week for the MVP Summit event and so I don't expect I'll get a chance to post anything next week either.

Thursday, February 16, 2012

Reparsing to a Different Volume in Win7 and Win8

So last time I mentioned what I believe to be one of the most serious problems that STATUS_REPARSE introduces, namely the fact that FltCreateFile above a STATUS_REPARSE that reparses to a different volume fails with STATUS_MOUNT_POINT_NOT_RESOLVED. In this post I’d like to illustrate how to write a filter that can expose this problem as well as to show some steps that Microsoft seems to have take in Win8 to address this issue. The code also shows how to attach an ECP to an IRP_MJ_CREATE request issued by a filter.

So first let me describe my setup. There are two volumes, C: and E:. I have created the file E:\test.txt and then using mklink I have created a symlink from C:\myfile.txt to E:\test.txt. I have a minifilter based on the passthrough sample that whenever it intercepts a create for any file named "myfile.txt" (regardless of the path) tries to issue a FltCreateFileEx2(). I've added a DbgBreakPoint() right before the FltCreateFileEx2() call so that we can see the status and inspect the variables. The environments are Win7 (latest SP) and Win8 developer preview.

So this is the code I've used to illustrate the problem (code I've added to the passthrough sample):

    PT_DBG_PRINT( PTDBG_TRACE_ROUTINES,  
           ("PassThrough!PtPreOperationPassThrough: Entered\n") );  


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

        UNICODE_STRING myFile = RTL_CONSTANT_STRING( L"myfile.txt" );
        OBJECT_ATTRIBUTES fileAttributes;
        HANDLE fileHandle = NULL;
        IO_STATUS_BLOCK ioStatus;
        IO_DRIVER_CREATE_CONTEXT driverContext;

        __try {

            //  
            // this is a preCreate, get the name.  
            //  
            status = FltGetFileNameInformation( Data,   
                                                FLT_FILE_NAME_OPENED | FLT_FILE_NAME_QUERY_DEFAULT,   
                                                &fileName );  

            if (!NT_SUCCESS(status)) {

                DbgBreakPoint();
                __leave;
            }

            //
            // we need to parse the name to get the final component
            //

            status = FltParseFileNameInformation( fileName );

            if (!NT_SUCCESS(status)) {

                DbgBreakPoint();
                __leave;
            }

            //
            //  Compare to see if this is a file we care about.
            //

            if (!RtlEqualUnicodeString( &fileName->FinalComponent,
                                        &myFile,
                                        TRUE )) {

                __leave;
            }

            //
            // initialize the driver context and the attributes. 
            //

            IoInitializeDriverCreateContext( &driverContext );

            InitializeObjectAttributes( &fileAttributes, 
                                        &fileName->Name,
                                        OBJ_KERNEL_HANDLE,
                                        NULL,
                                        NULL );

            //
            // finally issue the create.
            //

            status = FltCreateFileEx2( FltObjects->Filter,
                                       FltObjects->Instance,
                                       &fileHandle,
                                       NULL,
                                       FILE_READ_DATA | SYNCHRONIZE,
                                       &fileAttributes,
                                       &ioStatus,
                                       NULL,
                                       FILE_ATTRIBUTE_NORMAL,
                                       FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                       FILE_OPEN,
                                       FILE_SYNCHRONOUS_IO_ALERT,
                                       NULL,
                                       0,
                                       0,
                                       &driverContext ); 

            //
            // check the status.
            //

            DbgBreakPoint();

        } __finally {

            if (fileName != NULL) {

                FltReleaseFileNameInformation( fileName );
            }

            if (fileHandle != NULL) {

                ZwClose(fileHandle);
            }
        }
    }


    //
    //  See if this is an operation we would like the operation status
    //  for.  If so request it.

So as you can see we're not doing much here. As mentioned before, when trying to open c:\myfile.txt we will hit the breakpoint and the status will be STATUS_MOUNT_POINT_NOT_RESOLVED.

Looking in the win8 ddk we can see some interesting definitions for an ECP with the GUID GUID_ECP_FLT_CREATEFILE_TARGET and for a structure with the name FLT_CREATEFILE_TARGET_ECP_CONTEXT. The comment states that "This GUID and structure define an extra create parameter used for passing back reparse information from FltCreateFileEx2 when the create is reparsed to a different device.". When an ECP is used to pass back information it means that the caller of the IRP_MJ_CREATE must set an ECP list into the DriverContext otherwise any ECP that is added to the IRP_MJ_CREATE will be discarded before IoCreateFileEx returns and so there is no way to get that ECP back. First thing I tried was to set an ECP list and see if the IO manager or the FS allocates an ECP with the information and passes it back to me, but I didn't get any. Next thing I tried was to allocate an EPC and add it to the ECP list and that seemed to do the trick, the ECP that I allocated was filled with data. But before we look at the debugger data, let me paste the code that I used for Win8. BTW, all I did was to copy the definitions from the header into the passThrough.c file and compiled with the Win7 WDK. This works because everything except the structure definition and the ECP GUID was available in Win7 anyway. So here is the code:
    PT_DBG_PRINT( PTDBG_TRACE_ROUTINES,  
           ("PassThrough!PtPreOperationPassThrough: Entered\n") );  


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

        UNICODE_STRING myFile = RTL_CONSTANT_STRING( L"myfile.txt" );
        OBJECT_ATTRIBUTES fileAttributes;
        HANDLE fileHandle = NULL;
        IO_STATUS_BLOCK ioStatus;
        IO_DRIVER_CREATE_CONTEXT driverContext;
        PECP_LIST ecpList = NULL;
        PFLT_CREATEFILE_TARGET_ECP_CONTEXT targetEcp = NULL;

        NTSTATUS ecpFindStatus;
        ULONG targetEcpSize = 0;


        __try {

            //  
            // this is a preCreate, get the name.  
            //  

            status = FltGetFileNameInformation( Data,   
                                                FLT_FILE_NAME_OPENED | FLT_FILE_NAME_QUERY_DEFAULT,   
                                                &fileName );  
            if (!NT_SUCCESS(status)) {

                DbgBreakPoint();
                __leave;
            }

            //
            // we need to parse the name to get the final component
            //

            status = FltParseFileNameInformation( fileName );

            if (!NT_SUCCESS(status)) {

                DbgBreakPoint();
                __leave;
            }

            //
            //  Compare to see if this is a file we care about.
            //

            if (!RtlEqualUnicodeString( &fileName->FinalComponent,
                                        &myFile,
                                        TRUE )) {

                __leave;
            }

            //
            // initialize the driver context.
            //

            IoInitializeDriverCreateContext( &driverContext );

            //
            // allocate the ECP list we're going to add to our create
            // 

            status = FltAllocateExtraCreateParameterList( FltObjects->Filter,
                                                         0,
                                                         &ecpList );

            if (!NT_SUCCESS(status)) {
                
                DbgBreakPoint();
                __leave;
            }

            //
            // allocate an ECP
            //

            status = FltAllocateExtraCreateParameter( FltObjects->Filter,
                                                      &GUID_ECP_FLT_CREATEFILE_TARGET,
                                                      sizeof(FLT_CREATEFILE_TARGET_ECP_CONTEXT),
                                                      0,
                                                      MyECPCleanupCallback,
                                                      'pcET',
                                                      &targetEcp );

            if (!NT_SUCCESS(status)) {

                DbgBreakPoint();
                __leave;
            }

            //
            // don't initialize anything, just zero out the ECP and add it to the ECP list.
            //

            RtlZeroMemory( targetEcp, sizeof(FLT_CREATEFILE_TARGET_ECP_CONTEXT));

            status = FltInsertExtraCreateParameter( FltObjects->Filter,
                                                    ecpList,
                                                    targetEcp );

            if (!NT_SUCCESS(status)) {

                DbgBreakPoint();
                __leave;
            }

            //
            // we make targetEcp NULL here so we don't manually free it. it will 
            // automatically go away when the list is freed.
            //

            targetEcp = NULL;

            //
            // add the ECP to the driverContex so that it gets attached to the IRP_MJ_CREATE.
            //

            driverContext.ExtraCreateParameter = ecpList;

            //
            // intialize the attributes and issue the create.
            //

            InitializeObjectAttributes( &fileAttributes, 
                                        &fileName->Name,
                                        OBJ_KERNEL_HANDLE,
                                        NULL,
                                        NULL );

            status = FltCreateFileEx2( FltObjects->Filter,
                                       FltObjects->Instance,
                                       &fileHandle,
                                       NULL,
                                       FILE_READ_DATA | SYNCHRONIZE,
                                       &fileAttributes,
                                       &ioStatus,
                                       NULL,
                                       FILE_ATTRIBUTE_NORMAL,
                                       FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                       FILE_OPEN,
                                       FILE_SYNCHRONOUS_IO_ALERT,
                                       NULL,
                                       0,
                                       0,
                                       &driverContext );

            //
            // and now we need to find the targetEcp. we shouldn't try to use the
            // address from before because it's possible that someone removed the ECP
            // and freed it during processing of the IRP_MJ_CREATE and so we
            // would be touching freed memory. so we're going to see if it's
            // still in the ECP list. at this time (after FltCreateFileEx2 returns)
            // we should be the only ones using the ECP list so it's safe to do this.
            //

            ecpFindStatus = FltFindExtraCreateParameter( FltObjects->Filter,
                                                         ecpList,
                                                         &GUID_ECP_FLT_CREATEFILE_TARGET,
                                                         &targetEcp,
                                                         &targetEcpSize );

            if (ecpFindStatus == STATUS_SUCCESS) {

                NT_ASSERT(targetEcpSize == sizeof(FLT_CREATEFILE_TARGET_ECP_CONTEXT));

                //
                // add a breakpoint here to investigate the targetEcp
                //

                DbgBreakPoint();
                
                //
                // NULL it here so we don't free it.
                //

                targetEcp = NULL; 
            }
             

            DbgBreakPoint();

        } __finally {

            if (fileName != NULL) {

                FltReleaseFileNameInformation( fileName );
            }

            if (fileHandle != NULL) {

                ZwClose(fileHandle);
            }

            if (ecpList != NULL) {

                FltFreeExtraCreateParameterList( FltObjects->Filter,
                                                 ecpList );
            }

            if (targetEcp != NULL) {

                FltFreeExtraCreateParameter( FltObjects->Filter,
                                             targetEcp );

            }
        }
    }


    //
    //  See if this is an operation we would like the operation status
    //  for.  If so request it.

I've also added a function to cleanup the ECP (though please note that i'm just guessing at what needs to happen here, the documentation will explain what filters must release and when):


VOID MyECPCleanupCallback (
    __inout  PVOID EcpContext,
    __in     LPCGUID EcpType
    )
{
    PFLT_CREATEFILE_TARGET_ECP_CONTEXT targetEcp = NULL;
    
    targetEcp = (PFLT_CREATEFILE_TARGET_ECP_CONTEXT)EcpContext;

    if (targetEcp->FileNameInformation != NULL) {

        DbgBreakPoint();

        FltReleaseFileNameInformation( targetEcp->FileNameInformation );
    }

    if (targetEcp->Instance != NULL) {

        FltObjectDereference( targetEcp->Instance );
    }
}

Now, the same code works both under Win7 and Win8 (and Vista too). The FltCreateFile fails and under Win7 the targetEcp structure looks like this:

1: kd> !error @@(status)
Error code: (NTSTATUS) 0xc0000368 (3221226344) - The create operation failed because the name contained at least one mount point which resolves to a volume to which the specified device object is not attached.
1: kd> ?? targetEcp
struct _FLT_CREATEFILE_TARGET_ECP_CONTEXT * 0xd7e96f74
   +0x000 Instance         : (null) 
   +0x004 Volume           : (null) 
   +0x008 FileNameInformation : (null) 
   +0x00c Flags 

However, under Win8 we see this:

0: kd> !error @@(status)
Error code: (NTSTATUS) 0xc0000368 (3221226344) - The create operation failed because the name contained at least one mount point which resolves to a volume to which the specified device object is not attached.
0: kd> ?? targetEcp
struct _FLT_CREATEFILE_TARGET_ECP_CONTEXT * 0x9974cbfc
   +0x000 Instance         : 0x83572b18 _FLT_INSTANCE
   +0x004 Volume           : (null) 
   +0x008 FileNameInformation : 0x82144124 _FLT_FILE_NAME_INFORMATION
   +0x00c Flags            : 0
0: kd> dt 0x82144124 _FLT_FILE_NAME_INFORMATION
fltmgr!_FLT_FILE_NAME_INFORMATION
   +0x000 Size             : 0x40
   +0x002 NamesParsed      : 0
   +0x004 Format           : 2
   +0x008 Name             : _UNICODE_STRING "\Device\HarddiskVolume3\test.txt"
   +0x010 Volume           : _UNICODE_STRING "\Device\HarddiskVolume3"
   +0x018 Share            : _UNICODE_STRING ""
   +0x020 Extension        : _UNICODE_STRING ""
   +0x028 Stream           : _UNICODE_STRING ""
   +0x030 FinalComponent   : _UNICODE_STRING ""
   +0x038 ParentDir        : _UNICODE_STRING ""
0: kd> !fltkd.instance 0x83572b18 

FLT_INSTANCE: 83572b18 "PassThrough Instance" "370030"
   FLT_OBJECT: 83572b18  [01000000] Instance
      RundownRef               : 0x00000002 (1)
      PointerCount             : 0x00000001 
      PrimaryLink              : [84777bc4-83435b68] 
   OperationRundownRef      : 82c37ba8 
Could not read field "Number" of fltmgr!_EX_RUNDOWN_REF_CACHE_AWARE from address: 82c37ba8
   Flags                    : [00000000]
   Volume                   : 83435ae0 "\Device\HarddiskVolume3"
   Filter                   : 82db1a20 "PassThrough"
   TrackCompletionNodes     : 82db7588 
   ContextLock              : (83572b54)
   Context                  : 00000000 
   CallbackNodes            : (83572b64)
   VolumeLink               : [84777bc4-83435b68] 
   FilterLink               : [82db1a88-82d8403c] 

So as you can see the FileNameInformation member has the name of the target of the reparse and the Instance member points to the instance the current filter has on that volume. The documentation for FLT_CREATEFILE_TARGET_ECP_CONTEXT isn't up yet so I don't know exactly what the rules are for using this ECP. Who is supposed to release the FileNameInformation members ? When is the Volume member populated ? Is the Instance member always referenced (my research seems to indicate so but I'm not 100% sure) ?

This is a pretty big deal because all minifilters that use FltCreateFile() will need to use similar code whenever they make those calls to deal with the occasional STATUS_MOUNT_POINT_NOT_RESOLVED. This method also has the advantage that the same code can be used on any platform since Vista, and if the OS supports this functionality then the ECP will be populated. This should also make it easier in case MS decides to add support for this in Win7 SP2 or something like this.

Thursday, February 9, 2012

Problems with STATUS_REPARSE - Part II

In this post I'd like to talk about some of more complicated issues that might arise when using STATUS_REPARSE, in particular that ones that are introduced when crossing a volume boundary (when reparsing from a file on a volume to a file on a different volume).

First I'd like to talk about names (yes, the eternal problem of the file system filter developer). If a filter simply uses STATUS_REPARSE to reparse from FileA to FileB then any name queries will return the proper name of the file, FileB. So an application that queries the name of file might get a different path than the one the user actually tried to open. It's generally pretty rare that an application will open a file and then query its name so that's not really a problem but there are other applications on the system that rely on the file path to make policy decisions. For example, a firewall application might have rules that identify application by their path. If a filter redirects one those opens, even if the file contents are exactly the same one as before (like in the case of a deduplication solution) the firewall rule might not match because the path is different. So a filter that uses STATUS_REPARSE will probably end up implementing a mechanism by which to return the actual path the user tried to open. This is complicated by the fact that in some cases it is impossible to know what that name was before the reparse (see my previous post). However, assuming that a filter can know what the path the user tried to open was it can return that to functions that query the name from the file system (like IRP_MJ_QUERY_INFORMATION with the FileNameInformation class). A minifilter will also need to implement name provider functionality because just completing IRP_MJ_QUERY_INFORMATION isn't enough for that to work (see the WDK SimRep minifilter sample). Now, if the reparse crossed a volume boundary (so FileA is on Volume1 while FileB is on Volume2) then things get a bit more complicated because IRP_MJ_QUERY_INFORMATION only returns a path relative to the volume but not the actual volume name (\FileA and not Volume1\FileA). In most cases the volume is derived from the FILE_OBJECT and the filter can't really change that (and in fact since the mapping between FILE_OBJECT and the volume device and the mapping between the volume device and the drive letter are internal to the object manager (OB) a file system filter won't even see any operations at all). So it is quite likely that if FileA and FileB have different paths then a filter or even an user mode app trying to resolve the file name for the file will get a path that isn't right (like Volume2\FileA or even Volume1\FileB).

Just to point out how big this problem is, the MSDN post on Obtaining a File Name From a File Handle uses a method that gets the actual NT name for a handle by calling GetMappedFileName(). Because the handle points to the target file (Volume2\FileB) the device that is returned is the device for Volume2 (and the call to QueryDosDevice() is used to resolve this device to Volume2; btw, QueryDosDevice() is NOT the proper way to resolve a device name to a drive letter, it doesn't cover many cases and I wish MSDN would point this out). Still the point to note here is that there is nothing a filter can do to change the name the application gets (Volume2). So if the filter would actually return the original path for name queries (FileA) then the name would be Volume2\FileA which is wrong. Another API that can be used to get the file (available only since Vista though) is GetFinalPathNameByHandle() . This functions works by querying the full path like GetMappedFileName() but it gets it directly from OB and doesn't involve the memory manager and then it figures out the device to drive letter mapping, though it does it in a much more complicated way than by calling QueryDosDevice(). However it has the same problems as the previous approach because it always returns the actual volume, Volume2.

Another problem with STATUS_REPARSE when it traverses volume boundaries is that renames stop working (see my posts on renames (PartI and PartII) for a quick refresher). It is possible that some renames actually continue to work because of the MOVEFILE_COPY_ALLOWED flag, but there are cases where this flag isn't set or when the rename is issued directly via IRP_MJ_SET_INFORMATION. This is also pretty big because handling this in a filter is extremely complicated.

One other problem that STATUS_REPARSE might introduce is only specific to cross-volume reparses. I've already mentioned this in my previous post on IRP_MJ_CREATE but I'll explain again because I believe this is really important. Basically, whenever a filter calls FltCreateFile FltMgr will issue a targeted IRP_MJ_CREATE by calling IoCreateFileEx() with a device hint (or a similar mechanism in OSes where IoCreateFileEx isn't available). The problem is that if a filter below that filter tries to reparse to a different volume then the IRP_MJ_CREATE request will fail because the device hint specified by FltMgr will not be attached to that stack. As such the call to FltCreateFile will itself fail. So, in a nutshell, whenever a filter reparses to a different volume any filter above that filter cannot open any file with a path that will be reparsed by the lower filter. In my opinion this is a really big issue because it's an interop issue (so it might be hard to find during testing unless testing with a lot of filters) and because FltCreateFile is a very frequent operation in minifilters (for example most antivirus filter open files for scanning by calling FltCreateFile()). Moreover, this is not a problem most filter developers are aware of and so most filters are not written to be able to handle FltCreateFile() failing in this way. Besides there really isn't a good way to work around the issue anyway and so in practice I have yet to see any filter that actually does anything more that fail the original operation they are processing with the error returned from FltCreateFile(). I believe that is pretty much the biggest problem with STATUS_REPARSE and I haven't been able so far to come up with any good workaround. Most of the other problems I've listed require more development effort (in some cases significantly so) or some clever workarounds, but this particular issue is a blocker.

So this is it. In my experience these are the major issues (but not all the issues) that a filter must be aware of (and work around) if it implements a solution using STATUS_REPARSE. In my opinion this long list of problems makes using STATUS_REPARSE in a project pretty complicated outside of a couple of few limited scenarios. The main reason I've seen people use STATUS_REPARSE so far has been because it's very easy to use and one can have a prototype rather quickly, but I don't think it pays off in the long run.

Thursday, February 2, 2012

Problems with STATUS_REPARSE - Part I

I've seen a lot of filters lately using STATUS_REPARSE to implement some of their functionality and I wanted to talk a bit about some of the problems that using STATUS_REPARSE might cause.

I've mentioned STATUS_REPARSE a couple of times before but I wanted to quickly go over some of the details, just as a refresher. The basic idea is that during file creation (IRP_MJ_CREATE) a filter can return STATUS_REPARSE and change the FILE_OBJECT->FileName to point to a different path, in which case the status gets propagated back to the Object Manager (OB) which will retry the create with the new path. Because the new path is resolved at OB level the new file path must include the device name (either in "\Device\HarddiskVolume1" for or in "\??\C:" form, both of which are valid at OB level). There is even a minifilter sample in the latest WDK (SimRep) that shows a pretty easy way to implement a filter that redirects an open to a different file using STATUS_REPARSE. There are multiple ways in which this might be useful for a filter, in particular I've seen this used in data deduplication filters and virtualization filters.

There is another way to use STATUS_REPARSE. This generally requires a file system filter that needs to be notified by the file system when someone it trying to access a file. It is possible to tag a specific file on the file system so that when someone is trying to open the file the file system itself will return STATUS_REPARSE and the filter can watch in postCreate for STATUS_REPARSE and check if it owns the tag and if so it can perform whatever operations it wants on the file. This is a very common approach for hierarchical storage management solutions (HSM).

This technique of tagging a file can also be used by deduplication filters. For example a deduplication filter might find that fileA and fileB are identical and copy the file to a special folder (let's say it would be \SpecialFolder\UniqueFileA.bin) and then remove all the contents from FileA and FileB and tag them and store in an internal database the fact that the paths for FileA and fileB should be redirected to "\SpecialFolder\UniqueFileA.bin". Then the filter might simply wait for the file system to return STATUS_REPARSE with the right tag and then, based on the file path, it might simply update the FILE_OBJECT->FileName to point to some file under "\SpecialFolder". Of course, the filter might not need a tag in the file system and instead it might opt to check the paths for all the files in preCreate and reparse them as appropriate (actually as far as I can tell this seems to be the more popular approach).

Virtualization filters might also use this to redirect opens to a different file than the file the application or the user thinks its opening. This is fairly useful for application virtualization filters (where an application might try to open a specific configuration file in a certain location ("C:\program files\app..") but since the app is virtualized the file doesn't exist at that location and so a filter will return STATUS_REPARSE to open the file at the actual location).

So now that we have covered some of the scenarios, let's move on to some of the problem these approaches might have.

  • reparse tracking is impossible before Vista - by this I mean that it's impossible for a filter to know whether an IRP_MJ_CREATE request is the result of a STATUS_REPARSE (by the same or a different filter) or not. In other words, in the case of the deduplication filter above, when the filter sees a create for "\SpecialFolder\UniqueFileA.bin" it can't tell if the IRP_MJ_CREATE was originally sent to FileA or FileB or even if it was a direct open to \SpecialFolder\UniqueFileA.bin. Consider what happens when the user wants to do something like rename FileA. All the filter sees is a rename arrive on the FILE_OBJECT opened for \SpecialFolder\UniqueFileA.bin but it doesn't know whether this rename means FileA should be renamed or FileB should be renamed. The same applies to other namespace operations like deletes and also to all operations that modify the file contents. Please note that with the introduction of ECPs in Vista it is possible to track whether a file has been reparsed and where it has been reparsed from (if a filter sets an ECP before returning STATUS_REPARSE the ECP will be attached to IRP_MJ_CREATE on the new path).
  • split IOs - in the scenario described above, even if the filter knows what the original file that the user tried to open was (FileA or FileB), we should discuss what a filter can do when it sees a request to modify the file data. Let's say the filter gets a request to truncate the file. Clearly the request can't be allowed to happen to UniqueFileA.bin because it would then impact both FileA and FileB, but the request was only for one of them and so the other one should remain unchanged. For a deduplication filter this is a time when the deduplication should be broken (and so the UniqueFileA.bin should be copied to the original file (either FileA or FileB) and the filter should no longer reparse from that file to UniqueFileA.bin). However, this can't be done at the time when the modifying operation happens and it must be done before the file is opened, and so the only thing the filter can rely on is the information available in IRP_MJ_CREATE (such as requested access for the file) to guess whether the handle will be used for modifying the file or not. That is not very reliable but let's assume that the filter somehow can figure out exactly when to break the deduplication. There is still a problem if there are already opened handles to the file because they now point to a different file. In other words, if an application opens FileA for read-only and so the filter correctly decides that it can reparse it to UniqueFileA.bin (Handle1) and then later another application (or even the same app, doesn't matter) opens another handle for FileA for write and so the filter breaks the dedup (and so now we have Handle2), the problem is that any modification performed on Handle2 will not be seen by Handle1 because they point to different files. This is what I call Split IOs. In other words, whenever a file name changes the actual file it points to it is possible to get this type of problem, and the only way to avoid this with a filter that returns STATUS_REPARSE is to make sure that the underlying mapping to the file on a file system never changes. Also, I'd like to point out this is a pretty common scenario. During my university days I used to have homework along the lines of "implement a producer-consumer solution where there are 10 consumer processes and 1 producer process and they all use a file for communication" and naturally if I had such a filter on my system all the 10 consumer processes would point to UniqueFileA.bin and the producer would point to FileA and so the consumers would never see the producer data… Also, please note that once there are handles that are supposed to point to the same file but point to different files it's not just reads and writes that are going to be out of sync, but also locks and oplocks and many other file system semantics might be broken. This is a very serious issue because it can lead to security issues (what if an antivirus thinks it scans FileA (but instead scans UniqueFileA.bin) and it finds it clean and allows the IRP_MJ_CREATE to continue but then the actual request ends up on a different file that is malicious?), data corruption or data loss.
  • FileIDs are more complicated - please see my previous post on STATUS_REPARSE and fileIDs.
  • Performance is generally not great - in most cases using STATUS_REPARSE requires dealing with names and querying the name in preCreate is a big performance hit. Also, looking up names in internal databases is generally done using normalized names, which are also a bigger performance hit (so normalized names in preCreate are the worst in terms of performance). Finally, if the architecture of the product requires getting a normalized name in preCreate and because filters that need to run on XP or Server 2003 can't tell when a reparse has happened it is often the case that filters get the normalized name twice (in our example, for FileA when they determine they must reparse to "\SpecialFolder\UniqueFileA.bin" and then for the subsequent IRP_MJ_CREATE for \SpecialFolder\UniqueFileA.bin, where the filter would determine that no reparse is necessary (but it would still query the name to make that determination)). This can make this type of filters have a pretty significant performance hit.

This is pretty dense material so I'll leave the other issues I was going to talk about for next week.