Thursday, June 9, 2011

Handling IRP_MJ_NETWORK_QUERY_OPEN in a Minifilter

There are a couple of things that are worth mentioning about IRP_MJ_NETWORK_QUERY_OPEN from a minifilter writer perspective. The first interesting thing is that IRP_MJ_NETWORK_QUERY_OPEN is not the minifilter virtual IRP for FastIoQueryNetworkOpenInfo as the name might suggest. The way FltMgr dispatches FastIoQueryNetworkOpenInfo is by sending an IRP_MJ_QUERY_INFORMATION FLT_CALLBACK_DATA structure with the FLTFL_CALLBACK_DATA_FAST_IO_OPERATION flag set. IRP_MJ_NETWORK_QUERY_OPEN is in fact the operation for FastIoQueryOpen (which is a FastIo operation that doesn't really translate into any of the IRP type operations so that's why the IRP_MJ_NETWORK_QUERY_OPEN virtual IRP was added).

So in order to discuss the interesting aspects of IRP_MJ_NETWORK_QUERY_OPEN we need to discuss when FastIoQueryOpen is called and why. The main reason for this FastIo is that it wraps a set of operations (IRP_MJ_CREATE, IRP_MJ_QUERY_INFORMATION(FILE_NETWORK_OPEN_INFORMATION), IRP_MJ_CLEANUP,IRP_MJ_CLOSE) into one call, allowing the caller to get information about a file or just to check whether the file exists directly by name, without actually opening the file (see http://blogs.msdn.com/b/oldnewthing/archive/2007/10/23/5612082.aspx). This might not look like much but when dealing with a network protocol being able to do multiple things in one request can improve performance a lot. So it seems that this particular FastIo was introduced to really optimize this one thing, getting the FILE_NETWORK_OPEN_INFORMATION for a file by name.

In kernel mode one way to generate this FastIo (but not the only way) is by calling IoFastQueryNetworkAttributes(). This function is not documented in MSDN but it's pretty straightforward to figure out (though minifilters should not call this API since it breaks layering… It might be useful for testing IRP_MJ_NETWORK_QUERY_OPEN so that's way I'm mentioning it here). The way it works is by eventually calling IopParseDevice with an OPEN_PACKET that has the QueryOnly flag set. What happens in this case is that IopParseDevice allocates an IRP_MJ_CREATE IRP and passes it as a parameter to the FastIoQueryOpen callback. If the call is successful then IopParseDevice returns, if it is not then the already allocated IRP_MJ_CREATE IRP is sent down the stack the usual way.

One thing that is very interesting is the way the callbacks are defined:

typedef union _FLT_PARAMETERS {
  ... ;
  struct {
    PIRP                           Irp;
    PFILE_NETWORK_OPEN_INFORMATION NetworkInformation;
  } NetworkQueryOpen;
  ... ;
} FLT_PARAMETERS, *PFLT_PARAMETERS;

This snippet is taken from the MSDN page: FLT_PARAMETERS for IRP_MJ_NETWORK_QUERY_OPEN Union. There are quite a few things mentioned on that page that are interesting:

  • First and foremost there is the IRP parameter. This is interesting because it's the only place in a minifilter where one gets to see an IRP. I remember hearing that there was some debate in the FltMgr team whether this IRP should be wrapped in a FLT_CALLBACK_DATA (or maybe just an FLT_IO_PARAMETER_BLOCK), but it wouldn't be pretty no matter what and having an IRP here doesn't really change anything as it should be treated just like a structure (as opposed to a regular IRP on which methods like IoCallDriver can be called).
  • Then there is the note that "The file object associated with IRP_MJ_NETWORK_QUERY_OPEN is a stack-based object.A filter registered for the NetworkQueryOpen callback must not reference this object. That is, do not call ObReferenceObject or ObDereferenceObject on this stack-based file object. Also, do not save a pointer to the object.". The main idea here is that this particular operation was designed to be fast and obviously just allocating the FILE_OBJECT structure on the stack and then initializing some members in it is faster than allocating a full FILE_OBJECT. Also, the FILE_OBJECT is not going to be used anywhere, it is simply a way to let the file system know the path to the file for which the FILE_NETWORK_OPEN_INFORMATION is required. If the FastIo doesn’t work then when the full IRP_MJ_CREATE is sent down a full FILE_OBJECT is allocated.
  • Also there is a mention that "A filter must register for this operation". This is not true. A minifilter can safely ignore this operation.

So let's talk about some of the things minifilters developers might need to be aware of in this case:

  • When the IRP_MJ_NETWORK_QUERY_OPEN callback is called, the FILE_OBJECT is not opened and the minifilter is technically in a preCreate state. So trying to use any FILE_OBJECT related context (like FileContext, StreamContext and StreamHandleContext) will not work.
  • Moreover, the FILE_OBJECT is not a real FILE_OBJECT so there are some things that don't really apply to it (maybe not all the flags or all the members are set like they would on a real FILE_OBJECT - I didn't actually ever check; at least reference counting seems to not work as the note indicates).
  • When completing IRP_MJ_NETWORK_QUERY_OPEN with something other than STATUS_FLT_DISALLOW_FAST_IO (such as when the minifilter actually completes the operation successfully), the minifilter must set the status into IRP->IoStatus and not into the FLT_CALLBACK_DATA. The FLT_CALLBACK_DATA represents the current operation and the status is the status for the FastIo and not the status for the IRP. For example a minifilter that wants to complete IRP_MJ_NETWORK_QUERY_OPEN call to indicate that the file doesn't exist should set Irp->IoStatus.Status to the appropriate status (like STATUS_OBJECT_NAME_NOT_FOUND or STATUS_OBJECT_PATH_NOT_FOUND or whatever) and the CallbackData->IoStatus.Status to STATUS_SUCCESS to indicate that the FastIo has completed successfully.
  • Finally there are some layering issues with IRP_MJ_NETWORK_QUERY_OPEN which make it possible for minifilters to have their callback invoked for operations that should be layered below them. These are rather rare cases but they can happen and are painful to fix and to debug.

So as you can see properly handling IRP_MJ_NETWORK_QUERY_OPEN is a rather complicated deal. Moreover, the LUAFV minifilter that ships with Windows always returns STATUS_FLT_DISALLOW_FAST_IO for it, effectively failing the request and forcing it down the regular IRP_MJ_CREATE path. My recommendation is that unless a minifilter runs in an environment where LUAFV is not present (some server builds maybe) and the minifilter does very minimal processing in the handler for IRP_MJ_NETWORK_QUERY_OPEN (because if it does any heavy lifting like calling FltGetFileNameInformation() or something more complicated then it's not really "Fast"Io anymore anyway and there is no performance benefit in supporting this code path) then the minifilter is better off blindly returning STATUS_FLT_DISALLOW_FAST_IO and dealing with request when it comes down the IRP_MJ_CREATE path.