Thursday, June 23, 2011

Rename in File System Filters - part II

In this second part about renames in file system filters I'll to cover what steps need to be taken if a filter wants to issue its own IRP_MJ_SET_INFORMATION request. Also I'll like to look at the FastFat WDK implementation and point out some interesting things.

So what does a minifilter need to do if it wants to issue its own IRP_MJ_SET_INFORMATION? It turns out that it's not really that much. FltMgr provides a FltSetInformationFile() API that can be used for this purpose. A quick peek at that function reveals that for rename operations it calls a function fltmgr!FltpOpenLinkOrRenameTarget which performs a similar role to IopOpenLinkOrRenameTarget. So it would seem that for a minifilter there is never a need to actually send an IRP_MJ_SET_INFORMATION request directly. However, I have run into a case where FltSetInformationFile failed with STATUS_SHARING_VIOLATION on a customer machine in a scenario I've never been able to reproduce. After some investigation I've discovered that fltmgr!FltpOpenLinkOrRenameTarget issues its own create with FILE_SHARE_READ | FILE_SHARE_WRITE and no FILE_SHARE_DELETE. I can't tell for sure why that's the case, but it's consistent with IopOpenLinkOrRenameTarget. However, in this particular case, in an interaction with the CSC driver (the client side caching component in windows) NtSetInformationFile() worked without my minifilter in the picture while my call to FltSetInformationFile failed. I tried hard to reproduce this problem but I couldn't make it happen on my local machine and since there was only one IRP_MJ_CREATE issued I decided that I should try to add SHARE_DELETE and see if it fixes the problem (and it did). So I needed to implement my own function and build a FLT_CALLBACK_DATA structure and then send it to the file system below. These are the necessary steps that mimic the steps that fltmgr!FltpOpenLinkOrRenameTarget takes (the function itself is quite long but if there is enough interest it'll post it as an example… leave me a private message):

  1. Allocate and initialize the FILE_RENAME_INFORMATION structure.
  2. Allocate and initialize a FLT_CALLBACK_DATA structure.
  3. Call FltCreateFileEx2 (or FltCreateFile depending on the OS version) to open a handle to the target directory. Make sure to use IO_OPEN_TARGET_DIRECTORY here.
  4. Compare the DEVICE_OBJECTs associated with the source and target FILE_OBJECTs (resolve the handle returned by FltCreateFile for that) and fail if they're not the same (STATUS_NOT_SAME_DEVICE).
  5. Set PFILE_RENAME_INFORMATION->RootDirectory to the handle I've just got.
  6. Set PFLT_CALLBACK_DATA->Iopb->Parameters.SetFileInformation.ParentOfTarget to the PFILE_OBJECT associated with handle in PFILE_RENAME_INFORMATION->RootDirectory.
  7. Call FltPerformSynchronousIo( PFLT_CALLBACK_DATA …)
  8. Cleanup (close the handles, dereference the FILE_OBJECTs, free any buffers and so on)...

Finally it's time to look at the FastFat WDK sample and see what IO_OPEN_TARGET_DIRECTORY does. I'm looking at the files under the \WinDDK\7600.16385.1\src\filesys\fastfat\Win7\ directory in case you want to follow along. The reason this is interesting is because when looking at FatSetRenameInfo it's easy to see that if TargetFileObject is present, the new name for the file is exactly the name for TargetFileObject, completely ignoring whatever is set in the actual FILE_RENAME_INFORMATION buffer (here is the interesting line):

            NewName = *((PUNICODE_STRING)&TargetFileObject->FileName);

So looking at the FatCommonCreate function to see what it does for SL_OPEN_TARGET_DIRECTORY it is obvious that FatOpenTargetDirectory is the function where the magic happens. What FatOpenTargetDirectory does is that it replaces the FILE_OBJECT->FileName with the final component of that path, which explains why in FatSetRenameInfo Fat can simply look at the TargetFileObject->FileName to get the file name. This is pretty interesting since (as I said before) it means that when the IRP_MJ_SET_INFORMATION IRP is processed by filters, any modifications to the FILE_RENAME_INFORMATION->FileName are ignored.

The next step was to see if the other file systems work in a similar fashion. Unfortunately the other file system that ships with the WDK is CDFS, which doesn't support renames (I guess that's because it was designed to work on CDs, which are read-only and so renames would make no sense). So I took the passthrough sample and modified it a bit so that it would break during a successful postCreate for an operation that had the SL_OPEN_TARGET_DIRECTORY flag set, so that I could investigate what happens with the file name. First let me post the source code for the minifilter (i've just modified PtPreOperationPassThrough and PtPostOperationPassThrough):

FLT_PREOP_CALLBACK_STATUS
PtPreOperationPassThrough (
    __inout PFLT_CALLBACK_DATA Data,
    __in PCFLT_RELATED_OBJECTS FltObjects,
    __deref_out_opt PVOID *CompletionContext
    )
...
{
    NTSTATUS status;

    UNREFERENCED_PARAMETER( FltObjects );
    UNREFERENCED_PARAMETER( CompletionContext );

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

    if ((Data->Iopb->MajorFunction == IRP_MJ_CREATE) &&
        (FlagOn(Data->Iopb->OperationFlags, SL_OPEN_TARGET_DIRECTORY))) {

        //
        // this is an IRP_MJ_CREATE operation for a target of a rename.
        // tell the postCreate callback we'd like to break. Use the 
        // CompletionContext like a BOOLEAN variable.
        //

        *CompletionContext = (PVOID)TRUE;
    }
….


FLT_POSTOP_CALLBACK_STATUS
PtPostOperationPassThrough (
    __inout PFLT_CALLBACK_DATA Data,
    __in PCFLT_RELATED_OBJECTS FltObjects,
    __in_opt PVOID CompletionContext,
    __in FLT_POST_OPERATION_FLAGS Flags
    )
...
{
    UNREFERENCED_PARAMETER( Data );
    UNREFERENCED_PARAMETER( FltObjects );
    UNREFERENCED_PARAMETER( CompletionContext );
    UNREFERENCED_PARAMETER( Flags );

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

    if ((CompletionContext != NULL) &&
        (Data->IoStatus.Status == STATUS_SUCCESS)) {

        DbgBreakPoint();
    }

    return FLT_POSTOP_FINISHED_PROCESSING;
}

Once I had the minifilter in place I fired up FileTest.exe and renamed a file to C:\rename_target_dir\rename_target_file.bin. My plan was that once it breaks in the debugger I would poke around and see what the FILE_OBJECT->FileName looks like. It turns out that NTFS follows a similar approach, except that the FILE_OBJECT->FileName for the directory that is opened (since when the SL_OPEN_TARGET_DIRECTORY flag is set the CREATE always opens a directory) points to the actual path for the directory. However, that path to the directory comes from the original rename target file path, which is just truncated to not include the file component. Then that final component hidden is used by the file system as the target of the rename in a similar fashion to FastFat. In order to make sure that NTFS actually uses the name in the FileObject I changed in the debugger so that the file name would be "rename_target_fi1e.bin".

1: kd> ?? FltObjects->FileObject
struct _FILE_OBJECT * 0x924a18e8
   +0x000 Type             : 0n5
   +0x002 Size             : 0n128
   +0x004 DeviceObject     : 0x92f0ebc8 _DEVICE_OBJECT
   +0x008 Vpb              : 0x92f0b210 _VPB
   +0x00c FsContext        : 0xa5962d08 Void
   +0x010 FsContext2       : 0xb127a610 Void
   +0x014 SectionObjectPointer : (null) 
   +0x018 PrivateCacheMap  : (null) 
   +0x01c FinalStatus      : 0n0
   +0x020 RelatedFileObject : (null) 
   +0x024 LockOperation    : 0 ''
   +0x025 DeletePending    : 0 ''
   +0x026 ReadAccess       : 0 ''
   +0x027 WriteAccess      : 0x1 ''
   +0x028 DeleteAccess     : 0 ''
   +0x029 SharedRead       : 0x1 ''
   +0x02a SharedWrite      : 0x1 ''
   +0x02b SharedDelete     : 0 ''
   +0x02c Flags            : 0
   +0x030 FileName         : _UNICODE_STRING "\rename_target_dir"
   +0x038 CurrentByteOffset : _LARGE_INTEGER 0x0
   +0x040 Waiters          : 0
   +0x044 Busy             : 0
   +0x048 LastLock         : (null) 
   +0x04c Lock             : _KEVENT
   +0x05c Event            : _KEVENT
   +0x06c CompletionContext : (null) 
   +0x070 IrpListLock      : 0
   +0x074 IrpList          : _LIST_ENTRY [ 0x924a195c - 0x924a195c ]
   +0x07c FileObjectExtension : (null) 
1: kd> ?? FltObjects->FileObject->FileName
struct _UNICODE_STRING
 "\rename_target_dir"
   +0x000 Length           : 0x24
   +0x002 MaximumLength    : 0x52
   +0x004 Buffer           : 0xb0796550  "\rename_target_dir"
1: kd> db 0xb0796550 L0x52
b0796550  5c 00 72 00 65 00 6e 00-61 00 6d 00 65 00 5f 00  \.r.e.n.a.m.e._.
b0796560  74 00 61 00 72 00 67 00-65 00 74 00 5f 00 64 00  t.a.r.g.e.t._.d.
b0796570  69 00 72 00 5c 00 72 00-65 00 6e 00 61 00 6d 00  i.r.\.r.e.n.a.m.
b0796580  65 00 5f 00 74 00 61 00-72 00 67 00 65 00 74 00  e._.t.a.r.g.e.t.
b0796590  5f 00 66 00 69 00 6c 00-65 00 2e 00 62 00 69 00  _.f.i.l.e...b.i.
b07965a0  6e 00                                            n.
1: kd> eb b0796596 0x31
1: kd> db 0xb0796550 L0x52
b0796550  5c 00 72 00 65 00 6e 00-61 00 6d 00 65 00 5f 00  \.r.e.n.a.m.e._.
b0796560  74 00 61 00 72 00 67 00-65 00 74 00 5f 00 64 00  t.a.r.g.e.t._.d.
b0796570  69 00 72 00 5c 00 72 00-65 00 6e 00 61 00 6d 00  i.r.\.r.e.n.a.m.
b0796580  65 00 5f 00 74 00 61 00-72 00 67 00 65 00 74 00  e._.t.a.r.g.e.t.
b0796590  5f 00 66 00 69 00 31 00-65 00 2e 00 62 00 69 00  _.f.i.1.e...b.i.
b07965a0  6e 00                                            n.
1: kd> g

After continuing execution the file on the file system was renamed to the new name that I had changed in the debugger and it thus validated my theory (well, almost.. It was still possible that the associated IRP_MJ_SET_INFORMATION was somehow initialized to use the buffer I've modified from the FileObject->FileName so I debugged and made sure that's not the case…)

Here are some more things that make renames difficult to deal with in a file system filter (in addition to the list at the end of last post):

  • A file system filter that needs to redirect a rename operation can't just rely on changing the destination name in the FILE_RENAME_INFORMATION buffer for renames where the FILE_RENAME_INFORMATION->RootDirectory is not null, since some file systems ignore that. Instead it needs to make sure that it creates a handle to the parent directory using the IO_OPEN_TARGET_DIRECTORY flag. However a filter must also change the FILE_RENAME_INFORMATION because another filter might rely on that (FltMgr's FltGetDestinationFileNameInformation for example) so data in the buffer and the data that the file system will use must be kept in sync.
  • There are issues with FltSetInformationFile where calling it for a FileRenameInformation will fail because of a sharing violation. If someone has run into this problem and has figured out why it happens or if they have some steps to reproduce it so that I could investigate it myself I'd appreciate if they contacted me offline .