How to replace IsBadReadPtr?

By Stephen Kellett
26 May, 2010

The Microsoft Win32 API contains various functions that look useful at first sight but which have now become regarded as a pariahs. A good example of this is the IsBadReadPtr() function.

Under the hood this function uses structured exception handling to catch any exceptions that are thrown when the memory location read. If an exception is thrown the function returns TRUE, otherwise FALSE. So far so good.

So what could be wrong with that? A simplistic or naive interpretation would be “nothing”. But that ignores the fact that the callstacks of your application threads grow “on-demand”. This is done to avoid committing the full (default) 1MB upfront for each thread. This places a lower demand on application virtual memory and provides slightly faster startup for each thread. To allow each thread to grow on demand the stack has guard pages, which if you try to access them cause a guard page exception to be thrown which the OS handles gracefully, extends your stack space an appropriate amount and then returns execution to your application.

The problem with IsBadReadPtr() is that the exception handler inside IsBadReadPtr() eats the exception and thus the OS will not see it. So for the case where you end up using IsBadReadPtr() on a guard page you break the on-demand stack extension mechanism.

Raymond Chen of Microsoft has written a passionate post on this topic.

Raymond (and a few other folks) say that you should never use IsBadReadPtr(). I think thats a bit strong.

There are a few occasions where you may know what the datastructure is but you also know that it may have various memory protections on it. Such a case is when inspecting a DLL. Various parts are readonly. We have found during the last 10 years of writing tools like Memory Validator that it is not uncommon for a DLL loaded by LoadLibrary() to have memory protections on parts of the DLL that you don’t expect. We can’t control what DLLs our customer’s applications choose to load, so we have to handle all eventualities. We can’t just allow a crash to happen because we read a data location (in a customer DLL) that should be valid but isn’t.

Its also worth noting that the members of the team that wrote Boundschecker also came to the same conclusion and also tested certain DLL headers this way. You can find such code examples in the BugSlayer column in issues of Microsoft Systems Journal (MSJ) before it morphed into MSDN magazine.

One argument would be “Put an exception handler around it. Its an exceptional condition, handle it that way”.

The problem with that is sometimes that breaks the flow of the code and causes all manner of problems with the inability to mix C++ objects and SEH in the same function. Sometimes its much easier and simpler just to test for readability and abandon the function if you encounter one of these unusually constructed DLLs.

We are not advocating that you routinely use IsBadReadPtr() to hide the fact that you don’t know which objects to free, so you just call free on anything that passes IsBadReadPtr(). If you do that you will end up with exactly the problems that Raymond Chen describes.

But for the case where you do want IsBadReadPtr() functionality but you don’t want to use IsBadReadPtr(), what do you do? Here are drop in replacements for IsBadReadPtr() and IsBadWritePtr() that will not affect guard pages etc.

int isNotOKToReadMemory(void    *ptr,
                        DWORD   size)
{
    SIZE_T                      dw;
    MEMORY_BASIC_INFORMATION    mbi;
    int                         ok = TRUE;

    do 
    {
        dw = VirtualQuery(ptr, &mbi, sizeof(mbi));
        if (dw == 0)
        {
            // failed to get data on address, must be bad (kernel address)

            ok = FALSE;
            break;
        }

        if (!((mbi.Protect & PAGE_READONLY) ||
              (mbi.Protect & PAGE_READWRITE) ||
              (mbi.Protect & PAGE_WRITECOPY) ||
              (mbi.Protect & PAGE_EXECUTE_READ) ||
              (mbi.Protect & PAGE_EXECUTE_READWRITE) ||
              (mbi.Protect & PAGE_EXECUTE_WRITECOPY)))
        {
            ok = FALSE;
        }

        // check the page is not a guard page

        if (mbi.Protect & PAGE_GUARD)
            ok = FALSE;
        if (mbi.Protect & PAGE_NOACCESS)
            ok = FALSE;

        if (!ok)
            break;

        if (((DWORD_PTR)ptr + size) <= ((DWORD_PTR)mbi.BaseAddress + mbi.RegionSize))
            break;

        // move to end of buffer

        ptr = (void *)((DWORD_PTR)ptr + size - 1);
    } while(TRUE);      //lint !e506

    return !ok;
}

int isNotOKToWriteMemory(void   *ptr,
                         DWORD  size)
{
    SIZE_T                      dw;
    MEMORY_BASIC_INFORMATION    mbi;
    int                         ok;

    dw = VirtualQuery(ptr, &mbi, sizeof(mbi));
    if (dw == 0)
    {
        // failed to get data on address, must be bad (kernel address)

        ok = FALSE;
    }
    else
    {
        ok = ((mbi.Protect & PAGE_READWRITE) ||
              (mbi.Protect & PAGE_WRITECOPY) ||
              (mbi.Protect & PAGE_EXECUTE_READWRITE) ||
              (mbi.Protect & PAGE_EXECUTE_WRITECOPY));

        // check the page is not a guard page

        if (mbi.Protect & PAGE_GUARD)
            ok = FALSE;
        if (mbi.Protect & PAGE_NOACCESS)
            ok = FALSE;

        if (ok && (size > 0))
        {
            // check that our memory range is within this page range

            BYTE    *bp;

            bp = (BYTE *)ptr;
            bp += size;
            if (bp > ((BYTE *)mbi.BaseAddress + mbi.RegionSize))
            {
                // check the page for the end of the range (we'll assume the bit between is OK)

                dw = VirtualQuery(bp - 1, &mbi, sizeof(mbi));
                if (dw == 0)
                {
                    // failed to get data on address, must be bad (kernel address)

                    ok = FALSE;
                }
                else
                {
                    ok = ((mbi.Protect & PAGE_READWRITE) ||
                          (mbi.Protect & PAGE_WRITECOPY) ||
                          (mbi.Protect & PAGE_EXECUTE_READWRITE) ||
                          (mbi.Protect & PAGE_EXECUTE_WRITECOPY));

                    // check the page is not a guard page

                    if (mbi.Protect & PAGE_GUARD)
                        ok = FALSE;
                    if (mbi.Protect & PAGE_NOACCESS)
                        ok = FALSE;
                }
            }
        }
    }

    return !ok;
}

int isNotOKToReadWriteMemory(void   *ptr,
                             SIZE_T size)
{
    SIZE_T                      dw;
    MEMORY_BASIC_INFORMATION    mbi;
    int                         ok;

    dw = VirtualQuery(ptr, &mbi, sizeof(mbi));
    if (dw == 0)
    {
        // failed to get data on address, must be bad (kernel address)

        ok = FALSE;
    }
    else
    {
        ok = ((mbi.Protect & PAGE_READONLY) ||
              (mbi.Protect & PAGE_READWRITE) ||
              (mbi.Protect & PAGE_WRITECOPY) ||
              (mbi.Protect & PAGE_EXECUTE_READ) ||
              (mbi.Protect & PAGE_EXECUTE_READWRITE) ||
              (mbi.Protect & PAGE_EXECUTE_WRITECOPY));

        // check the page is not a guard page

        if (mbi.Protect & PAGE_GUARD)
            ok = FALSE;
        if (mbi.Protect & PAGE_NOACCESS)
            ok = FALSE;
    }

    return !ok;
}

Remember: Use with caution, use sparingly and only if you need to. If you are routinely using IsBadReadPtr() or an equivalent to avoid keeping track of which data you should or should not use, you should think again about your software design.

Fully functional, free for 30 days