Here is a small function that I've added to everyone's favorite WDK sample, Passthrough (please note the hardcoded path to E:):
I'm simply calling this function for each IRP_MJ_CREATE (in PtPreOperationPassThrough)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; }
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:... if (Data->Iopb->MajorFunction == IRP_MJ_CREATE) { status = MyOpenVolume( FltObjects ); } return FLT_PREOP_SUCCESS_WITH_CALLBACK; ...
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.1: kd> !fileobj 935afbf0 Device Object: 0x92f0da60 \Driver\volmgr Vpb is NULL Event signalled Flags: 0x40800 Direct Device Open Handle Created CurrentByteOffset: 0
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:
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> !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:
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.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
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().
Wow, what a mess...This is the sort of behavior that makes me crazy, it's a classic example of why we need all return values documented. Though, in this case, a different return value or an assert would have been nice.
ReplyDeleteGreat post though, thanks!
-scott