Thursday, March 15, 2012

Volume Names

In this post I want to talk about something that's not directly related to file system filters but that I've spent a lot of time fighting with. I'm specifically talking about volume names and the reason this is important to me is because these days I work on virtualization filters and in some cases when creating virtual files I need to make them feel the same as regular files on a real volume and the way some applications (both kernel mode and user mode) handle volume names is downright broken.
The most important point I want to make is that a volume name is NOT a drive letter. I read a lot of articles and attend a lot of presentations where volumes are identified by "drive letter" which, while is useful as a way to express a concept because everyone is familiar with drive letters, is actually wrong. Drive letters are a DOS concept, the NT concept is the volume name (and it looks like this '\\?\Volume{0d5759d1-429c-11df-8e0f-806e6f6e6963}'). Easiest way to see this is to use the "mountvol.exe" command line tool. This difference is very clearly expressed in the mountmgr.h file (%DDKPATH%\inc\ddk\mountmgr.h) where there are macros like 'MOUNTMGR_IS_VOLUME_NAME(s)' and 'MOUNTMGR_IS_DRIVE_LETTER':


//
// Macro that defines what a "drive letter" mount point is.  This macro can
// be used to scan the result from QUERY_POINTS to discover which mount points
// are find "drive letter" mount points.
//

#define MOUNTMGR_IS_DRIVE_LETTER(s) (   \
    (s)->Length == 28 &&                \
    (s)->Buffer[0] == '\\' &&           \
    (s)->Buffer[1] == 'D' &&            \
    (s)->Buffer[2] == 'o' &&            \
    (s)->Buffer[3] == 's' &&            \
    (s)->Buffer[4] == 'D' &&            \
    (s)->Buffer[5] == 'e' &&            \
    (s)->Buffer[6] == 'v' &&            \
    (s)->Buffer[7] == 'i' &&            \
    (s)->Buffer[8] == 'c' &&            \
    (s)->Buffer[9] == 'e' &&            \
    (s)->Buffer[10] == 's' &&           \
    (s)->Buffer[11] == '\\' &&          \
    (s)->Buffer[12] >= 'A' &&           \
    (s)->Buffer[12] <= 'Z' &&           \
    (s)->Buffer[13] == ':')

//
// Macro that defines what a "volume name" mount point is.  This macro can
// be used to scan the result from QUERY_POINTS to discover which mount points
// are "volume name" mount points.
//

#define MOUNTMGR_IS_VOLUME_NAME(s) (                                          \
     ((s)->Length == 96 || ((s)->Length == 98 && (s)->Buffer[48] == '\\')) && \
     (s)->Buffer[0] == '\\' &&                                                \
     ((s)->Buffer[1] == '?' || (s)->Buffer[1] == '\\') &&                     \
     (s)->Buffer[2] == '?' &&                                                 \
     (s)->Buffer[3] == '\\' &&                                                \
     (s)->Buffer[4] == 'V' &&                                                 \
     (s)->Buffer[5] == 'o' &&                                                 \
     (s)->Buffer[6] == 'l' &&                                                 \
     (s)->Buffer[7] == 'u' &&                                                 \
     (s)->Buffer[8] == 'm' &&                                                 \
     (s)->Buffer[9] == 'e' &&                                                 \
     (s)->Buffer[10] == '{' &&                                                \
     (s)->Buffer[19] == '-' &&                                                \
     (s)->Buffer[24] == '-' &&                                                \
     (s)->Buffer[29] == '-' &&                                                \
     (s)->Buffer[34] == '-' &&                                                \
     (s)->Buffer[47] == '}'                                                   \
    )
So unless you're writing applications that are specific to DOS, please stop thinking in terms of "drive letters" and instead think of "volume names", especially when writing articles and presentations. There are many volume user mode APIs that are very well documented (see the page Volume Management Functions in MSDN) and that should be used. Also, as a developer, never write a function that takes a parameter a volume as a "char" and instead always use mount points or volume names (which is a string). There is also a page on Naming a Volume which discusses some of the use cases and the available APIs.
As I mentioned in my previous post on Problems with STATUS_REPARSE - Part II, a lot of the times the problems come from user mode apps trying to build a path to a file and they expect to get a drive letter as the volume, which is just wrong. Even the MSDN example Obtaining a File Name From a File Handle falls into this trap by using drive letters all over:


…
        if (GetLogicalDriveStrings(BUFSIZE-1, szTemp)) 
        {
          TCHAR szName[MAX_PATH];
          TCHAR szDrive[3] = TEXT(" :");   <- this is wrong…
          BOOL bFound = FALSE;
          TCHAR* p = szTemp;

          do 
          {
            // Copy the drive letter to the template string
            *szDrive = *p;

            // Look up each device name
            if (QueryDosDevice(szDrive, szName, MAX_PATH))  <- this is wrong...
            {
              size_t uNameLen = _tcslen(szName);

              if (uNameLen < MAX_PATH) 
              {
                bFound = _tcsnicmp(pszFilename, szName, uNameLen) == 0
                         && *(pszFilename + uNameLen) == _T('\\');

                if (bFound) 
                {
                  // Reconstruct pszFilename using szTempFile
                  // Replace device path with DOS path
                  TCHAR szTempFile[MAX_PATH];
                  StringCchPrintf(szTempFile,
                            MAX_PATH,
                            TEXT("%s%s"),
                            szDrive,
                            pszFilename+uNameLen);
                  StringCchCopyN(pszFilename, MAX_PATH+1, szTempFile, _tcslen(szTempFile));
...
I've always wondered, as a windows developer, does it not bother people that they're calling functions like "QueryDosDevice" ? What does DOS have to do with anything ? Step into the 21st century already!
Anyway, the best way to do this is to call GetFinalPathNameByHandle() and use the VOLUME_NAME_GUID flag to get used to using volume names. Unfortunately this is only available in Vista and newer OSes and so for XP one could still use the technique described in Obtaining a File Name From a File Handle but there is something that needs to be changed. The problem is that the volume APIs don't seem to have a way to convert a volume device name ('\Device\HarddiskVolume2') to a volume GUID name. In fact, none of the volume APIs offer an easy way to work with volume device names. The one way I've been able to do this in the general case was to use the MountMgr APIs directly. I don't have any user mode code that shows exactly what need to be done but I'll show the kernel mode code piece that queries the MountMgr:

#define MY_MOUNTMGR_MOUNT_POINT_TAG = 'mMyM'

typedef enum _MY_MOUNTMGR_BUFFER_TYPE {

    //
    // we'll query the MOUNTMGR using one of the three keys it supports..
    //

    MY_MOUNTMGR_SYMLINK = ' myS',
    MY_MOUNTMGR_UNIQUE_ID = 'DIUU',
    MY_MOUNTMGR_DEVICE = ' veD',
        
} MY_MOUNTMGR_BUFFER_TYPE, *PMY_MOUNTMGR_BUFFER_TYPE;


NTSTATUS
MyQueryMountMgr(
    __in PVOID Buffer,
    __in USHORT BufferLength,
    __in MY_MOUNTMGR_BUFFER_TYPE BufferType,  
    __out PMOUNTMGR_MOUNT_POINTS * MountPoints  
    )
/*++

Routine Description:

    Call MountMgr to get a names of a volume when knowing one of the
    other names.

Arguments:

    Buffer - the buffer that we want to send MountMgr to allow it to identify 
             the volume we're talking about. 

    BufferLength - the length of that buffer

    BufferType - the type of information that the buffer describes.

    MountPoints - this is a buffer that is allocated inside this function that 
                  the caller must free which is the list of mount points that
                  MountMgr returned... if it's NULL then no buffer is returned..
                  This is NOT the standard convention (the caller should supply 
                  the buffer) but it saves time.

Return Value:

    an appropriate NTSTATUS value

--*/
{
    NTSTATUS status = STATUS_SUCCESS;

    PMOUNTMGR_MOUNT_POINT mountMgrKey = NULL;
    ULONG mountMgrKeyLength = 0;

    PIRP irp = NULL;

    UNICODE_STRING mountMgrName;
    PFILE_OBJECT mountMgrFileObject = NULL;
    PDEVICE_OBJECT mountMgrDeviceObject = NULL;

    IO_STATUS_BLOCK ioStatus;
    KEVENT ioEvent;

    PMOUNTMGR_MOUNT_POINTS mountMgrMountPoints = NULL;
    ULONG mountMgrMountPointsLength = 0;
    
    PAGED_CODE();

    __try{

        KeInitializeEvent( &ioEvent, NotificationEvent, FALSE);

        //
        // first try to set up the buffer for the name..
        //
        
        mountMgrKeyLength = sizeof(MOUNTMGR_MOUNT_POINT);
        mountMgrKeyLength += BufferLength;


        mountMgrKey = ExAllocatePoolWithTag( PagedPool,
                                             mountMgrKeyLength,
                                             MY_MOUNTMGR_MOUNT_POINT_TAG );

        if (mountMgrKey == NULL) {

            status = STATUS_INSUFFICIENT_RESOURCES;
            __leave;
        }

        //
        // populate the structure..
        //

        RtlZeroMemory( mountMgrKey, mountMgrKeyLength);

        switch(BufferType) {

            case MY_MOUNTMGR_DEVICE:

                mountMgrKey->DeviceNameLength = BufferLength;
                mountMgrKey->DeviceNameOffset = sizeof(MOUNTMGR_MOUNT_POINT);
                break;

            case MY_MOUNTMGR_UNIQUE_ID:

                mountMgrKey->UniqueIdLength= BufferLength;
                mountMgrKey->UniqueIdOffset = sizeof(MOUNTMGR_MOUNT_POINT);
                break;

            case MY_MOUNTMGR_SYMLINK:

                mountMgrKey->SymbolicLinkNameLength= BufferLength;
                mountMgrKey->SymbolicLinkNameOffset= sizeof(MOUNTMGR_MOUNT_POINT);
                break;

            default:

                status = STATUS_INVALID_PARAMETER;
                __leave;
                break;
        }

        RtlCopyMemory( Add2Ptr(mountMgrKey, sizeof(MOUNTMGR_MOUNT_POINT)),
                       Buffer,
                       BufferLength );

        //
        // now we need a reference to MountMgr
        //

        RtlInitUnicodeString(&mountMgrName, MOUNTMGR_DEVICE_NAME);
        
        status = IoGetDeviceObjectPointer( &mountMgrName,
                                           FILE_READ_ATTRIBUTES, 
                                           &mountMgrFileObject, 
                                           &mountMgrDeviceObject);
        
        if (!NT_SUCCESS(status)) {
        
            __leave;
        }

        mountMgrMountPointsLength = sizeof(MOUNTMGR_MOUNT_POINTS);

        status = STATUS_BUFFER_OVERFLOW;

        while(status == STATUS_BUFFER_OVERFLOW) {

            NT_ASSERT(mountMgrMountPoints == NULL);

            mountMgrMountPoints = ExAllocatePoolWithTag( PagedPool,
                                                         mountMgrMountPointsLength,
                                                         MY_MOUNTMGR_MOUNT_POINT_TAG );

            if (mountMgrMountPoints == NULL) {

                status = STATUS_INSUFFICIENT_RESOURCES;
                __leave;
            }

            irp = IoBuildDeviceIoControlRequest( IOCTL_MOUNTMGR_QUERY_POINTS,
                                                 mountMgrDeviceObject, 
                                                 mountMgrKey, 
                                                 mountMgrKeyLength, 
                                                 mountMgrMountPoints, 
                                                 mountMgrMountPointsLength, 
                                                 FALSE, 
                                                 &ioEvent, 
                                                 &ioStatus);

            if (irp == NULL) {

                status = STATUS_INSUFFICIENT_RESOURCES;
                __leave;
            }
        
            status = IoCallDriver( mountMgrDeviceObject, irp );
            
            if (status == STATUS_PENDING) {
            
                status = KeWaitForSingleObject( &ioEvent,
                                                Executive,
                                                KernelMode,
                                                FALSE,
                                                NULL );
            
                status = ioStatus.Status;
            }

            switch (status) {

                case STATUS_BUFFER_OVERFLOW:

                    //
                    // we need a bigger buffer, the Size should tell us how big.
                    // assert that it's more that we previously had...
                    //

                    NT_ASSERT(mountMgrMountPointsLength < mountMgrMountPoints->Size);

                    mountMgrMountPointsLength = mountMgrMountPoints->Size;

                    ExFreePoolWithTag( mountMgrMountPoints, MY_MOUNTMGR_MOUNT_POINT_TAG );
                    mountMgrMountPoints = NULL;
                    
                    break;

                case STATUS_OBJECT_NAME_NOT_FOUND:

                    //
                    // it is possible that the IOCTL doesn't find anything, this
                    // is not a problem...
                    //
                    
                    break;

                case STATUS_SUCCESS:

                    //
                    // we got the links back, all is good... for the delete case
                    // it's possible we'll get called multiple times..
                    //

                    NT_ASSERT((mountMgrMountPoints->NumberOfMountPoints != 0) ||
                               (MountMgrIoctl == IOCTL_MOUNTMGR_DELETE_POINTS));

                    break;

                default:

                    NT_ASSERT(!"why are we here ? investigate...");
                    break;
                        
            }

        }

    }
    __finally{

        if (mountMgrKey != NULL) {

            ExFreePoolWithTag( mountMgrKey, MY_MOUNTMGR_MOUNT_POINT_TAG );
        }

        if (mountMgrFileObject != NULL) {

            ObDereferenceObject( mountMgrFileObject );
        }

    }
    
    //
    // if we have some mount points and we were successful and the caller
    // gave us a pointer, then set it in that pointer. Otherwise, free it..
    //

    if (NT_SUCCESS(status) &&
        MountPoints != NULL) {

        *MountPoints = mountMgrMountPoints;
        
    }
     
    return status;
}
Something very similar can be done in user mode (though it would be a lot simpler), where instead of IoGetDeviceObjectPointer() one would have to open MOUNTMGR_DOS_DEVICE_NAME and get a handle to the MountMgr device and also the call to IoBuildDeviceIoControlRequest would be replaced with DeviceIoControl.

5 comments:

  1. Hi, Alex. I'm learning very much from your posts. Do you have a plan to introduce 'how to support UNC' though?
    I read the MSDN document and studying IOCTL_REDIR_QUERY_PATH control code. But I can hardly understand what LengthAccepted mean. What should I fill to the variable?

    ReplyDelete
    Replies
    1. Hi Jeho!

      I think this link explains this quite well: http://www.osronline.com/showThread.CFM?link=222210

      Thanks,
      Alex.

      Delete
  2. Thanks a lot for your posts. It helps me a lot in my work! )

    ReplyDelete
  3. What about GetVolumeNameForVolumeMountPoint() API?

    "Retrieves a volume GUID path for the volume that is associated with the specified volume mount point ( drive letter, volume GUID path, or mounted folder)."

    ReplyDelete
    Replies
    1. I'm not sure what the question is. GetVolumeNameForVolumeMountPoint() is a useful API for sure. However you can't pass in an NT device name (\Device\HarddiskVolume1) as the input parameter (at least according to the documentation). The documentation states that the input must be "A pointer to a string that contains the path of a mounted folder (for example, "Y:\MountX\") or a drive letter (for example, "X:\"). The string must end with a trailing backslash ('\').".

      The code I showed shows how to implement something similar to GetVolumeNameForVolumeMountPoint() and also be able to provide an NT device name as an input parameter.

      Thanks,
      Alex.

      Delete