Thursday, September 6, 2012

Completing Information Queries in File System Filters

Completing IRP_MJ_QUERY_INFORMATION requests isn't a terribly difficult task but I wanted to discuss some of the basics as well as share some code to show how a minifilter might go about doing this. I've already discussed some of the more advanced topics related to this in my previous posts on Filters And IRP_MJ_QUERY_INFORMATION and on Setting IoStatus.Information.

There are many information classes and many of them can be found on the documentation page for the ZwQueryInformationFile function, though the complete list is documented on the 2.4 File Information Classes page in the MS_FSCC document. Please note that not all of them can be used with IRP_MJ_QUERY_INFORMATION. However, for this post I'll only be focusing on those that can.

Pretty much each information class has an associated structure. For example, the information class of FileAlignmentInformation has the associated FILE_ALIGNMENT_INFORMATION structure. Some of these structures have a fixed size so in that case all a filter has to do is to populate the information and complete the request:

        switch ( Data->Iopb->Parameters.QueryFileInformation.FileInformationClass ) {

            case FilePositionInformation:
                    PFILE_POSITION_INFORMATION positionInfo = Data->Iopb->Parameters.QueryFileInformation.InfoBuffer;

                    // retrieve the current position from the file object

                    positionInfo->CurrentByteOffset = FltObjects->FileObject->CurrentByteOffset;

                    // then complete the operation

                    Data->IoStatus.Status = STATUS_SUCCESS;
                    Data->IoStatus.Information = sizeof(positionInfo->CurrentByteOffset);
                    callbackStatus = FLT_PREOP_COMPLETE;
 return callbackStatus;

The filter can expect that a buffer of the appropriate size is received, otherwise the IO manager will reject the operation with STATUS_INFO_LENGTH_MISMATCH. This is done inside the IO manager, where the IO manager has an array of minimum sizes for each information class (can be seen in the debugger as nt!IopQueryOperationLength) and if the size is smaller than that then the request will fail with the error code I mentioned:

0: kd> u nt!NtQueryInformationFile+0x36 L0xB
82aa87dc 8b4518          mov     eax,dword ptr [ebp+18h]
82aa87df 83f838          cmp     eax,38h
82aa87e2 7377            jae     nt!NtQueryInformationFile+0xb5 (82aa885b)
82aa87e4 8a80d4a0a782    mov     al,byte ptr nt!IopQueryOperationLength (82a7a0d4)[eax]
82aa87ea 84c0            test    al,al
82aa87ec 746d            je      nt!NtQueryInformationFile+0xb5 (82aa885b)
82aa87ee 0fb6c0          movzx   eax,al
82aa87f1 394514          cmp     dword ptr [ebp+14h],eax
82aa87f4 730a            jae     nt!NtQueryInformationFile+0x5a (82aa8800)
82aa87f6 b8040000c0      mov     eax,0C0000004h
82aa87fb e98e080000      jmp     nt!NtQueryInformationFile+0x8e3 (82aa908e)

Now, there are certain information classes that have variable length. In general this happens with classes that contain names (for example, FileNameInformation) or that return certain lists (like FileStreamInformation that returns all the alternate data streams for the file). In these cases it's impossible for the IO manager to know what the required size might be without querying the file system and so file systems and file system filters will receive requests with what is possibly an insufficient buffer size (there are apps that allocate huge buffer just to avoid reissuing a request, but there are many callers that don't do that; I generally prefer to issue a request with enough buffer in cases where that's possible, for example most files don't have Alternate Data Streams so it's possible to calculate the size of the buffer). Those requests will be failed by the file system (or filter) with the STATUS_BUFFER_OVERFLOW status (which is actually a warning, not an error!). However, different information classes have different requirements about how the structure has to be populated in such cases. There are some structures (like FILE_NAME_INFORMATION) that have a field where the caller can fill in the required size so that the caller can just allocate a buffer of the appropriate size and resend the request. Other structures don't have any such field (like FILE_STREAM_INFORMATION) and so any caller must simply increase the size of the buffer without knowing in advance if it will be sufficient (I personally double the size of the buffer for each iteration but there are other ways of doing it) and then they must issue the request and see whether it was enough.

Finally, there are certain requirements for each information class about how to fill in the buffer even in the event when the request is going to be failed with STATUS_BUFFER_OVERFLOW. For example, for FileNameInformation the buffer must be filled with as many characters as will fit the buffer whereas the FILE_STREAM_INFORMATION must be filled only with complete records. These type of requirements make it necessary for whoever completes the request to be able to specify how much of the buffer actually contains real information. This can be achieved by setting the IoStatus.Information member to the number of bytes to return the caller. The IO manager will then copy the specified number of bytes to the caller's buffer. It is very important to get this right because returning a larger number here will result in the IO manager copying more bytes than were actually written by the file system or filter and is a security problem. For more details on this see Doron's post on How to return the number of bytes required for a subsequent operation. Of course, the specifics of how the buffer must be set up for each class are documented in the pages for each information class or associated structure, so whenever dealing with this subject make sure to re-read those pages and pay great attention to the details.

I wanted to show some more code on how to do this, but there is plenty of code that shows how this is done in the file system sources that come with the WDK. I found the CDFS sources to be a bit more clear for this specific topic, where CdCommonQueryInfo pretty much does it all and shows exactly how to set things for most information classes. FastFat can be a bit more convoluted at times.

Finally, I want to say that whenever I have to deal with this I find it extremely useful to just use FileTest and then query the information class I want while supplying different sized buffers (starting with 1 byte, then 2 bytes and so on) and make sure that I understand exactly what the file system returns without my filter present and how things look with the filter installed.