Thursday, August 23, 2012

Mount Point Resolution in FltGetFileNameInformation

In this post I'd like to discuss one particular aspect of using FltGetFileNameInformation and how mount point resolution is handled. The issue I want to talk about is mentioned in the documentation for the FLT_FILE_NAME_INFORMATION structure, where in the Remarks section it says that a name is normalized if all the mount points are resolved. This was mentioned in the context of the OSR thread http://www.osronline.com/showThread.CFM?link=230176, where it was suggested that the definition means that opened names would not resolve mount points (see the discussion starting at message 21).
So the plan is to discuss how mount points work and how name normalization works in this context and what's the actual behavior for mount point resolution for normalized and opened names.
A mount point is a special type of reparse point, where it reparses from a directory to the root of a volume. A typical scenario would be mounting a volume like \Device\HarddiskVolume4\ as a folder under \Device\HarddiskVolume3\mnt\ (the volume names are actual names that I'm going to use in my examples later). This can be easily done using mountvol.exe, like so:
C:\>mountvol.exe

Creates, deletes, or lists a volume mount point.
....
....

Possible values for VolumeName along with current mount points are:

    \\?\Volume{a681bf9c-c97b-11de-90d7-806e6f6e6963}\
        *** NO MOUNT POINTS ***

    \\?\Volume{f4810a4e-cfbb-11de-86cd-000c291f01a1}\
        D:\

    \\?\Volume{f4810a5a-cfbb-11de-86cd-000c291f01a1}\
        E:\

    \\?\Volume{a681bf9d-c97b-11de-90d7-806e6f6e6963}\
        C:\

    \\?\Volume{a681bfa1-c97b-11de-90d7-806e6f6e6963}\
        A:\

    \\?\Volume{a681bfa0-c97b-11de-90d7-806e6f6e6963}\
        F:\

C:\>mountvol.exe D:\mnt \\?\Volume{f4810a5a-cfbb-11de-86cd-000c291f01a1}\

C:\>mountvol.exe

Creates, deletes, or lists a volume mount point.
....
....

Possible values for VolumeName along with current mount points are:

    \\?\Volume{a681bf9c-c97b-11de-90d7-806e6f6e6963}\
        *** NO MOUNT POINTS ***

    \\?\Volume{f4810a4e-cfbb-11de-86cd-000c291f01a1}\
        D:\

    \\?\Volume{f4810a5a-cfbb-11de-86cd-000c291f01a1}\
        E:\
        D:\mnt\

    \\?\Volume{a681bf9d-c97b-11de-90d7-806e6f6e6963}\
        C:\

    \\?\Volume{a681bfa1-c97b-11de-90d7-806e6f6e6963}\
        A:\

    \\?\Volume{a681bfa0-c97b-11de-90d7-806e6f6e6963}\
        F:\
Please note that I can still use both E:\ and D:\mnt\ to access the volume. One could also remove the E:\ mount point and rely only on D:\mnt\.
So what happens when someone tries to open D:\mnt\foo.txt ? Here are the steps, and pretty much most of this has been explained in other posts on this site, mainly around STATUS_REPARSE and IRP_MJ_CREATE processing:
  1. the IO manager issues an IRP_MJ_CREATE for the volume that is D: (\Device\HarddiskVolume3) with the FILE_OBJECT->FileName set to "\mnt\foo.txt"
  2. NTFS sees there is a reparse point on "\mnt" and returns the new path for the file, concatenating the name of the volume in the reparse point with the rest of the path, as "\Device\HarddiskVolume4\FOO.TXT"
  3. the IO manager issues a new IRP_MJ_CREATE for the volume that is E: (\Device\HarddiskVolume4) with the FILE_OBJECT->FileName set to "\foo.txt", which succeeds.
Before we start looking at what the names look like, let's quickly discuss what FltMgr does when generating names. There are two steps here, first FltMgr will generate a name for the file (the Generate step) and then, if the caller requested it, it will normalize it (the Normalize step).
The Normalize step is pretty simple so we'll discuss it first. FltMgr opens the parent directory for the name it has from the Generate step and does a directory query to get the long name associated with the file (or directory). It doesn't know if the name it has for the file is short or long and it doesn't really matter, it will figure it out from the directory query anyway. It then concatenates this with the normalized name for the parent directory. If it doesn't have the normalized name for the parent directory cached it will build it using the same algorithm. Please note that this is different in Win8, where NTFS can directly return the normalized name for a file. Also, this can explain why it can be pretty expensive to generate a normalized name, though FltMgr's name cache is pretty effective and so in most cases there is no need to perform the normalization of the parent directory name.
The Generate step differs between the time when the file isn't opened (preCreate) and the time after the file is opened (postCreate and all operations after that). If the file isn't opened FltMgr will take the name from the FILE_OBJECT->FileName and the RelatedFileObject (if there is one). If the file is opened then FltMgr will simply query NTFS for the file name using an IRP_MJ_QUERY_INFORMATION request. Please note that the OPENED name is actually the name FltMgr gets from the Generate step.
So now that all the pieces are in place, let's discuss the differences between the opened and the normalized name:
  • If the file is opened (so after a successful postCreate and for any operation where the FILE_OBJECT is opened) the name will always have the mount point resolved because during the Generate step FltMgr will simply concatenate the volume name with the path to the file that NTFS returns.
  • If this is a postCreate but the status is STATUS_REPARSE FltGetFileNameInformation will fail with STATUS_FLT_INVALID_NAME_REQUEST, so the issue of the mount point resolution doesn't apply.
  • The one case that remains is that of a preCreate, then whether mount points are resolved depends on whether this is the final IRP_MJ_CREATE or not. If it is the final IRP_MJ_CREATE then IO manager has already resolved the mount points so the opened name and the normalized name will have the same volume name. If this is not the final IRP_MJ_CREATE then the opened name will always have the volume name on which the IRP_MJ_CREATE was issued, and the normalized name will always use the name it has when opening the parent folder for the final component. This last bit is interesting because it's not clear what happens when opening the actual mount point. Will it be resolved or not ? It depends on how the query for the normalization is done, if necessary.
So let's go over some examples. First I'm going to show what happens when I open D:\mnt\folder_under_mount_point\foo.txt:
preCreate|opened -> "\Device\HarddiskVolume3\mnt\folder_under_mount_point\foo.txt" -> as expected, mount point not resolved.
preCreate|normalized -> "\Device\HarddiskVolume4\folder_under_mount_point\foo.txt" -> this is after the reparse, so mount point is resolved.
preCreate|opened -> "\Device\HarddiskVolume4\FOLDER_UNDER_MOUNT_POINT\FOO.TXT"  -> after reparse, so no mount point. Please note the case.
postCreate|normalized -> "\Device\HarddiskVolume4\folder_under_mount_point\foo.txt" -> normalize gets the actual proper case from NTFS
postCreate|opened -> "\Device\HarddiskVolume4\FOLDER_UNDER_MOUNT_POINT\FOO.TXT" -> but the opened name has the original case from the user's request.
Things to note here are the fact that there are no names from the postCreate for the first creates since they fail with STATUS_FLT_INVALID_NAME_REQUEST because those creates aren't successful. Also, interesting to note that for the first create (for which we get the opened name) trying to get the normalized name fails with STATUS_NOT_SAME_DEVICE on my system. Not sure why but I'll investigate it for next week's blog :).
Now let's look at what happens when we open something directly under the mount point (pretty much the same as above):
preCreate|opened -> "\Device\HarddiskVolume3\mnt\foo.txt" -> as expected, mount point not resolved.
preCreate|normalized -> "\Device\HarddiskVolume4\foo.txt" -> and again, after reparse, mount point resolved.
preCreate|opened -> "\Device\HarddiskVolume4\FOO.TXT" 
postCreate|normalized -> "\Device\HarddiskVolume4\foo.txt"
postCreate|opened -> "\Device\HarddiskVolume4\FOO.TXT"
And finally I want to show you what happens if I try to open the actual mount point itself, D:\mnt\.
preCreate|normalized -> "\Device\HarddiskVolume3\mnt" -> please note how the normalized name in this case is actually the mount point, so the mount point is NOT resolved.
preCreate|opened -> "\Device\HarddiskVolume3\mnt\" -> naturally, mount point not resolved since this is the opened name.
preCreate|normalized -> "\Device\HarddiskVolume4\"
preCreate|opened -> "\Device\HarddiskVolume4\"
postCreate|normalized -> "\Device\HarddiskVolume4\"
postCreate|opened -> "\Device\HarddiskVolume4\"
Hopefully this shows what's going on with mount point resolution and opened and normalized names, in more detail that anyone ever needed :).