Подтвердить что ты не робот

Немедленное обнаружение ошибок повреждения кучи в Windows. Как?

Я не могу спать!:)

У меня есть достаточно большой проект в Windows и столкнулся с некоторыми проблемами с кучей коррупции. Я прочитал все SO, включая эту приятную тему: Как отлаживать ошибки кучи коррупции?, однако ничто не было подходящим, чтобы помочь мне из коробки. Debug CRT и BoundsChecker обнаружены повреждения кучи, но адреса всегда были разными, а точка обнаружения всегда находилась далеко от фактической памяти. Я не спал до середины ночи и делал следующий взлом:

DWORD PageSize = 0;

inline void SetPageSize()
{
    if ( !PageSize )
    {
        SYSTEM_INFO sysInfo;
        GetSystemInfo(&sysInfo);
        PageSize = sysInfo.dwPageSize;
    }
}

void* operator new (size_t nSize)
{
    SetPageSize();
    size_t Extra = nSize % PageSize;
    nSize = nSize + ( PageSize - Extra );
    return Ptr = VirtualAlloc( 0, nSize, MEM_COMMIT, PAGE_READWRITE);
}

void operator delete (void* pPtr)
{
    MEMORY_BASIC_INFORMATION mbi;
    VirtualQuery(pPtr, &mbi, sizeof(mbi));
    // leave pages in reserved state, but free the physical memory
    VirtualFree(pPtr, 0, MEM_DECOMMIT);
    DWORD OldProtect;
    // protect the address space, so noone can access those pages
    VirtualProtect(pPtr, mbi.RegionSize, PAGE_NOACCESS, &OldProtect);
}

Некоторые ошибки повреждения кучи стали очевидными, и я смог их исправить. Больше не было предупреждений об ошибках Debug CRT при выходе. Однако у меня есть некоторые вопросы относительно этого взлома:

1. Может ли он выдавать ложные срабатывания?

2. Может ли он пропустить некоторые из кучи коррупции? (даже если мы заменим malloc/realloc/free?)

3. Он не может работать на 32-битных с OUT_OF_MEMORY, только на 64-битных. Правильно ли мы просто закончили виртуальное адресное пространство на 32-битных?

4b9b3361

Ответ 1

Может ли он произвести какие-либо ложные срабатывания?

Таким образом, это приведет к сбоям только в ошибках класса "use after free()". Для этого, я думаю, это разумно хорошо.

Если вы попытаетесь delete что-то, что не было new 'ed, это другой тип ошибки. В delete вы должны сначала проверить, действительно ли память была выделена. Вы не должны слепо освобождать память и отмечать ее как недоступную. Я попытался бы избежать этого и сообщить (скажем, сделать разрыв отладки) при попытке delete чего-то, что не должно быть удалено, поскольку оно никогда не было new 'ed.

Можно ли пропустить некоторые из кучи коррупции? (даже если мы заменим malloc/realloc/free?)

Очевидно, что это не будет захватывать все повреждения данных кучи между new и соответствующим delete. Это приведет только к попыткам после delete.

например:.

myObj* = new MyObj(1,2,3);
// corruption of *myObj happens here and may go unnoticed
delete myObj;

Он не работает на 32-битной цели с ошибкой OUT_OF_MEMORY, только на 64-битной. Правильно ли, что мы просто закончили виртуальное адресное пространство на 32-битных?

Обычно у вас есть около ~ 2 ГБ виртуального адресного пространства на 32-битной Windows. Это полезно не более ~ 524288 new, как в предоставленном коде. Но с объектами размером больше 4 КБ вы сможете успешно распределить меньше экземпляров. И тогда фрагментация адресного пространства еще больше уменьшит это число.

Это совершенно ожидаемый результат, если вы создаете множество экземпляров объектов в течение жизненного цикла вашей программы.

Ответ 2

Это не будет ловить:

  • использование неинициализированной памяти (как только ваш указатель будет выделен, вы можете прочитать мусор из него по своему усмотрению)
  • переполнение буфера (если только вы не наберете границу PageSize)

В идеале вы должны написать хорошо известный шаблон битов до и после выделенных блоков, чтобы operator delete мог проверить, были ли они перезаписаны (указанный буфер перегружен или запущен).

В настоящее время это будет разрешено тихо в вашей схеме, а возврат обратно к malloc и т.д. позволит ему мгновенно повредить кучу и позже появиться как ошибка (например, при освобождении блока после перегрузки, запустите его).

Вы не можете все поймать: обратите внимание, например, что если основная проблема (допустимая) указатель где-то переписывается с помощью мусора, вы не можете обнаружить ее до тех пор, пока поврежденный указатель не будет указан.

Ответ 3

Да, в вашем текущем ответе могут отсутствовать повреждения кучи буфера ниже и перерасхода.
Функция delete() довольно хороша!
Я реализовал функцию new() аналогичным образом, что добавляет защитные страницы как для недоимок, так и для перерасхода.
Из документации GFlags я делаю вывод, что она защищает только от перерасхода.

Обратите внимание, что при возвращении просто указателя рядом с страницей защиты недогрузки, защитная страница для перерасхода, скорее всего, будет удаляться от выделенного объекта и ближайшей близости после того, как выделенный объект НЕ охраняемая.
Чтобы компенсировать это, нужно было бы вернуть такой указатель, что объект находится непосредственно перед страницей защиты от переполнения (в этом случае реверс менее вероятен для обнаружения).
Нижеприведенный код делает одно или другое поочередно для каждого вызова new(). Или, возможно, вы захотите изменить его, чтобы вместо него использовать потоковый случайный генератор, чтобы предотвратить любые помехи с кодом, вызывающим новый(). Учитывая все это, следует помнить, что обнаружение недоиспользования и перерасхода по приведенному ниже коду по-прежнему вероятностно - это особенно актуально в том случае, когда некоторые объекты выделяются только один раз в течение всей продолжительности программы.

NB!. Поскольку new() возвращает измененный aadress, функция delete() также должна была быть скорректирована немного, поэтому теперь она использует mbi.AllocationBase вместо ptr для VirtualFree() и VirtualProtect().

PS. Driver Verifier Специальный пул использует аналогичные трюки.

volatile LONG priorityForUnderrun = rand(); //NB! init with rand so that the pattern is different across program runs and different checks are applied to global singleton objects

void ProtectMemRegion(void* region_ptr, size_t sizeWithGuardPages)
{
    size_t preRegionGuardPageAddress = (size_t)region_ptr;
    size_t postRegionGuardPageAddress = (size_t)(region_ptr) + sizeWithGuardPages - PageSize;   

    DWORD flOldProtect1;
    BOOL preRegionProtectSuccess = VirtualProtect(
        (void*)(preRegionGuardPageAddress),
        pageSize,
        PAGE_NOACCESS,
        &flOldProtect1  
    );

    DWORD flOldProtect2;
    BOOL postRegionProtectSuccess = VirtualProtect(
        (void*)(postRegionGuardPageAddress),
        PageSize,
        PAGE_NOACCESS,
        &flOldProtect2  
    );
}   

void* operator new (size_t size)
{
    size_t sizeWithGuardPages = (size + PageSize - 1) / PageSize * PageSize + 2 * PageSize;

    void* ptr = VirtualAlloc
    (
        NULL,
        sizeWithGuardPages,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE
    );

    if (ptr == NULL)    //NB! check for allocation failures
    {
        return NULL;
    }

    ProtectMemRegion(ptr, sizeWithGuardPages);

    void* result;
    if (InterlockedIncrement(&priorityForUnderrun) % 2)
        result = (void*)((size_t)(ptr) + pageSize);
    else 
        result = (void*)(((size_t)(ptr) + sizeWithGuardPages - pageSize - size) / sizeof(size_t) * sizeof(size_t)); 

    return result;
}   

void operator delete (void* ptr) 
{
    MEMORY_BASIC_INFORMATION mbi;
    DWORD OldProtect;

    VirtualQuery(ptr, &mbi, sizeof(mbi));
    // leave pages in reserved state, but free the physical memory
    VirtualFree(mbi.AllocationBase, 0, MEM_DECOMMIT);
    // protect the address space, so noone can access those pages
    VirtualProtect(mbi.AllocationBase, mbi.RegionSize, PAGE_NOACCESS, &OldProtect);
}