0

I'm trying to call DeviceIoControl(IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS) API, as shown here, but I need it to first "tell me" how much memory it needs (unlike the code I linked to.)

So I call it as such:

//First determine how much data do we need?
BYTE dummyBuff[1];
DWORD bytesReturned = 0;
if(!::DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, 
    dummyBuff, sizeof(dummyBuff), &bytesReturned, NULL))
{
    //Check last error
    int nError = ::GetLastError();
    if(nOSError == ERROR_INSUFFICIENT_BUFFER ||
        nOSError == ERROR_MORE_DATA)
    {
        //Alloc memory from 'bytesReturned' ...
    }
}

but it always returns error code 87, or ERROR_INVALID_PARAMETER and my bytesReturned is always 0.

So what am I doing wrong?

Community
  • 1
  • 1
c00000fd
  • 20,994
  • 29
  • 177
  • 400
  • Do you care to explain downvotes? – c00000fd Oct 25 '15 at 07:57
  • The buffer size is documented as `sizeof(VOLUME_DISK_EXTENTS)`, no need to query. Have you [read the docs](https://msdn.microsoft.com/en-au/library/windows/desktop/aa365194%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396)? – Jonathan Potter Oct 25 '15 at 08:10
  • We can't see all of the code needed to know what you are doing. A [mcve] is very easy to make. Might I ask why you did not spend that extra time to do so. – David Heffernan Oct 25 '15 at 08:19
  • @JonathanPotter: Have you? `Extents` member of `VOLUME_DISK_EXTENTS` is defined with the size of `ANYSIZE_ARRAY`, which defaults to 1, which works only if `NumberOfDiskExtents` == 1, that will obviously fail if there's more than 1 `extent`. I don't think I need to explain this, do I? This is uncommon and that is why most code can get away with what you thought. – c00000fd Oct 25 '15 at 08:35
  • @DavidHeffernan: To get `hDevice` for my code above, do this: `CreateFile(L"\\\\.\\C:", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);` and set `dwIoControlCode` to `IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS`, and `lpInBuffer` and `nInBufferSize` to 0. – c00000fd Oct 25 '15 at 08:37
  • Please make a [mcve]. Don't ask the question in comments. – David Heffernan Oct 25 '15 at 08:45
  • You need to pass a [VOLUME_DISK_EXTENTS structure](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365727.aspx) with a `sizeof(VOLUME_DISK_EXTENTS)` output buffer size. On return, the call either succeeds, or *"[...] the error code ERROR_MORE_DATA is returned. You should call DeviceIoControl again, allocating enough buffer space based on the value of NumberOfDiskExtents after the first DeviceIoControl call."* This is [documented](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365727.aspx). – IInspectable Oct 25 '15 at 10:28

3 Answers3

4

The instructions for getting all disk volume extents are documented under the VOLUME_DISK_EXTENTS structure:

When the number of extents returned is greater than one (1), the error code ERROR_MORE_DATA is returned. You should call DeviceIoControl again, allocating enough buffer space based on the value of NumberOfDiskExtents after the first DeviceIoControl call.

The behavior, if you pass an output buffer, that is smaller than sizeof(VOLUME_DISK_EXTENTS) is also documented at IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS control code:

If the output buffer is less than sizeof(VOLUME_DISK_EXTENTS), the call fails, GetLastError returns ERROR_INSUFFICIENT_BUFFER, and lpBytesReturned is 0 (zero).

While this explains the returned value in lpBytesReturned, it doesn't explain the error code 87 (ERROR_INVALID_PARAMETER)1).

The following code will return the disk extents for all volumes:

VOLUME_DISK_EXTENTS vde = { 0 };
DWORD bytesReturned = 0;
if ( !::DeviceIoControl( hDevice, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, NULL, 0, 
                         (void*)&vde, sizeof(vde), &bytesReturned, NULL ) )
{
    // Check last error
    int nError = ::GetLastError();
    if ( nError != ERROR_MORE_DATA )
    {
        // Unexpected error -> error out
        throw std::runtime_error( "DeviceIoControl() failed." );
    }

    size_t size = offsetof( VOLUME_DISK_EXTENTS, Extents[vde.NumberOfDiskExtents] );
    std::vector<BYTE> buffer( size );
    if ( !::DeviceIoControl( hDevice, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, NULL, 0, 
                             (void*)buffer.data(), size, &bytesReturned, NULL ) )
    {
        // Unexpected error -> error out
        throw std::runtime_error( "DeviceIoControl() failed." );
    }
    // At this point we have a fully populated VOLUME_DISK_EXTENTS structure
    const VOLUME_DISK_EXTENTS& result =
        *reinterpret_cast<const VOLUME_DISK_EXTENTS*>( buffer.data() );
}
else
{
    // Call succeeded; vde is populated with single disk extent.
}


Additional references:


1) At a guess I would assume, that BYTE[1] begins at a memory address, that is not sufficiently aligned for the alignment requirements of VOLUME_DISK_EXTENTS.
IInspectable
  • 46,945
  • 8
  • 85
  • 181
  • When posting the answer I didn't have access to a disk with multiple extents. The documentation is unclear (to me anyway) whether it returns an error or success code, if the first `DeviceIoControl` call succeeds, but there are additional disk extents. @c00000fd: If you can test the code, I would be thankful, if you posted the result, so I can update the answer as necessary. – IInspectable Oct 25 '15 at 12:01
  • Yes, thank you. Your method does work for `IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS` with more than one disk extent. What I didn't see is that this API is totally a$#backwards. Most WinAPIs return the size of required buffer in bytes if you didn't provide a buffer long enough. Anyway, this still doesn't solve the issue for me. My goal was to write a "shim" method that would allocate memory dynamically independent of `dwIoControlCode` (like it should've been done in this API in the first place.) – c00000fd Oct 25 '15 at 20:03
  • @c00000fd: there is no method that works for every I/O control code, any more than there is a method that works for every Win32 API function. (How do you expect Windows to know how much buffer space the device driver is going to require?) – Harry Johnston Oct 25 '15 at 22:44
  • @HarryJohnston: Well, I just wrote one. How would Windows expect to know the size of the buffer? It'd ask the driver to provide it. Although this API is probably seriously old, going back to 1995 or even older, so knowing how sloppy Microsoft used to write their code back then, it is probably not even in the documentation for the device driver. So yes, in that case, they can't know... – c00000fd Oct 26 '15 at 00:31
  • @c00000fd: you mean trial and error? Sure, that will work, sort of - but I assumed you wanted something a bit more elegant. (Better make sure you thoroughly document the fact that the IO control code may be issued multiple times, since that might have side-effects.) It's true that the device driver API *could* have required every driver to provide a guess as to the output buffer size needed, but it didn't, because it would be inefficient and because you're not supposed to be issuing I/O codes you don't understand anyway. – Harry Johnston Oct 26 '15 at 22:34
  • (Ideally, of course, the OS would allocate the output buffer for you. On modern computers, that might even be practical. But when Windows was originally written, it would have been too slow, and it isn't an important enough API to warrant re-implementation.) – Harry Johnston Oct 26 '15 at 22:39
  • @HarryJohnston: What `side-effects` are you referring to? Can you specify. – c00000fd Oct 27 '15 at 00:25
  • I mean "side-effects" in the programming sense of the word, i.e., the control code might actually *do* something apart from returning data, and it might not be something the caller wanted to happen multiple times. I didn't have any particular control code in mind, so I'll make up an example out of thin air - oh, I don't know - a control code for a computerized toilet, which makes the toilet flush and returns information about the water flow rates during the flush. Calling your function might flush the toilet ten times instead of once. :-) (Which is fine, so long as it's documented!) – Harry Johnston Oct 27 '15 at 00:41
0

Following @IInspectable's advice, here's what I came up with for a more general case:

BYTE* DeviceIoControl_Dynamic(HANDLE hDevice, DWORD dwIoControlCode, DWORD dwszCbInitialSuggested, LPVOID lpInBuffer, DWORD nInBufferSize, DWORD* pncbOutDataSz)
{
    //Calls DeviceIoControl() API by pre-allocating buffer internally
    //'dwIoControlCode' = control code, see DeviceIoControl() API
    //'dwszCbInitialSuggested' = suggested initial size of the buffer in BYTEs, must be set depending on the description of 'dwIoControlCode'
    //'lpInBuffer' = input buffer, see DeviceIoControl() API
    //'nInBufferSize' = size of 'lpInBuffer', see DeviceIoControl() API
    //'pncbOutDataSz' = if not NULL, receives the size of returned data in BYTEs
    //RETURN:
    //      = Data obtained from DeviceIoControl() API -- must be removed with delete[]!
    //      = NULL if error -- check GetLastError() for info
    BYTE* pData = NULL;
    int nOSError = NO_ERROR;

    DWORD ncbSzData = 0;

    if((int)dwszCbInitialSuggested > 0)
    {
        //Initially go with suggested memory size
        DWORD dwcbMemSz = dwszCbInitialSuggested;

        //Try no more than 10 times
        for(int t = 0; t < 10; t++)
        {
            //Reserve mem
            ASSERT(!pData);
            pData = new (std::nothrow) BYTE[dwcbMemSz];
            if(!pData)
            {
                //Memory fault
                nOSError = ERROR_NOT_ENOUGH_MEMORY;
                break;
            }

            //And try calling with that size
            DWORD bytesReturned = 0;
            if(::DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, 
                pData, dwcbMemSz, &bytesReturned, NULL))
            {
                //Got it
                ncbSzData = bytesReturned;
                nOSError = NO_ERROR;

                break;
            }

            //Check last error
            nOSError = ::GetLastError();

            //Knowing how badly Windows drivers are written, don't rely on the last error code!

            //Alloc more memory (we'll just "wing it" on the amount)
            dwcbMemSz += 1024;

            //Free old mem
            delete[] pData;
            pData = NULL;
        }
    }
    else
    {
        //Bad initial size
        nOSError = ERROR_INVALID_MINALLOCSIZE;
    }

    if(pncbOutDataSz)
        *pncbOutDataSz = ncbSzData;

    ::SetLastError(nOSError);
    return pData;
}

and then to call it, say for IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS:

DWORD bytesReturned;
VOLUME_DISK_EXTENTS* p_vde = (VOLUME_DISK_EXTENTS*)DeviceIoControl_Dynamic(hDsk, 
    IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, sizeof(VOLUME_DISK_EXTENTS), NULL, NULL, &bytesReturned);

which can be later used as such:

//Ensure that driver returned the correct data
if(p_vde &&
    offsetof(VOLUME_DISK_EXTENTS, Extents[p_vde->NumberOfDiskExtents]) <= bytesReturned)
{
    //All good
    for(int x = 0; x < p_vde->NumberOfDiskExtents; x++)
    {
        DWORD diskNumber = p_vde->Extents[x].DiskNumber;
        //...
    }
}

//Remember to free mem when not needed!
if(p_vde)
{
    delete[] (BYTE*)p_vde;
    p_vde = NULL;
}
Community
  • 1
  • 1
c00000fd
  • 20,994
  • 29
  • 177
  • 400
  • You've got the arithmetics wrong. `sizeof(VOLUME_DISK_EXTENTS) + (p_vde->NumberOfDiskExtents - 1) * sizeof(p_vde->Extents)` does not take alignment into account. I posted a link in [my answer](http://stackoverflow.com/a/33329198/1889329), that specifically explains, why your calculation is wrong. Plus, your wrapper isn't generic. If you pass *dwszCbInitialSuggested* that is smaller than `sizeof(VOLUME_DISK_EXTENTS)` together with `IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS`, it'll fail. Not good. – IInspectable Oct 26 '15 at 00:41
  • And now that you've committed yourself to one particular allocator, you can no longer [safely put the code in a DLL](https://msdn.microsoft.com/en-us/library/ms235460.aspx). It's also unusual to impose manual memory management on the client, when using C++. You could return a `std::vector`, or at least a `std::unique_ptr`. – IInspectable Oct 26 '15 at 00:53
  • @IInspectable: Yeah, good point about alignment. How do you use `offsetof` macro with a struct pointer? – c00000fd Oct 26 '15 at 01:05
  • As for memory allocation, you can choose whatever pleases you. I just posted my production code. – c00000fd Oct 26 '15 at 01:06
  • `offsetof( VOLUME_DISK_EXTENTS, Extents[p_vde->NumberOfDiskExtents] );` – IInspectable Oct 26 '15 at 01:43
  • @IInspectable: Although you know, I just checked "my math" and it holds true. Did you see that I used `<= bytesReturned` which should account for alignment discrepancy. Also your other point about specifying a smaller original size in `dwszCbInitialSuggested`. Even if you pass 1 byte, it will take several passes to call `DeviceIoControl` but the overall method will eventually succeed. So I'm not sure what you meant there in your first comment? – c00000fd Oct 26 '15 at 02:03
  • Take a disk with 100 extents. That makes your math accumulate 100 times the error. The expected size (which is a lot smaller than the required size) could now be lower than *bytesReturned*, although the real number of bytes (accounting for alignment) is beyond *bytesReturned*. The math is wrong, and works by coincidence. Occasionally. – IInspectable Oct 26 '15 at 02:10
  • @IInspectable: Sorry, you didn't understand how my method works. If there's 100 disk extents, it will require about 2400 bytes (for a 32-bit process.) If I set `dwszCbInitialSuggested` as 1 byte, the first pass will fail, the second pass with 1025 bytes will fail as well, the third pass with 2049 bytes will also fail, but it will succeed on the 4th pass with 3073 bytes of allocated memory. 4 passes is not 100. – c00000fd Oct 26 '15 at 02:20
  • I'm referring to the sanity check in the `//Ensure that driver returned the correct data` part. The math is off, and it can produce false positives, as described in my previous comment. – IInspectable Oct 26 '15 at 02:24
  • @IInspectable: In that case where are you taking this `"the real number of bytes (accounting for alignment) is beyond bytesReturned"`? My calculation method in the sanity check part can only lower the number of bytes required because it doesn't account for the alignment. `bytesReturned` is returned by the driver, or how much data it filled out. I'm checking that the number of `DISK_EXTENT` structs it claims to have found fits the minimum required size. – c00000fd Oct 26 '15 at 02:36
  • [You are checking, if a number of bytes, **or a smaller number** have been returned by the driver.](http://blogs.msdn.com/b/oldnewthing/archive/2004/08/26/220873.aspx). Using `offsetof` returns the true size in bytes, regardless of the alignment rules of any given platform. – IInspectable Oct 26 '15 at 09:53
  • @IInspectable: OK, we're arguing about something that is unrelated to my question. I agree your method is more acceptable for a general case (although it won't matter in my situation) so I changed it to `offsetof` in the example above. – c00000fd Oct 26 '15 at 21:47
  • It might perform better if you allocate a big buffer and then shrink it afterwards rather than looping; many control codes are quite slow, and they'll already have done most of the work by the time they notice the buffer isn't big enough. Or have a big permanent buffer and then copy the data to a new buffer of the exact size. – Harry Johnston Oct 26 '15 at 22:50
  • @HarryJohnston: OK, good point. Although I would probably increment it by 4K instead of 1K, or maybe even 8K. For a modern Windows PC that would not be an issue but that buffer size would definitely be long enough for the driver to fill it with what it needs to. – c00000fd Oct 27 '15 at 00:27
0

You are getting error code ERROR_INVALID_PARAMETER when you have invalid parameter, like its name says. In your case it should be bad handle because all others looks fine, if we expect that dwIoControlCode argument is IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, lpInBuffer and nInBufferSize are ignored.

In insufficient buffer you will get another error code mentioned in above comments.

Lets check what is saying documentation:

DeviceIoControl can accept a handle to a specific device. For example, to open a handle to the logical drive A: with CreateFile, specify \.\a:. Alternatively, you can use the names \.\PhysicalDrive0, \.\PhysicalDrive1, and so on, to open handles to the physical drives on a system.

In other words, when you open handle with "C:\" instead of "\\.\c:" argument in CreateFile and use it in DeviceIoControl, the result is ERROR_INVALID_PARAMETER.

slayer69
  • 81
  • 10