Thursday, May 3, 2012

Identifying the Target Device for an Opened File Object

As you may have guessed from the title, in this post I plan to cover how the OS knows to which device a certain request needs to go for a certain FILE_OBJECT. We spent quite a bit of time on this blog talking about how IRP_MJ_CREATE works and how the actual DEVICE_OBJECT and file contents are found in that case, but once the IRP_MJ_CREATE has successfully completed and the FILE_OBJECT is initialized, subsequent requests for that FILE_OBJECT take a different path through the system.
So first let's look at the FILE_OBJECT structure and its fields (this is x86 Win7):
0: kd> dt nt!_FILE_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x004 DeviceObject     : Ptr32 _DEVICE_OBJECT
   +0x008 Vpb              : Ptr32 _VPB
   +0x00c FsContext        : Ptr32 Void
   +0x010 FsContext2       : Ptr32 Void
   +0x014 SectionObjectPointer : Ptr32 _SECTION_OBJECT_POINTERS
   +0x018 PrivateCacheMap  : Ptr32 Void
   +0x01c FinalStatus      : Int4B
   +0x020 RelatedFileObject : Ptr32 _FILE_OBJECT
   +0x024 LockOperation    : UChar
   +0x025 DeletePending    : UChar
   +0x026 ReadAccess       : UChar
   +0x027 WriteAccess      : UChar
   +0x028 DeleteAccess     : UChar
   +0x029 SharedRead       : UChar
   +0x02a SharedWrite      : UChar
   +0x02b SharedDelete     : UChar
   +0x02c Flags            : Uint4B
   +0x030 FileName         : _UNICODE_STRING
   +0x038 CurrentByteOffset : _LARGE_INTEGER
   +0x040 Waiters          : Uint4B
   +0x044 Busy             : Uint4B
   +0x048 LastLock         : Ptr32 Void
   +0x04c Lock             : _KEVENT
   +0x05c Event            : _KEVENT
   +0x06c CompletionContext : Ptr32 _IO_COMPLETION_CONTEXT
   +0x070 IrpListLock      : Uint4B
   +0x074 IrpList          : _LIST_ENTRY
   +0x07c FileObjectExtension : Ptr32 Void
As you can see the FILE_OBJECT has a DeviceObject structure that (at least according to the name) should what we're looking for. This is actually true for FILE_OBJECTs for a direct device open but is not really the case for FILE_OBJECTs that represent files on a file system (which is the main focus of this blog). Still, if you have one of those FILE_OBJECTs for a direct device open then the proper way to get the target DEVICE_OBJECT is by calling IoGetAttachedDevice() or IoGetAttachedDeviceReference() on the FILE_OBJECT->DeviceObject member. Because DEVICE_OBJECTs can be stacked in NT it is important to note that both these functions walk the stack of devices and return the topmost one.
What about FILE_OBJECTs that represent files on file system ? In that case the FILE_OBJECT->DeviceObject member actually points to the storage DEVICE_OBJECT and not the filesystem DEVICE_OBJECT, which is where the request should go. So in that case the IO manager uses the FILE_OBJECT->Vpb->DeviceObject to get the file system device object that is mounted on the actual storage device. Once the file system device is found IoGetAttachedDevice() should be called to get the topmost DEVICE_OBJECT.
Things aren't that complicated so far, but there is yet another twist. If the FILE_OBJECT was opened with a device hint (IoCreateFileSpecifyDeviceObjectHint() or IoCreateFileEx() or any of the FltCreateFile functions with an Instance parameter that is not NULL) then the IO manager remembers the DEVICE_OBJECT hint in the FILE_OBJECT and it uses that as the target DEVICE_OBJECT.
Fortunately, all this logic is hidden inside the IoGetRelatedDeviceObject() so developers don't really need to implement it. It is useful, however, to know how this works for debugging things.
I think it is worth mentioning is that inside IoGetRelatedDeviceObject() the IO manager also checks that the hint DEVICE_OBJECT (if the FILE_OBJECT was opened in that way) is actually attached to the mounted file system stack for that volume. This check is obviously unnecessary when there is no hint because the IO manager returns the mounted file system stack.
There is yet another function that does something pretty similar to this, IoGetBaseFileSystemDeviceObject(). This function is undocumented but what it does is very similar to what IoGetRelatedDeviceObject() except that it doesn't return the topmost DEVICE_OBJECT on the file system stack, but rather the lowest one. Should you need this functionality and don't want to use undocumented functions, the same thing can be achieved by calling IoGetRelatedDeviceObject() followed by IoGetDeviceAttachmentBaseRef().
Finally, when working with these function always pay attention to whether the DEVICE_OBJECTs you get are referenced or not. The documentation isn't always clear and so the debugger can really come in handy (just look at the PointerCount on the DEVICE_OBJECT before and after you call the function). Anyway, from the ones I mentioned here, IoGetAttachedDevice(), IoGetRelatedDeviceObject() and IoGetBaseFileSystemDeviceObject() don't return a referenced DEVICE_OBJECT, while IoGetAttachedDeviceReference() and IoGetDeviceAttachmentBaseRef() do.