2

I was using

DeviceIoControl(dev, IOCTL_DISK_GET_DRIVE_GEOMETRY, NULL, 0, &driveInfo, sizeof(driveInfo), &dwResult, NULL)

to check if driveInfo.MediaType is RemovableMedia or FixedMedia, but it seems that all my volumes are "seen" as fixed:

\\.\C:      NTFS Fixed, this is ok - internal hard drive
\\.\D:      NTFS Fixed, this is ok - internal hard drive
\\.\E:      NTFS Fixed, this is ok - internal hard drive
\\.\F:      NTFS Fixed, this is NOT ok, this is a USB external 2.5" hard drive

Thus my question:

Is there a reliable way to know if a volume is removable or not?

There should be a way, because Windows does distinguish the removable ones (they have an icon "Safely remove hardward and eject media" near the clock).

Basj
  • 41,386
  • 99
  • 383
  • 673
  • MSDN says that you should be using https://msdn.microsoft.com/en-us/library/windows/desktop/aa365171(v=vs.85).aspx –  Jul 11 '17 at 23:02
  • @JerryCoffin OK I see now... How to distinguish internal devices volumes from external ones (USB etc.) then? – Basj Jul 11 '17 at 23:30

3 Answers3

4

The problem is that you're asking the wrong question. As they use the term, "removable" means that the media and the drive for the media are separate (like a floppy drive or CD-ROM). Anything that doesn't allow a single drive to hold different media at different times is a "fixed" drive.

Based on what you seem to want, I believe you want to use SetupDiGetDeviceRegistryProperty with the SPDRP_CAPABILITIES flag. This will tell you whether a drive can eject its media (pretty much equivalent to the "removable" you've already found), but also whether the device itself is removable (CM_DEVCAP_REMOVABLE).

Unfortunately, Microsoft's SetupDi* functions are kind of a mess to use (to put it as nicely as I know how). They have some demo code that uses the right functions and retrieves fairly similar information, but the code is also somewhat ugly, so it will probably take a little bit of study and experimentation to modify it to get what you want.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • 1
    +100 for the definition of removable in WinApi's context `"removable" means that the media and the drive for the media are separate (like a floppy drive or CD-ROM). Anything that doesn't allow a single drive to hold different media at different times is a "fixed" drive.` – Basj Jul 12 '17 at 07:13
4

Jerry Coffin is almost right. However, you have to check the SPDRP_REMOVAL_POLICY property instead. Getting there is quite some hassle. Here is a C# implementation. It returns a list of PNPDeviceIDs or the device path. This can then be used to correlate with WMI data or the result of PowerShell's Get-Disk.

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;

namespace checkIfDriveIsRemovable
{

    class Program
    {
        // some relevant sources:
        // https://www.pinvoke.net/default.aspx/setupapi.setupdigetclassdevs
        // https://www.pinvoke.net/default.aspx/setupapi.setupdigetdeviceregistryproperty
        // https://stackoverflow.com/questions/15000196/reading-device-managers-property-fields-in-windows-7-8
        // https://stackoverflow.com/questions/14621211/determine-if-drive-is-removable-flash-or-hdd-knowing-only-the-drive-letter
        private static string GUID_DEVINTERFACE_DISK = "53F56307-B6BF-11D0-94F2-00A0C91EFB8B";

        private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr( -1 );

        const int BUFFER_SIZE = 1024;

        enum RemovalPolicy : uint
        {
            CM_REMOVAL_POLICY_EXPECT_NO_REMOVAL            = 1,
            CM_REMOVAL_POLICY_EXPECT_ORDERLY_REMOVAL       = 2,
            CM_REMOVAL_POLICY_EXPECT_SURPRISE_REMOVAL      = 3
        }

        enum SetupDiGetDeviceRegistryPropertyEnum : uint
        {
            SPDRP_DEVICEDESC = 0x00000000, // DeviceDesc (R/W)
            SPDRP_HARDWAREID = 0x00000001, // HardwareID (R/W)
            SPDRP_COMPATIBLEIDS = 0x00000002, // CompatibleIDs (R/W)
            SPDRP_UNUSED0 = 0x00000003, // unused
            SPDRP_SERVICE = 0x00000004, // Service (R/W)
            SPDRP_UNUSED1 = 0x00000005, // unused
            SPDRP_UNUSED2 = 0x00000006, // unused
            SPDRP_CLASS = 0x00000007, // Class (R--tied to ClassGUID)
            SPDRP_CLASSGUID = 0x00000008, // ClassGUID (R/W)
            SPDRP_DRIVER = 0x00000009, // Driver (R/W)
            SPDRP_CONFIGFLAGS = 0x0000000A, // ConfigFlags (R/W)
            SPDRP_MFG = 0x0000000B, // Mfg (R/W)
            SPDRP_FRIENDLYNAME = 0x0000000C, // FriendlyName (R/W)
            SPDRP_LOCATION_INFORMATION = 0x0000000D, // LocationInformation (R/W)
            SPDRP_PHYSICAL_DEVICE_OBJECT_NAME = 0x0000000E, // PhysicalDeviceObjectName (R)
            SPDRP_CAPABILITIES = 0x0000000F, // Capabilities (R)
            SPDRP_UI_NUMBER = 0x00000010, // UiNumber (R)
            SPDRP_UPPERFILTERS = 0x00000011, // UpperFilters (R/W)
            SPDRP_LOWERFILTERS = 0x00000012, // LowerFilters (R/W)
            SPDRP_BUSTYPEGUID = 0x00000013, // BusTypeGUID (R)
            SPDRP_LEGACYBUSTYPE = 0x00000014, // LegacyBusType (R)
            SPDRP_BUSNUMBER = 0x00000015, // BusNumber (R)
            SPDRP_ENUMERATOR_NAME = 0x00000016, // Enumerator Name (R)
            SPDRP_SECURITY = 0x00000017, // Security (R/W, binary form)
            SPDRP_SECURITY_SDS = 0x00000018, // Security (W, SDS form)
            SPDRP_DEVTYPE = 0x00000019, // Device Type (R/W)
            SPDRP_EXCLUSIVE = 0x0000001A, // Device is exclusive-access (R/W)
            SPDRP_CHARACTERISTICS = 0x0000001B, // Device Characteristics (R/W)
            SPDRP_ADDRESS = 0x0000001C, // Device Address (R)
            SPDRP_UI_NUMBER_DESC_FORMAT = 0X0000001D, // UiNumberDescFormat (R/W)
            SPDRP_DEVICE_POWER_DATA = 0x0000001E, // Device Power Data (R)
            SPDRP_REMOVAL_POLICY = 0x0000001F, // Removal Policy (R)
            SPDRP_REMOVAL_POLICY_HW_DEFAULT = 0x00000020, // Hardware Removal Policy (R)
            SPDRP_REMOVAL_POLICY_OVERRIDE = 0x00000021, // Removal Policy Override (RW)
            SPDRP_INSTALL_STATE = 0x00000022, // Device Install State (R)
            SPDRP_LOCATION_PATHS = 0x00000023, // Device Location Paths (R)
            SPDRP_BASE_CONTAINERID = 0x00000024  // Base ContainerID (R)
        }

        [Flags]
        public enum DiGetClassFlags : uint
        {
            DIGCF_DEFAULT = 0x00000001,  // only valid with DIGCF_DEVICEINTERFACE
            DIGCF_PRESENT = 0x00000002,
            DIGCF_ALLCLASSES = 0x00000004,
            DIGCF_PROFILE = 0x00000008,
            DIGCF_DEVICEINTERFACE = 0x00000010,
        }

        public enum RegType : uint
        {
            REG_BINARY = 3,
            REG_DWORD = 4,
            REG_EXPAND_SZ = 2,
            REG_MULTI_SZ = 7,
            REG_SZ = 1
        }

        [StructLayout( LayoutKind.Sequential )]
        struct SP_DEVICE_INTERFACE_DATA
        {
            public Int32 cbSize;
            public Guid interfaceClassGuid;
            public Int32 flags;
            private UIntPtr reserved;
        }

        [StructLayout( LayoutKind.Sequential )]
        struct SP_DEVINFO_DATA
        {
            public UInt32 cbSize;
            public Guid ClassGuid;
            public UInt32 DevInst;
            public IntPtr Reserved;
        }

        [StructLayout( LayoutKind.Sequential, CharSet = CharSet.Auto )]
        struct SP_DEVICE_INTERFACE_DETAIL_DATA
        {
            public int cbSize;
            [MarshalAs( UnmanagedType.ByValArray, SizeConst = BUFFER_SIZE )]
            public byte[] DevicePath;
        }

        [DllImport( "setupapi.dll", CharSet = CharSet.Auto )]
        static extern IntPtr SetupDiGetClassDevs(
                                                  ref Guid ClassGuid,
                                                  IntPtr Enumerator,
                                                  IntPtr hwndParent,
                                                  uint Flags
                                                 );

        [DllImport( @"setupapi.dll", CharSet = CharSet.Auto, SetLastError = true )]
        static extern Boolean SetupDiEnumDeviceInterfaces( IntPtr hDevInfo, IntPtr devInfo, ref Guid interfaceClassGuid, UInt32 memberIndex, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData );

        [DllImport( @"setupapi.dll", CharSet = CharSet.Auto, SetLastError = true )]
        static extern Boolean SetupDiGetDeviceInterfaceDetail( IntPtr hDevInfo, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, ref SP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData, UInt32 deviceInterfaceDetailDataSize, ref UInt32 requiredSize, ref SP_DEVINFO_DATA deviceInfoData );

        [DllImport( "setupapi.dll", CharSet = CharSet.Auto, SetLastError = true )]
        static extern bool SetupDiGetDeviceRegistryProperty( IntPtr deviceInfoSet, ref SP_DEVINFO_DATA deviceInfoData, uint property, out UInt32 propertyRegDataType, byte[] propertyBuffer, uint propertyBufferSize, out UInt32 requiredSize );

        [DllImport( "setupapi.dll" )]
        static extern bool SetupDiGetDeviceInstanceIdA(IntPtr deviceInfoSet, ref SP_DEVINFO_DATA DeviceInfoData, byte[] DeviceInstanceId, Int32 DeviceInstanceIdSize, out UInt32 RequiredSize);

        [DllImport( "setupapi.dll" )]
        static extern Int32 SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);

        static string getStringProp(IntPtr h, SP_DEVINFO_DATA da, SetupDiGetDeviceRegistryPropertyEnum prop)
        {
            UInt32 requiredSize;
            UInt32 regType;
            byte[] ptrBuf = new byte[BUFFER_SIZE];
            if( !SetupDiGetDeviceRegistryProperty( h, ref da, ( uint ) prop, out regType, ptrBuf, BUFFER_SIZE, out requiredSize ) )
                throw new InvalidOperationException( "Error getting string property" );

            if( regType != (uint) RegType.REG_SZ || ( requiredSize & 1 ) != 0 )
                throw new InvalidOperationException( "Property is not a REG_SZ" );

            if( requiredSize == 0 )
                return "";

            return Encoding.Unicode.GetString( ptrBuf, 0, (int) requiredSize - 2 );
        }

        static uint getDWORDProp(IntPtr h, SP_DEVINFO_DATA da, SetupDiGetDeviceRegistryPropertyEnum prop)
        {
            UInt32 requiredSize;
            UInt32 regType;
            byte[] ptrBuf = new byte[4];
            if( !SetupDiGetDeviceRegistryProperty( h, ref da, ( uint ) prop, out regType, ptrBuf, 4, out requiredSize ) )
                throw new InvalidOperationException( "Error getting DWORD property" );

            if( regType != ( uint ) RegType.REG_DWORD || requiredSize != 4 )
                throw new InvalidOperationException( "Property is not a REG_DWORD" );

            return BitConverter.ToUInt32( ptrBuf, 0 );
        }

        public static string[] getRemovableDisks( bool getPNPDeviceID = false )
        {
            List<String> result = new List<string>();
            Guid DiskGUID = new Guid( GUID_DEVINTERFACE_DISK );
            IntPtr h = SetupDiGetClassDevs( ref DiskGUID, IntPtr.Zero, IntPtr.Zero, ( uint ) ( DiGetClassFlags.DIGCF_PRESENT | DiGetClassFlags.DIGCF_DEVICEINTERFACE ) );
            if( h == INVALID_HANDLE_VALUE )
                return null;

            IntPtr x = new IntPtr(); ;
            int y = Marshal.SizeOf( x );


            try {
                for( uint i = 0; ; i++ ) {
                    SP_DEVICE_INTERFACE_DATA dia = new SP_DEVICE_INTERFACE_DATA();
                    dia.cbSize = Marshal.SizeOf( dia );
                    if( !SetupDiEnumDeviceInterfaces( h, IntPtr.Zero, ref DiskGUID, i, ref dia ) )
                        break;

                    SP_DEVINFO_DATA da = new SP_DEVINFO_DATA();
                    da.cbSize = ( uint ) Marshal.SizeOf( da );

                    // build a Device Interface Detail Data structure
                    SP_DEVICE_INTERFACE_DETAIL_DATA didd = new SP_DEVICE_INTERFACE_DETAIL_DATA();
                    // I honestly don't know, why this works. The if-part can be found on the net a few times, the else part is from me
                    if( IntPtr.Size == 4 )
                        didd.cbSize = 4 + Marshal.SystemDefaultCharSize; // trust me :)
                    else
                        didd.cbSize = 8;

                    // now we can get some more detailed information
                    uint nRequiredSize = 0;
                    uint nBytes = BUFFER_SIZE;
                    if( SetupDiGetDeviceInterfaceDetail( h, ref dia, ref didd, nBytes, ref nRequiredSize, ref da ) ) {

                        string devicePath = Encoding.Unicode.GetString( didd.DevicePath, 0, ( int ) nRequiredSize - 6 );  // remove 6 bytes: 2 bytes zero termination and another 4 bytes, because nRequiredSize also counts SP_DEVICE_INTERFACE_DETAIL_DATA.cbSize (as it seems...)

                        UInt32 RequiredSize;
                        byte[] ptrBuf = new byte[BUFFER_SIZE];
                        string PNPDeviceID = "";
                        if( SetupDiGetDeviceInstanceIdA( h, ref da, ptrBuf, BUFFER_SIZE, out RequiredSize ) ) {
                            if( RequiredSize >= 1 )
                                PNPDeviceID = Encoding.ASCII.GetString( ptrBuf, 0, ( int ) RequiredSize - 1 );
                        }

                        // you can get the properties, which are shown in "device manager -> properties of the drive -> details"
                        /*
                        string desc = getStringProp( h, da, SetupDiGetDeviceRegistryPropertyEnum.SPDRP_PHYSICAL_DEVICE_OBJECT_NAME ); // SPDRP_DEVICEDESC );
                        string driver = getStringProp( h, da, SetupDiGetDeviceRegistryPropertyEnum.SPDRP_DRIVER );
                        string friendlyname = getStringProp( h, da, SetupDiGetDeviceRegistryPropertyEnum.SPDRP_FRIENDLYNAME );
                        // no, the removable flag in the capabalities is of no use! Use removalPolicy!
                        uint capabilities = getDWORDProp( h, da, SetupDiGetDeviceRegistryPropertyEnum.SPDRP_CAPABILITIES );
                        uint removalPolicy = getDWORDProp( h, da, SetupDiGetDeviceRegistryPropertyEnum.SPDRP_REMOVAL_POLICY );
                        uint removalPolicyHW = getDWORDProp( h, da, SetupDiGetDeviceRegistryPropertyEnum.SPDRP_REMOVAL_POLICY_HW_DEFAULT );
                        //uint removalPolicyOVR = getDWORDProp( h, da, SetupDiGetDeviceRegistryPropertyEnum.SPDRP_REMOVAL_POLICY_OVERRIDE );

                        Console.WriteLine( "{0,-40} {1,-60} {2,-20} {3,-20} {4,5} {5} {6}", friendlyname, PNPDeviceID, desc, driver, capabilities, removalPolicy, removalPolicyHW );
                        */
                        try {
                            uint removalPolicy = getDWORDProp( h, da, SetupDiGetDeviceRegistryPropertyEnum.SPDRP_REMOVAL_POLICY );
                            if( removalPolicy == ( uint ) RemovalPolicy.CM_REMOVAL_POLICY_EXPECT_ORDERLY_REMOVAL || removalPolicy == ( uint ) RemovalPolicy.CM_REMOVAL_POLICY_EXPECT_SURPRISE_REMOVAL )
                                result.Add( getPNPDeviceID ? PNPDeviceID : devicePath );
                        } catch( InvalidOperationException ) {
                            continue;
                        }
                    }
                }
            } finally {
                SetupDiDestroyDeviceInfoList( h );
            }

            return result.ToArray();
        }

        static void Main(string[] args)
        {
            string[] removableDisks = getRemovableDisks();
        }
    }
}
2

most simply and reliable way use IOCTL_STORAGE_QUERY_PROPERTY with StorageDeviceProperty. on return we got STORAGE_DEVICE_DESCRIPTOR - and look for

RemovableMedia

Indicates when TRUE that the device's media (if any) is removable. If the device has no media, this member should be ignored. When FALSE the device's media is not removable.

so we need disk handle with any access (because IOCTL_STORAGE_QUERY_PROPERTY defined as CTL_CODE(IOCTL_STORAGE_BASE, 0x0500, METHOD_BUFFERED, FILE_ANY_ACCESS) (in every IOCtl encoded access(read, write, both or any), in this case FILE_ANY_ACCESS. with this handle query can look like next

ULONG IsRemovable(HANDLE hDisk, BOOLEAN& RemovableMedia)
{
    STORAGE_PROPERTY_QUERY spq = { StorageDeviceProperty, PropertyStandardQuery }; 

    STORAGE_DEVICE_DESCRIPTOR sdd;

    ULONG rcb;
    if (DeviceIoControl(hDisk, IOCTL_STORAGE_QUERY_PROPERTY, &spq, sizeof(spq), &sdd, sizeof(sdd), &rcb, 0))
    {
        RemovableMedia = sdd.RemovableMedia;
        return NOERROR;
    }

    return GetLastError();
}

for enumerate all disk drives we can use for example next code:

void EnumDisks()
{
    ULONG len;

    if (!CM_Get_Device_Interface_List_SizeW(&len, const_cast<GUID*>(&GUID_DEVINTERFACE_DISK), 0, CM_GET_DEVICE_INTERFACE_LIST_PRESENT))
    {
        PWSTR buf = (PWSTR)alloca(len << 1);
        if (!CM_Get_Device_Interface_ListW(const_cast<GUID*>(&GUID_DEVINTERFACE_DISK), 0, buf, len, CM_GET_DEVICE_INTERFACE_LIST_PRESENT))
        {
            while (*buf)
            {
                HANDLE hDisk = CreateFile(buf, 0, FILE_SHARE_VALID_FLAGS, 0, OPEN_EXISTING, 0, 0);

                if (hDisk != INVALID_HANDLE_VALUE)
                {
                    BOOLEAN RemovableMedia;
                    if (!IsRemovable(hDisk, RemovableMedia))
                    {
                        DbgPrint("%u %S\n", RemovableMedia, buf);
                    }
                    CloseHandle(hDisk);
                }
                buf += wcslen(buf) + 1;
            }
        }
    }
}

but for test you can and open disk as L"\\\\?\\X:" for example

RbMm
  • 31,280
  • 3
  • 35
  • 56
  • Thanks for this helpful code! Even with `IOCTL_STORAGE_QUERY_PROPERTY` / `StorageDeviceProperty`, in the case of my external USB hard drive, it is still display as fixed / non removable though, sadly. See [this answer](https://stackoverflow.com/a/45046123/1422096) for the possible reasons. – Basj Jul 12 '17 at 07:09
  • @Basj - but hard drive and was not removable. not support safely remove - are you can do this with it in windows ? – RbMm Jul 12 '17 at 07:15
  • Sorry, what do you mean @RbMm? – Basj Jul 12 '17 at 07:17
  • @Basj - in bottom-righ exist icon - `safely remove hardware and eject media` - here list of removable devices - are your external USB hard drive in this list and can be removed ? – RbMm Jul 12 '17 at 07:20
  • yes the external USB hard drive is in this list "Safely remove hardware and eject media". But still with IOCTL_STORAGE_QUERY_PROPERTY / StorageDeviceProperty, it appears as FixedMedia. – Basj Jul 12 '17 at 07:24