6

How can i enumerate a list of all logical volumes on a disk? I want the name of the volume that is suitable for opening with CreateFile.

What have you tried?

I've used the FindFirstVolume/FindNextVolume API to enumerate a list of volumes. It returns a list of names such as:

  • \\?\Volume{0b777018-3313-11e2-8ccd-806e6f6e6963}\
  • \\?\Volume{0b777019-3313-11e2-8ccd-806e6f6e6963}\
  • \\?\Volume{758a2cf2-cf3a-11e4-8dce-c86000d0b92a}\
  • \\?\Volume{4f81d34b-34f4-11e2-9f6e-c86000d0b92a}\

But none of those volume names are valid volume names. That is, none of those names can be passed to CreateFile to open the volume:

0x00000003 (The system cannot find the path specified)

The question might be how do i convert the thing returned by FindFirstVolume into a volume name?

But the real question is how do i enumerate volumes in the first place?

Why not just use \\.\C:?

I wasn't asking how to hard-code a volume name; i was asking how to enumerate volume names.

Besides, not every volume has a drive letter, e.g.:

  • \\?\Volume{0b777019-3313-11e2-8ccd-806e6f6e6963}\ ==> \\.\C:
  • \\?\Volume{758a2cf2-cf3a-11e4-8dce-c86000d0b92a}\ ==> \\.\D:
  • \\?\Volume{0b777018-3313-11e2-8ccd-806e6f6e6963}\ ==> the system reserved volume with no drive letter
  • \\?\Volume{4f81d34b-34f4-11e2-9f6e-c86000d0b92a}\ ==> a CD ROM that is mounted in a folder

I swear there is an API to enumerate volumes.

GetLogicalDriveStrings

The problem with GetLogicalDriveStrings function is that it only returns logical drives:

  • C:\
  • D:\

and not volumes. In my case it misses two volumes:

  • System Reserved
  • D:\CDROM

that FindFirstVolume does correctly return.

Converting Volume to drive number

  1. Open the volume \\?\Volume{0b777019-3313-11e2-8ccd-806e6f6e6963} using CreateFile
  2. Call DeviceIoControl with the IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS control code
  3. This returns a series of DISK_EXTENT structures
   DISK_EXTENT = record
       DiskNumber: DWORD;
       StartingOffset: Int64;
       ExtentLength: Int64;
   end;

from which you can get the DiskNumbers.

function GetVolumeDiskExtents(VolumeName: string): TArray<DISK_EXTENT>;
var
    hVolume: THandle;
    vde:  PVolumeDiskExtents;
    bufferSize: Integer;
    bytesReturned: DWORD;
    le: DWORD;
begin
    SetLength(Result, 0);

    {
        CreateFile requires the trailing backslash of a volume name removed.
        While Volume API functions require the trailing backslash.
    }
//  VolumeName := ExcludeTrailingBackslash(VolumeName);

    bufferSize := SizeOf(vde)+2048*SizeOf(DISK_EXTENT);
    GetMem(vde, bufferSize);
    try
        hVolume := CreateFile(PChar(VolumeName),
                GENERIC_READ,
                FILE_SHARE_READ or FILE_SHARE_WRITE,
                nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
        if (hVolume = INVALID_HANDLE_VALUE) then
            RaiseLastOSError;
        try
            if not (DeviceIoControl(hVolume, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
                    nil, 0,
                    vde, bufferSize,
                    {var}bytesReturned, nil)) then
            begin
                le := GetLastError;
                if le = ERROR_INVALID_FUNCTION then //ie. CD-ROM
                    Exit;

                raise Exception.CreateFmt('Could not get volume disk extents for volume "%s": %s (0x%s)', [VolumeName, SysErrorMessage(le), IntToHex(le, 8)]);
            end;
        finally
            CloseHandle(hVolume);
        end;

        SetLength(Result, vde^.NumberOfDiskExtents);
        Move(vde^.Extents[0], Result[0], SizeOf(DISK_EXTENT)*vde^.NumberOfDiskExtents);
    finally
        FreeMem(vde);
    end;
end;

Bonus Reading

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • Do you know how to get the disk number it belongs to by the drive letter? Such like `C:` -> `PhysicalDrive0` or `Disk 0` (in my case Disk 0 is SSD), `D:` -> `PhysicalDrive1` or `Disk 1` (in my case Disk 1 is HDD). – huang Nov 14 '22 at 16:45
  • @JoeHuang Be away that one physical disk (e.g. `PhysicalDisk0` can contain multiple volumes (e.g `C:`, `D:`). And also one volume (e.g. `C:`) can be on multiple physical disks (e.g. `PhysicalDisk0`, `PhysicalDisk1`) because, for example, because of using RAID. – Ian Boyd Nov 14 '22 at 22:56
  • Thanks your reply, but I found the solution. Open the volume by `CreateFile` then call `DeviceIoControl` the handle with `IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS`. – huang Nov 15 '22 at 18:29
  • @JoeHuang That was what i added to my question as well. – Ian Boyd Nov 16 '22 at 14:34

1 Answers1

12

The answer to your question is hidden inside Naming a Volume. When using a volume GUID path, the rules are slightly different:

All volume and mounted folder functions that take a volume GUID path as an input parameter require the trailing backslash. [...] but this is not the case with the CreateFile function. You can open a volume by calling CreateFile and omit the trailing backslash from the volume name you specify. CreateFile processes a volume GUID path with an appended backslash as the root directory of the volume.

The solution is easy: Strip the trailing backslash from a volume GUID path to open the volume using CreateFile.

In other words, while volume management functions such as:

  • GetVolumeInformation
  • GetVolumePathNamesForVolumeName

do take the full volume name returned by FindFirstVolume/FindNextVolume, CreateFile requires the returned trailing backslash removed:

  • \\?\Volume{0b777018-3313-11e2-8ccd-806e6f6e6963}
  • \\?\Volume{0b777019-3313-11e2-8ccd-806e6f6e6963}
  • \\?\Volume{758a2cf2-cf3a-11e4-8dce-c86000d0b92a}
  • \\?\Volume{4f81d34b-34f4-11e2-9f6e-c86000d0b92a}
Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
IInspectable
  • 46,945
  • 8
  • 85
  • 181
  • 5
    `CreateFile` will actually succeed in opening the root directory if `FILE_FLAG_BACKUP_SEMANTICS` is requested. Otherwise `ERROR_PATH_NOT_FOUND` isn't exactly helpful, but the NT status code from `ntdll!RtlGetLastNtStatus` reveals the real error is `STATUS_FILE_IS_A_DIRECTORY`, i.e. the call tried to open a directory as a regular file. – Eryk Sun Mar 23 '15 at 19:13
  • 1
    Interestingly, Windows only translates NT's `STATUS_FILE_IS_A_DIRECTORY` to `ERROR_PATH_NOT_FOUND` when `lpFileName` ends with a backslash. Otherwise trying to open a directory without a trailing backslash results in `ERROR_ACCESS_DENIED`. – Eryk Sun Mar 23 '15 at 19:18
  • 1
    @eryksun: Interesting information. It all makes sense now (except for the less-than-informative error code). The documentation for [CreateFile](https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858.aspx) does point out that the `FILE_FLAG_BACKUP_SEMANTICS` flag is required to obtain a handle to a directory, but it didn't connect to the `ERROR_PATH_NO_FOUND` error code, until you spelled it out. – IInspectable Mar 23 '15 at 20:18
  • 1
    @IInspectable: while `FILE_FLAG_BACKUP_SEMANTICS` is required for directory handles, the backup privilege isn't. There seems to be some confusion about this, since I have seen several developers attempting to enable the backup privilege prior to working with `FILE_FLAG_BACKUP_SEMANTICS`. – 0xC0000022L Feb 16 '18 at 21:19