Thursday, July 7, 2011

Opening Volume Handles in Minifilters

This should be a pretty straight-forward topic, right ? After all, FltMgr even provides a function for this, FltOpenVolume. However, a recent post on OSR's NTFSD made me take a deeper look of this issue and there are some interesting things that I found. First, let me say upfront that the real problem of the poster was trying to issue an FSCTL using FltDeviceIoControlFile instead of FltFsControlFile. However, his post was about FltCreateFile failing and looking at the code I couldn't figure out why which is usually a good sign I'm missing something and that I should investigate further.

Here is a small function that I've added to everyone's favorite WDK sample, Passthrough (please note the hardcoded path to E:):

NTSTATUS MyOpenVolume(
    __in PCFLT_RELATED_OBJECTS FltObjects
    )
{
    NTSTATUS status = STATUS_SUCCESS;
    OBJECT_ATTRIBUTES objectAttributes;
    IO_STATUS_BLOCK ioStatus;
    HANDLE volumeHandle = NULL;
    UNICODE_STRING gVolumeRoot = RTL_CONSTANT_STRING(L"\\DosDevices\\E:");

    InitializeObjectAttributes( &objectAttributes, 
                                &gVolumeRoot, 
                                OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, 
                                NULL, 
                                NULL );

    status = FltCreateFile( gFilterHandle, 
                            FltObjects->Instance, 
                            &volumeHandle, 
                            FILE_READ_ATTRIBUTES, 
                            &objectAttributes, 
                            &ioStatus, 
                            NULL, 
                            FILE_ATTRIBUTE_NORMAL, 
                            FILE_SHARE_READ | FILE_SHARE_WRITE, 
                            FILE_OPEN, 
                            0 , 
                            NULL, 
                            0L, 
                            0);  

    if (volumeHandle != NULL) {

        ZwClose(volumeHandle);
    }

    return status;
}

I'm simply calling this function for each IRP_MJ_CREATE (in PtPreOperationPassThrough)

...
    if (Data->Iopb->MajorFunction == IRP_MJ_CREATE) {

        status = MyOpenVolume( FltObjects );        
    }

    return FLT_PREOP_SUCCESS_WITH_CALLBACK;
...

So anyway, FltCreateFile simply fails with STATUS_INVALID_PARAMETER. This was quite unexpected because I had used similar code before, just not in a minifilter (and looking through some old code I was able to confirm that). So I decided to see what would happen if I called ZwCreateFile() instead of FltCreateFile(). To my surprise, it worked using the exact same parameters (well, except of course for the Filter, Instance and Flags parameters). I was surprised that it worked because I was expecting an infinite loop since ZwCreateFile() doesn't target the IRP_MJ_CREATE and so it would go into my create handler again and again… Then my next step was to try to replace ZwCreateFile() with FltCreateFile() and instead of using my instance use a NULL instance so that the request should also go to the top of the stack just like ZwCreateFile() would. But that also failed with STATUS_INVALID_PARAMETER, which was also pretty strange. So I decided to look at the handle I've just opened to see if I notice anything:

1: kd> !fileobj 935afbf0  



Device Object: 0x92f0da60   \Driver\volmgr
Vpb is NULL
Event signalled

Flags:  0x40800
 Direct Device Open
 Handle Created

CurrentByteOffset: 0

There are a couple of things that looked unusual. First, there is no FsContext or FsContext2, meaning they are NULL. Then, the Flags field has the "Direct Device Open" flag (FO_DIRECT_DEVICE_OPEN). Also, there is no FO_VOLUME_OPEN flag even though this should be a volume open. And finally, the VPB is NULL, even though the volume is mounted (this is not obvious from this FO, I just happen to know it's mounted). All this means that the handle I have is in fact a handle to the storage stack volume instead of the file system volume. This is an interesting NT behavior that I had forgotten about. The idea is that opening a volume using certain access rights will open the storage volume without triggering a mount of the file system. This is useful when a driver wants to talk to the actual volume and query some attributes or something of that nature without forcing the file system to be mounted. You can find out more about this behavior on the MSDN page "Common Driver Reliability Issues", if you scroll all the way to "Requests to Create and Open Files and Devices" and then look at the entry for "Relative Open Requests for Direct Device Open Handles". Please note that this is not the same as DASD IO.

So anyway I wanted to see what happens if when calling FltCreateFile() I also request FILE_WRITE_ATTRIBUTES, thus changing the semantics for the IRP_MJ_CREATE and not getting a direct device open. And this time around FltCreateFile() worked. Here is the FILE_OBJECT that got created:

1: kd> !fileobj 94126488  



Device Object: 0x92f0da60   \Driver\volmgr
Vpb: 0x92f09570
Event signalled

Flags:  0x440008
 No Intermediate Buffering
 Handle Created
 Volume Open

FsContext: 0x92fb2e18 FsContext2: 0xa3f08bf8
CurrentByteOffset: 0
Cache Data:
  Section Object Pointers: 9352d4f4
  Shared Cache Map: 00000000


File object extension is at 9305e2f0:

So this is a file system volume open, as we can see from the Volume Open flag (FO_VOLUME OPEN). Also, FsContext and FsContext2 and the VPB are no longer null. Still, it's not clear why FltCreateFile would return STATUS_INVALID_PARAMETER for a direct device open. Once again tracing through IopParseDevice provides the answer:

1: kd> kn
 # ChildEBP RetAddr  
00 a157d5bc 82a77ff2 nt!IopCheckTopDeviceHint+0x5c
01 a157d698 82a5926b nt!IopParseDevice+0x81c
02 a157d714 82a7f2d9 nt!ObpLookupObjectName+0x4fa
03 a157d774 82a7762b nt!ObOpenObjectByName+0x165
04 a157d7f0 82aaee29 nt!IopCreateFile+0x673
05 a157d920 a0ede0e1 nt!IoCreateFileEx+0x9e
06 a157d994 a0ede1d4 PassThrough!MyOpenVolume+0xd1 [c:\temp3\passthrough\passthrough.c @ 409]
07 a157d9ac 96029aeb PassThrough!PtPreOperationPassThrough+0xa4 [c:\temp3\passthrough\passthrough.c @ 873]
WARNING: Frame IP not in any known module. Following frames may be wrong.
08 a157da88 828744bc 0x96029aeb
09 a157daa0 82a786ad nt!IofCallDriver+0x63
0a a157db78 82a5926b nt!IopParseDevice+0xed7
0b a157dbf4 82a7f2d9 nt!ObpLookupObjectName+0x4fa
0c a157dc50 82a7762b nt!ObOpenObjectByName+0x165
0d a157dccc 82ab267e nt!IopCreateFile+0x673
0e a157dd14 8287b44a nt!NtOpenFile+0x2a
0f a157dd14 774764f4 nt!KiFastCallEntry+0x12a
10 0012d958 00439f12 0x774764f4
11 0012d99c 0049a03e 0x439f12
12 0012dbd0 0049b43f 0x49a03e
13 0012dbec 004551cc 0x49b43f
14 0012f590 00491382 0x4551cc
15 0012f5ac 004914e8 0x491382
16 0012f5d0 00491630 0x4914e8
17 0012fe50 0048ecfe 0x491630
18 0012fe94 0048f4bd 0x48ecfe
19 0012ff40 004ed433 0x48f4bd
1a 0012ff88 765e1194 0x4ed433
1b 0012ff94 7748b495 0x765e1194
1c 0012ffd4 7748b468 0x7748b495
1d 0012ffec 00000000 0x7748b468
1: kd> u nt!IopCheckTopDeviceHint+0x5c
nt!IopCheckTopDeviceHint+0x5c:
82a9b354 b80d0000c0      mov     eax,0C000000Dh
82a9b359 5d              pop     ebp
82a9b35a c20400          ret     4
82a9b35d 90              nop
82a9b35e 90              nop
82a9b35f 90              nop
82a9b360 90              nop
82a9b361 90              nop

What is going on here is that nt!IopCheckTopDeviceHint simply fails with STATUS_INVALID_PARAMETER if it's a direct device open. Basically, the combination of targeted IRP_MJ_CREATE (like FltCreateFile() issues when an Instance parameter is specified) and direct device open always fails. But while this explains why ZwCreateFile works, it's not clear why FltCreateFile() with a NULL instance fails. So after another bit of tracing there I discovered that FltCreateFileEx2() (which both FltCreateFile() and FltCreateFileEx() call) fails any request if the FILE_OBJECT it gets has the FO_DIRECT_DEVICE_OPEN flag set.

So before this post gets waaaay too long, let's get to our conclusions:

  • FltCreateFile() simply cannot be used for direct device opens. Minifilter developers can use ZwCreateFile() in this scenario, which is safe because the IRP_MJ_CREATE issued does not go to any file system and so there is no reentrancy. This is the same as opening any non-file system device on the system.
  • Direct device handles are not the same as DASD handles. DASD FILE_OBJECTs have the FO_VOLUME_OPEN flag set and represent an open to the file system volume device, while the direct device FILE_OBJECT have the FO_DIRECT_DEVICE_OPEN flag set and are targeted directly at the storage volume.
  • Try to use FltOpenVolume() instead of rolling your own open volume code.
  • Do not use a direct device handle when issuing FSCTLs, it makes no sense. The FSCTLs must go to the file system device.
  • Do not send FSCTLs using ZwDeviceIoControlFile() or FltDeviceIoControlFile(). Instead one should use ZwFsControlFile() or FltFsControlFile().