Thursday, July 28, 2011

Rolling your Own IO in Minifilters

The topic of how to issue an "IRP" in a minifilter does occasionally come up. In general this is required when there is no Flt API to perform a certain operation (for example the well known case of FltQueryDirectoryFile() in XP) or when the Flt API does not provide some feature that the caller needs (see my example in a previous post about FltSetInformationFile not issuing a create with FILE_SHARE_DELETE). There two main articles that I believe any file system filter writer should be familiar with, OSR's Rolling Your Own - Building IRPs to Perform I/O and Microsoft's own document about IO in minifilters, Minifilter Generated I/O.

As I've said before, a minifilter can be either in the context of an IO operation or not. When the minifilter is not in the context of an IO operation or when it wants to send a request to a different device then the one it's currently processing IO on the minifilter can usually issue an IRP directly (though this is rather rare so if you need to issue an actual IRP from a minifilter then make sure there isn't something wrong about your design; layering problems have a nasty habit of not showing up until testing interop with other minifilters, which doesn't usually happen in early testing). In other words, a minifilter only needs to issue IO using the FltMgr framework in the same circumstances when it would call a FltXxx routine, which I've discussed here. There isn't anything special to issuing an IRP from a minifilter so I'll focus only on issuing a FLT_CALLBACK_DATA type IO.

The steps involved in issuing a FLT_CALLBACK_DATA are pretty straightforward:

  1. Allocate a FLT_CALLBACK_DATA structure by calling FltAllocateCallbackData().
  2. Initialize the FLT_CALLBACK_DATA structure for your IO. This primarily requires setting up the FLT_CALLBACK_DATA->Iopb structure. Don't forget to set up the MajorFunction and MinorFunction members.
  3. Send the request down to the filters below using FltPerformSynchronousIo() or FltPerformAsynchronousIo().
  4. Either free the FLT_CALLBACK_DATA (FltFreeCallbackData()) or reuse it (FltReuseCallbackData()), depending on whether you need to issue additional IO or not.

Here is an example of what the code looks like to issue your own call similar to FltQueryDirectoryFile() (which is the code I have in my minifilters that need to run on XP):

NTSTATUS
MyFltQueryDirectoryFile(
    __in PFLT_INSTANCE  Instance,
    __in PFILE_OBJECT  FileObject,
    __out PVOID FileInformation,
    __in ULONG Length,
    __in FILE_INFORMATION_CLASS  FileInformationClass,
    __in BOOLEAN  ReturnSingleEntry,
    __in_opt PUNICODE_STRING  FileName,
    __in BOOLEAN  RestartScan,
    __out_opt PULONG  LengthReturned
    )
{
    NTSTATUS status = STATUS_SUCCESS;
    PFLT_CALLBACK_DATA callbackData = NULL;
    PFLT_PARAMETERS params = NULL;

    status = FltAllocateCallbackData( Instance,
                                      FileObject,
                                      &callbackData );
    if (!NT_SUCCESS(status)) {

        return status;
    }

    callbackData->Iopb->MajorFunction = IRP_MJ_DIRECTORY_CONTROL;
    callbackData->Iopb->MinorFunction = IRP_MN_QUERY_DIRECTORY;

    if (RestartScan) {

        SetFlag( callbackData->Iopb->OperationFlags, SL_RESTART_SCAN );
    }

    if (ReturnSingleEntry) {

        SetFlag( callbackData->Iopb->OperationFlags, SL_RETURN_SINGLE_ENTRY );
    }

    params = &callbackData->Iopb->Parameters;
    params->DirectoryControl.QueryDirectory.Length = Length;
    params->DirectoryControl.QueryDirectory.FileName = FileName;
    params->DirectoryControl.QueryDirectory.FileInformationClass = FileInformationClass;
    params->DirectoryControl.QueryDirectory.FileIndex = 0;
    params->DirectoryControl.QueryDirectory.DirectoryBuffer = FileInformation;
    params->DirectoryControl.QueryDirectory.MdlAddress = NULL;

    FltPerformSynchronousIo( callbackData );

    status = callbackData->IoStatus.Status;    

    if (LengthReturned != NULL) {

        *LengthReturned = (ULONG)(callbackData->IoStatus.Information);
    }

    FltFreeCallbackData( callbackData );

    return status;
}

The MSDN documentation for the APIs is pretty thorough and combined with the powerpoint presentation from MS I think it covers the subject matter pretty well. However, there are some things I'd like to emphasize:

  • You simply can't issue an IRP_MJ_CREATE this way, use FltCreateFile(Ex(2)).
  • Both FltPerformSynchronousIo() and FltPerformAsynchronousIo() will set the FLT_CALLBACK_DATA->IoStatus.Status to reflect the status of the operation. Unfortunately, it's impossible to tell whether the request failed in FltMgr or if it was actually sent down and it failed in a lower layer. However, in the debugger one can tell whether the request failed in a lower layer by looking at the IRP associated with the FLT_CALLBACK_DATA. The filter allocated FLT_CALLBACK_DATA starts without being associated with an IRP so if the IRP is still NULL then that's an indication that the request failed in FltMgr. If the IRP is not null then it's possible to tell whether the IRP is completed or not and to see the status of the operation.
  • FltPerformAsynchronousIo() will ALWAYS call the asynchronous completion routine. Basically, once a minifilter calls FltPerformAsynchronousIo() it is guaranteed one call to the async completion routine no matter what. So don't make assumptions that the async completion routine won't be called if the request fails in any way.
  • The best way that I've found to figure out how to initialize a FLT_CALLBACK_DATA structure for a certain operation is to filter that operation and see what a FLT_CALLBACK_DATA generated by the FltMgr for an existing IRP looks like.
  • For a call to FltPerformAsynchronousIo() that returned STATUS_PENDING, make sure to not free the FLT_CALLBACK_DATA until the IO actually completes. In fact, a good strategy is to call FltFreeCallbackData() from the async completion routine, which is guaranteed to be called only after the IO is complete.
  • This could be a pretty useful feature to preallocate IRPs in the event that FltAllocateCallbackData() or some other FltXxx API fail because of low system resources and the filter wants to try to implement forward progress (for more discussion on forward progress in general see this page and the RamDisk WDK sample). However, since FltAllocateCallbackData() doesn't allocate the IRP associated for the FLT_CALLBACK_DATA structure, it's possible that even if one preallocates some FLT_CALLBACK_DATA structures to use for forward progress, the calls to FltPerformSynchronousIo() and FltPerformAsynchronousIo() might still fail when trying to allocate the IRP. This is why in Win7 FltMgr introduced FltAllocateCallbackDataEx() which allows a minifilter to preallocate a FLT_CALLBACK_DATA that is guaranteed to preallocate all the necessary memory thus enabling forward progress in a low-memory situation (see the explanation for the FLT_ALLOCATE_CALLBACK_DATA_PREALLOCATE_ALL_MEMORY flag).