Что может объяснить повреждение кучи при вызове free()?
Я отлаживал крах в течение нескольких дней, что происходит в глубинах OpenSSL (обсуждение с сопровождающими здесь). Я потратил некоторое время на исследование, поэтому постараюсь сделать этот вопрос интересным и информативным.
Во-первых, и чтобы дать некоторый контекст, моя минимальная выборка, которая воспроизводит краш, следующая:
#include <openssl/crypto.h>
#include <openssl/ec.h>
#include <openssl/objects.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/engine.h>
int main()
{
ERR_load_crypto_strings(); OpenSSL_add_all_algorithms();
ENGINE_load_builtin_engines();
EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_sect571k1);
EC_GROUP_set_point_conversion_form(group, POINT_CONVERSION_UNCOMPRESSED);
EC_KEY* eckey = EC_KEY_new();
EC_KEY_set_group(eckey, group);
EC_KEY_generate_key(eckey);
BIO* out = BIO_new(BIO_s_file());
BIO_set_fp(out, stdout, BIO_NOCLOSE);
PEM_write_bio_ECPrivateKey(out, eckey, NULL, NULL, 0, NULL, NULL); // <= CRASH.
}
В принципе, этот код генерирует ключ Эллиптической кривой и пытается вывести его в стандартный stdout
. Подобный код можно найти в openssl.exe ecparam
и в Wikis онлайн. Он отлично работает на Linux (valgrind вообще не сообщает об ошибке). Он только сбой в Windows (Visual Studio 2013 - x64). Я убедился, что правильное время работы связано с (/MD
в моем случае, для всех зависимостей).
Не боясь зла, я перекомпилировал OpenSSL в x64-debug (на этот раз, связав все в /MDd
), и /MDd
через код, чтобы найти набор команд для защиты. Мой поиск привел меня к этому коду (в файле OpenSSL tasn_fre.c
):
static void asn1_item_combine_free(ASN1_VALUE **pval, const ASN1_ITEM *it, int combine)
{
// ... some code, not really relevant.
tt = it->templates + it->tcount - 1;
for (i = 0; i < it->tcount; tt--, i++) {
ASN1_VALUE **pseqval;
seqtt = asn1_do_adb(pval, tt, 0);
if (!seqtt) continue;
pseqval = asn1_get_field_ptr(pval, seqtt);
ASN1_template_free(pseqval, seqtt);
}
if (asn1_cb)
asn1_cb(ASN1_OP_FREE_POST, pval, it, NULL);
if (!combine) {
OPENSSL_free(*pval); // <= CRASH OCCURS ON free()
*pval = NULL;
}
// Some more code...
}
Для тех, кто не слишком хорошо разбирается в OpenSSL и его процедурах ASN.1, в основном то, что делает это for
-loop, заключается в том, что он проходит через все элементы последовательности (начиная с последнего элемента) и "удаляет" их (подробнее об этом позже).
Прямо перед сбоем происходит удаление из трех элементов (при *pval
, который равен 0x00000053379575E0
). Глядя на память, можно увидеть следующее:
Последовательность имеет длину 12 байт, каждый элемент имеет длину 4 байта (в данном случае 2
, 5
и 10
). На каждой итерации цикла элементы "удаляются" OpenSSL (в этом контексте не delete
ни delete
ни free
: они просто установлены на определенное значение). Вот как выглядит память после одной итерации:
Последний элемент здесь был установлен как ff ff ff 7f
который, как я полагаю, является способом OpenSSL для обеспечения отсутствия утечки информации о ключах, когда память не будет распределена позже.
Сразу после цикла (и до вызова OPENSSL_free()
) память выглядит следующим образом:
Все элементы были установлены в ff ff ff 7f
, asn1_cb
- NULL
поэтому вызов не производится. Следующее, что происходит, это вызов OPENSSL_free(*pval)
.
Этот вызов free()
на то, что кажется действительной и выделенной памятью, терпит неудачу и приводит к тому, что выполнение прерывается сообщением: "HEAP CORRUPTION DETECTED".
Любопытно, что я подключился к malloc
, realloc
и free
(как разрешено OpenSSL), чтобы гарантировать, что это не было двойной или свободной на никогда не выделенной памяти. Оказывается, память на 0x00000053379575E0
действительно представляет собой 12-байтовый блок, который действительно был выделен (и никогда не был освобожден раньше).
Я нахожусь в убытке, выясняя, что происходит здесь: из моих исследований кажется, что free()
терпит неудачу в указателе, который обычно возвращался malloc()
. В дополнение к этому, это место памяти записывалось в пару инструкций, прежде чем без каких-либо проблем, подтверждающих гипотезу о том, что память будет правильно распределена.
Я знаю, что трудно, если не невозможно, отлаживать удаленно без всякой информации, но я понятия не имею, какие должны быть мои следующие шаги.
Итак, мой вопрос: как именно это "HEAP CORRUPTION" обнаружено отладчиком Visual Studio? Каковы все возможные причины для этого, исходя из вызова free()
?
Ответы
Ответ 1
В целом, возможности включают:
- Дублируйте бесплатно.
- До дублирования бесплатно.
- (Наиболее вероятно). Ваш код написал за пределами выделенного фрагмента памяти либо до начала, либо после конца.
malloc()
и друзья добавляют дополнительную информацию о бухгалтерском учете здесь, например, размер и, вероятно, проверку на работоспособность, которую вы не сможете перезаписать. - Освободить то, что не было
malloc()
-ed. - Продолжая писать в кусок, который уже был
free()
-d.
Ответ 2
Я мог бы, наконец, найти проблему и решить ее.
Оказалось, что какая-то инструкция записывала байты через выделенный буфер кучи (следовательно, 0x00000000
вместо ожидаемого 0xfdfdfdfd
).
В режиме отладки эта перезапись защитных устройств памяти остается необнаруженной, пока память не будет освобождена с помощью free()
или перераспределена с помощью realloc()
. Вот почему я столкнулся с сообщением HEAP CORRUPTION.
Я ожидаю, что в режиме выпуска это может иметь драматические эффекты, такие как перезапись допустимого блока памяти, используемого где-то еще в приложении.
В будущем для людей, сталкивающихся с подобными проблемами, вот как я это сделал:
OpenSSL предоставляет функцию CRYPTO_set_mem_ex_functions()
, определенную так:
int CRYPTO_set_mem_ex_functions(void *(*m) (size_t, const char *, int),
void *(*r) (void *, size_t, const char *,
int), void (*f) (void *))
Эта функция позволяет подключать и заменять функции выделения/освобождения памяти в OpenSSL. Хорошая вещь - это добавление параметров const char *, int
которые в основном заполняются для вас OpenSSL и содержат имя файла и номер строки выделения.
Вооружившись этой информацией, было легко узнать место, где был выделен блок памяти. Затем я мог бы пройти через код, глядя на инспектор памяти, ожидая, что блок памяти будет поврежден.
В моем случае, что случилось:
if (!combine) {
*pval = OPENSSL_malloc(it->size); // <== The allocation is here.
if (!*pval) goto memerr;
memset(*pval, 0, it->size);
asn1_do_lock(pval, 0, it);
asn1_enc_init(pval, it);
}
for (i = 0, tt = it->templates; i < it->tcount; tt++, i++) {
pseqval = asn1_get_field_ptr(pval, tt);
if (!ASN1_template_new(pseqval, tt))
goto memerr;
}
ASN1_template_new()
вызывается для трех элементов последовательности для их инициализации.
ASN1_template_new()
вызовы в свою очередь asn1_item_ex_combine_new()
который делает это:
if (!combine)
*pval = NULL;
pval
является ASN1_VALUE**
, эта инструкция устанавливает 8 байтов в системах Windows x64 вместо предполагаемых 4 байтов, что приводит к повреждению памяти для последнего элемента списка.
Для полного обсуждения того, как эта проблема была решена выше, см. Эту ветку.