Thursday, March 24, 2011

Names in Minifilters - Using FltGetTunneledName

I'd like to start a longer set of posts on the topic of names in minifilters and using FltMgr name APIs (as always, fell free to submit suggestions and questions using the comment mechanism). This post is the first in that series and it's a topic that I've seen come up a lot. It's a pretty well documented feature but it still seems to be less known. The main articles on it (that I'm aware of) are:

This topic is very important to minifilter writers that have minifilters that use names. In particular minifilters that call FltGetFileNameInformation with FLT_FILE_NAME_NORMALIZED in preCreate (and there seem to be quite a few of those despite the various issues associated with this design) and in preSetInformation. But to explain why this is an issue we need to look at FltMgr's behavior in preCreate and during IRP_MJ_SET_INFORMATION.
As you're probably aware, FltMgr caches the names it generates every time someone asks for FltGetFileNameInformation. Because name generation is pretty expensive, a cache is a very good idea and performance does benefit greatly especially on system with multiple minifilters installed (and since Windows now ships with 2 minifilters and most people use anti-virus software which also use file system filters you can imagine that the performance benefits add up). The name cache is stored in a context associated with a stream (but it might be per FILE_OBJECT if there are multiple hardlinks to the file that the stream belongs to). However, in preCreate the stream is not yet opened. The IRP_MJ_CREATE operation must complete in the file system for the stream to be known and so  FltMgr can't use the name cache in preCreate. This has two major implications:
  1. Performance will be affected since the name will have to be generated for every single IRP_MJ_CREATE (which is a pretty big reason why one shouldn't query names in preCreate if they can avoid it).
  2. Not only can the name not be looked up in the cache, but it also can't be stored in the cache. So now consider the case of 3 minifilters that all query the name in preCreate. If FltMgr didn’t cache the name at all it would have to build it three times. FltMgr does what it can in this case and it caches the name in an internal cache associated with the IRP_MJ_CREATE operation. Then, once the operation completes, the cache is transparently moved to the stream cache.

One other thing to consider is that the name generated in preCreate, even if the caller asks for FLT_FILE_NAME_NORMALIZED, might still contain a short name. This can happen when the IRP_MJ_CREATE is trying to create a new file (that doesn't exist yet) specifying only the short name. FltMgr tries to normalize the name and it gets a normalized path for the parent directory of that file, but the file itself is not in the directory and there is nothing FltMgr can do except return the short name to the caller for that file (the final component). This is in fact where the Name Tunneling Cache comes into play. In this case when the created reaches the file system, if it finds the short file name in the cache it will create the file with the long name from the cache as well. But as we've explained before the minifilter might have a normalized name from the preCreate which no longer matches the file.
In order to show when this happens (and to show some code using this function), I wrote a small modification on top of the passthrough minifilter sample from the WDK:

 FLT_PREOP_CALLBACK_STATUS  
 PtPreOperationPassThrough (  
   __inout PFLT_CALLBACK_DATA Data,  
   __in PCFLT_RELATED_OBJECTS FltObjects,  
   __deref_out_opt PVOID *CompletionContext  
   )  
 /*++  
 Routine Description:  
   This routine is the main pre-operation dispatch routine for this  
   miniFilter. Since this is just a simple passThrough miniFilter it  
   does not do anything with the callbackData but rather return  
   FLT_PREOP_SUCCESS_WITH_CALLBACK thereby passing it down to the next  
   miniFilter in the chain.  
   This is non-pageable because it could be called on the paging path  
 Arguments:  
   Data - Pointer to the filter callbackData that is passed to us.  
   FltObjects - Pointer to the FLT_RELATED_OBJECTS data structure containing  
     opaque handles to this filter, instance, its associated volume and  
     file object.  
   CompletionContext - The context for the completion routine for this  
     operation.  
 Return Value:  
   The return value is the status of the operation.  
 --*/  
 {  
   NTSTATUS status;  
   PFLT_FILE_NAME_INFORMATION name;  
   PT_DBG_PRINT( PTDBG_TRACE_ROUTINES,  
          ("PassThrough!PtPreOperationPassThrough: Entered\n") );  
   if (Data->Iopb->MajorFunction == IRP_MJ_CREATE) {  
     //  
     // this is a preCreate, get the name.  
     //  
     status = FltGetFileNameInformation( Data,   
                       FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT,   
                       &name );  
     if (NT_SUCCESS(status)) {  
       //  
       // send it to postCreate  
       //  
       *CompletionContext = (PVOID) name;  
     } else {  
       PT_DBG_PRINT( PTDBG_TRACE_FILE_NAME_FAILURES,  
              ("PassThrough!PtPreOperationPassThrough: Failed to get file name, status=%08x\n",  
               status) );  
     }  
   }  
   //  
   // See if this is an operation we would like the operation status  
   // for. If so request it.  
   //  
   // NOTE: most filters do NOT need to do this. You only need to make  
   //    this call if, for example, you need to know if the oplock was  
   //    actually granted.  
   //  
   if (PtDoRequestOperationStatus( Data )) {  
     status = FltRequestOperationStatusCallback( Data,  
                           PtOperationStatusCallback,  
                           (PVOID)(++OperationStatusCtx) );  
     if (!NT_SUCCESS(status)) {  
       PT_DBG_PRINT( PTDBG_TRACE_OPERATION_STATUS,  
              ("PassThrough!PtPreOperationPassThrough: FltRequestOperationStatusCallback Failed, status=%08x\n",  
               status) );  
     }  
   }  
   return FLT_PREOP_SUCCESS_WITH_CALLBACK;  
 }  
 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  
   )  
 /*++  
 Routine Description:  
   This routine is the post-operation completion routine for this  
   miniFilter.  
   This is non-pageable because it may be called at DPC level.  
 Arguments:  
   Data - Pointer to the filter callbackData that is passed to us.  
   FltObjects - Pointer to the FLT_RELATED_OBJECTS data structure containing  
     opaque handles to this filter, instance, its associated volume and  
     file object.  
   CompletionContext - The completion context set in the pre-operation routine.  
   Flags - Denotes whether the completion is successful or is being drained.  
 Return Value:  
   The return value is the status of the operation.  
 --*/  
 {  
   NTSTATUS status;  
   PFLT_FILE_NAME_INFORMATION name = NULL;  
   PFLT_FILE_NAME_INFORMATION realName = NULL;  
   UNREFERENCED_PARAMETER( FltObjects );  
   UNREFERENCED_PARAMETER( Flags );  
   PT_DBG_PRINT( PTDBG_TRACE_ROUTINES,  
          ("PassThrough!PtPostOperationPassThrough: Entered\n") );  
   name = (PFLT_FILE_NAME_INFORMATION)CompletionContext;  
   if (name != NULL) {  
     //  
     // we got a name from preCreate. check if it's a tunneled name.  
     //  
     status = FltGetTunneledName( Data,   
                    name,  
                    &realName );  
     if (NT_SUCCESS(status)) {  
       //  
       // see if we actually got a tunneled name.  
       //  
       if (realName != NULL) {  
         DbgPrint( "Got a tunneled name: File:%p, original name: %wZ, real name:%wZ\n",  
              FltObjects->FileObject,  
              &name->Name,  
              &realName->Name );  
       }  
     }  
     FltReleaseFileNameInformation( name );  
     if (realName != NULL) {  
       FltReleaseFileNameInformation( realName );  
     }  
   }  
   return FLT_POSTOP_FINISHED_PROCESSING;  
 }  
To recap, you need to care about this only if your minifilter queries normalized names in preCreate or preSetInformation and then uses them in the postOp callback. Also, you might need to worry about this if you keep a normalized name in a context. However, this is only a concern for renames (you can't use a context in preCreate) and for minifilters that keep a name in a context the easiest way is to update it in postRename and not do anything in preRename.

4 comments:

  1. This is definitely useful, particularly the performance implications of doing name lookups in IRP_MJ_CREATE. Any chance you can post something about GenerateFileNameCallback and NormalizeNameComponentCallback, and various 'gotcha' things that might affect one if you start changing the filenames?

    ReplyDelete
  2. I plan to show how to implement a simple name provider (and thus show a basic implementation for GenerateFileNameCallback and NormalizeNameComponentCallback) pretty soon.
    Unfortunately, the gotchas depend primarily on the architecture of the solution so there is no way to cover them all.

    ReplyDelete
  3. I've seen a lot of mention of the issues of querying normalized names in PreCreate. I took that to heart and query for the normalized name in PostCreate, as I don't really care when I get the name, just so long as I can get it. It seems to work fine, but are there issues with that approach? If so, what are they?

    ReplyDelete
  4. Hi Phil.

    This has the potential to be a long discussion. I wrote a post a while back on using names in file system filters (http://fsfilters.blogspot.com/2010/02/names-and-file-systems-filters.html) in which i wrote about some of the problems i see with various approaches.
    If you compare getting a normalized name in postCreate to the same in preCreate, then I don't think there are any additional issues except that some designs really need a normalized name in preCreate. For example some solutions that use names to implement policy (files under a certain path must be read-only). However, even this could be implemented so that the opened name is used and then the lookup can take a non-normalized path for example.
    So to answer your question, I don't know of any additional issues introduced by querying a normalized name in postCreate.

    ReplyDelete