Last reviewed and updated: 10 August 2020
One of the challenges in building file system filter drivers is differentiating between the name space as typical applications see it to the version seen by drivers, particularly file system mini-filter drivers. A technique that we have used recently to aid in this involves using volume GUIDs rather than drive letters for local volumes. In this article, we will discuss why you might not want to use drive letters and describe how volume GUIDs can be used for local drives to provide consistent naming.
What’s Wrong with Drive Letters?
The biggest problem with drive letters in a file system mini-filter is that we don’t see them and when we do we aren’t really sure what they mean.
Operations sent to local drives don’t show us the drive letters used by the application. This is because they are symbolic links to the media volume on which the file system instance is mounted. By the time the name is stored in the file object passed to IRP_MJ_CREATE the device level naming, including that drive letter information, has been consumed by the Object Manager.
In fact, local drives do not even require a drive letter, which happens in the case of mount points. Further, in the case of mount points, the name the filter will see is just the name relative to the final volume – not to the original path. Thus, the path the application uses might be “\\?\c:\mountpoint\subdir” but what the filter will see this name (via IRP_MJ_CREATE) twice: first when it sees the original path (device object + “\mountpoint\subdir”) and once again when it sees the reparsed path (second device object + “\subdir”). The first will complete with a STATUS_REPARSE return value. The second will complete based upon the existence of the object on the second volume. This significantly complicates reconstructing the name within a mini-filter driver if the expectation is that it will match the “original application name”.
The reason this is complicated is that we frequently see rules driven mechanisms that are drive letter based – “let’s intercept any file that is on the C drive with a *.docx suffix”.
The complication then is how to determine when something is on the C drive from the filter. Since the drive letters are not provided to the filter, we have no easy way to determine when this is a match. There are functions (IoQueryFileDosDevice Name) that can be used in some circumstances, but they don’t work with network volumes or when there is no drive letter. It also only returns one possible drive letter, even if there is more than one assigned.
Volume GUIDs
The alternative approach that we have used successfully in the past relates to the use of Volume GUIDs. These are unique identifiers associated with the given volume. The Mount Manager is a kernel mode driver that handles associating drive letters with volumes and it uses volume GUIDs for this task.
Drive letter assignment to physical devices is managed by the Mount Manager. If we look in the registry (HKLM\System \MountedDevices) we can see the current drive letter mappings known by the Mount Manager. An example of this can be seen below:
This information in the registry consists of the information used by the Mount Manager to map a given physical device to a corresponding drive letter and to its volume GUID name. In this way, changing the connection of the drive to the system does not change its drive letter. Note that not all drive letters present in that table are necessarily in use currently. This can happen, for example, if a disk with a partition table has previously been seen by the system and a persistent drive letter assigned to it.
To see the current list of volumes and their matching drive letters on your system, you can use the mountvol.exe utility, which is included in Windows. With no arguments, it will give you a list of all the current volumes and if they do have drive letters and/or mount points it will display that information.
You might also notice that each of those drives displays a Volume GUID based name. It turns out that you can actually use those volume GUID paths programmatically and in some of the UI components. These are persistent as well and are defined for all physical media volumes. Unfortunately, they aren’t available for network drives, so we’ll still have to do more work to handle the network case.
But let’s look at how to manage volume GUIDs.
Obtaining Volume GUIDs in Kernel Mode
Filter Manager provides a simple API for obtaining the volume GUID name FltGetVolumeGuidName. This function takes a caller provided buffer, a Filter Manager volume pointer (FLT_VOLUME) and an optional value indicating the buffer size that is required.
The following code fragment demonstrates one way to obtain this information in kernel mode:
status = STATUS_SUCCESS; // // We use a while loop for cleanup // while (STATUS_SUCCESS == status) { // // First call is to get the correct size // volumeContext->VolumeGuidString.Buffer = NULL; volumeContext->VolumeGuidString.Length = 0; volumeContext->VolumeGuidString.MaximumLength = 0; (void) FltGetVolumeGuidName(FltObjects->Volume, &volumeContext->VolumeGuidString, &bytesRequired); // // Let's allocate space // volumeContext->VolumeGuidString.Buffer = (PWCHAR) ExAllocatePoolWithTag(PagedPool, bytesRequired, HYPERBAC_MEMTAG_VOL_GUID); volumeContext->VolumeGuidString.Length = 0; ASSERT(bytesRequired <= UNICODE_STRING_MAX_BYTES); volumeContext->VolumeGuidString.MaximumLength = (USHORT) bytesRequired; if (NULL == volumeContext->VolumeGuidString.Buffer) { status = STATUS_INSUFFICIENT_RESOURCES; break; } // // Lets call it again // status = FltGetVolumeGuidName(FltObjects->Volume, &volumeContext->VolumeGuidString, &bytesRequired); if (!NT_SUCCESS(status)) { break; } // // The format is \??\Volume{GUID} // for (index = 0; (L'{' != volumeContext->VolumeGuidString.Buffer[index] && index < (volumeContext->VolumeGuidString.Length / sizeof(WCHAR))); index++) /* nothing */ ; volumeGuidName.Buffer = &volumeContext->VolumeGuidString.Buffer[index]; volumeGuidName.Length = (USHORT) (volumeContext->VolumeGuidString.Length - sizeof(WCHAR) * index); status = RtlGUIDFromString(&volumeGuidName, &volumeContext->VolumeGuid); if (!NT_SUCCESS(status)) { break; } // // Success or failure, we're done // break; }
Note the use of the function RtlGUIDFromString in this code sample – a clear demonstration of how important GUIDs actually are in Windows – supported even in kernel mode as part of the OS runtime library.
One way we have used this in our own drivers is to collect this Volume GUID when we first see the volume – while setting up the volume context – and then we store it in the respective context structure. This allows us to easily grab that information later when we need it, and volume GUIDs won’t normally change.
With this information we can now pass the volume GUID to our user mode service components.
Obtaining Volume GUIDs in User Mode
Obtaining a volume GUID in user mode is different and relies on using functions from the RPC libraries. Below is our sample function for obtaining a volume GUID:
// // GetVolumeGuid // // The purpose of this function is to extract the volume GUID // for the given file. Note that this will extract current // volume/path from the current context. // // Inputs: // OriginalFilePathName - this is the file and path to check // // _Success_(return) BOOLEAN GetVolumeGuid(_In_z_ TCHAR *OriginalFilePathName, __out GUID *Guid) { TCHAR *filePathName; ULONG filePathNameSize = UNICODE_STRING_MAX_BYTES; TCHAR guidVolumeName[64]; // these names are fixed size and much smaller than this USHORT index; RPC_STATUS rstatus; TCHAR *fileNamePart; filePathName = (TCHAR *) ExAllocatePoolWithTag(PagedPool, filePathNameSize, POOL_TAG_FILE_NAME_BUFFER); if (NULL == filePathName) { return FALSE; } filePathNameSize /= sizeof(TCHAR); GetFullPathName(OriginalFilePathName,filePathNameSize,filePathName,&fileNamePart); // // We now have a path name, let's see if we can trim it until we find a valid path // index = (USHORT) _tcslen(filePathName); if (0 == index) { // // This is a very strange case - why would we get a zero length path name? // _tprintf(TEXT("GetVolumeGuid: Original Name is %s, GetFullPathName returned a zero length. filePathName 0x%p, fileNamePart 0x%p\n"), OriginalFilePathName, filePathName, fileNamePart); return FALSE; } // // We need to point to the last character // index--; // // volume mount points require a trailing backslash // if (TEXT('\\') != filePathName[index]) { if (index == UNICODE_STRING_MAX_CHARS) { // // We can't deal with this case - but it won't really happen (32K long path name?) // ExFreePoolWithTag(filePathName, POOL_TAG_FILE_NAME_BUFFER); return FALSE; } // // Add the trailing backslash // filePathName[++index] = TEXT('\\'); filePathName[++index] = TEXT('\0'); } while (!GetVolumeNameForVolumeMountPoint(filePathName, guidVolumeName, sizeof(guidVolumeName)/sizeof(TCHAR))) { while (--index) { if (filePathName[index] == TEXT('\\')) { filePathName[index+1] = TEXT('\0'); break; } // // Otherwise we just keep seeking back in the string } if (0 == index) { // // We don't have any string left to check, this is an error condition // break; } } // // At this point we are done with the buffer // ExFreePoolWithTag(filePathName, POOL_TAG_FILE_NAME_BUFFER); filePathName = NULL; // // If the index is zero, we terminated the loop without finding the mount point // if (0 == index) { return FALSE; } // // Look for the trailing closing brace } // for (index = 0; index < sizeof(guidVolumeName)/sizeof(TCHAR); index++) { if (L'}' == guidVolumeName[index]) break; } if (index >= sizeof(guidVolumeName)/sizeof(TCHAR)) { return FALSE; } // // Set it as null // guidVolumeName[index++] = L'\0'; // // Look for the leading opening brace { // for (index = 0; index < sizeof(guidVolumeName)/sizeof(TCHAR); index++) { if (L'{' == guidVolumeName[index]) break; } if (index >= sizeof(guidVolumeName)/sizeof(TCHAR)) { return FALSE; } // // Skip over the leading { // index++; rstatus = UuidFromString((RPC_WSTR) &guidVolumeName[index], (UUID *) Guid); if (RPC_S_OK != rstatus) { return FALSE; } return TRUE; }
In this case our sample function takes a user mode path and figures out the volume GUID name of the volume where that path resides. If there is a mount point, this will resolve the mount point and return the volume GUID where the path or file resides.
This mechanism provides a clean and unambiguous way of telling a kernel mode driver which volumes are actually of interest, rather than using drive letters and/or mount points.
Volume GUIDs, Drive Letters and Mount Points
In our experience, the primary benefit to using Volume GUIDs rather than drive letters is they are unambiguous – there is only one volume with a given GUID, while a given drive can have zero or more drive letters – check out the subst and assign commands for examples of creating further aliases to existing drives and subdirectories.
Further, Volume GUIDs are persistent and drive letters can change. Thus, if the original policy is based upon drive letters, the user mode component should monitor changes to drive letters. This could be done using the WM_DEVICECHANGE notification, for example.
In addition there can be confusion about mount points. If someone specifies a pattern like “C:*” your user mode components will need to define what this means with respect to mount points. If it means “everything including mount points” then you will need to enumerate the mount points and determine if they are mounted on the C drive. This is typically done by using the Win32 function FindFirstVolumeMountPoint. Thus, you can use the volume GUID for the C: drive and then scan all the mount points on that volume.
Another alternative is to simply note that matching does not traverse across volume mount points – we note that directory change notifications don’t cross volume mount points either, so there is some precedent for this behavior.
Regardless of which behavior you choose, be consistent and document it for your users to understand.
Conclusions
In our work, we’ve found that working with volume GUIDs simplifies a file system mini-filter considerably because it eliminates string handling and drive letter understanding. By moving that logic into user mode components we can simplify our mini-filter.
In our experience, any code we can move out of kernel mode is generally beneficial as it simplifies the driver. Further, string handling code is some of the most sensitive and error prone code in a kernel mode driver, so this leads to a more robust solution.