Que peut expliquer la corruption de tas lors d’un appel à free ()?

Je suis en train de déboguer un crash depuis plusieurs jours, qui se produit dans les profondeurs d’OpenSSL (discussion avec les responsables ici ). J’ai pris un peu de temps pour enquêter, alors je vais essayer de rendre cette question intéressante et informative.

Tout d’abord et pour donner un peu de contexte, mon échantillon minimal reproduisant le crash est le suivant:

#include  #include  #include  #include  #include  #include  int main() { ERR_load_crypto_ssortingngs(); 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. } 

Fondamentalement, ce code génère une clé à courbe elliptique et tente de la sortir sur stdout . Un code similaire peut être trouvé dans openssl.exe ecparam et sur les wikis en ligne. Cela fonctionne très bien sous Linux (valgrind ne rapporte aucune erreur). Il se bloque uniquement sur Windows (Visual Studio 2013 – x64). Je me suis assuré que les temps d’exécution appropriés étaient liés à ( /MD dans mon cas, pour toutes les dépendances).

Ne craignant aucun mal, j’ai recompilé OpenSSL dans x64-debug (cette fois, tout en liant tout dans /MDd ), puis /MDd parcouru le code pour trouver le jeu d’instructions incriminé. Ma recherche m’a conduit à ce code (dans le fichier tasn_fre.c d’OpenSSL):

 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 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... } 

Pour ceux qui ne sont pas trop familiers avec OpenSSL et ses routines ASN.1, en gros, ceci for -loop fait en sorte qu’il passe par tous les éléments d’une séquence (en commençant par le dernier élément) et les “supprime” (plus à ce sujet plus tard) .

Juste avant que le crash ne se produise, une séquence de 3 éléments est supprimée (à *pval , qui est 0x00000053379575E0 ). En regardant la mémoire, on peut voir les choses suivantes se produire:

vidage mémoire n ° 1

La séquence est longue de 12 octets, chaque élément étant long de 4 octets (dans ce cas, 2 , 5 et 10 ). A chaque itération de boucle, les éléments sont “supprimés” par OpenSSL (dans ce contexte, ni delete ni free sont appelés: ils sont simplement définis avec une valeur spécifique). Voici à quoi ressemble la mémoire après une itération:

vidage de la mémoire n ° 2

Le dernier élément ici a été défini sur ff ff ff 7f ce qui, je suppose, est le moyen utilisé par OpenSSL pour garantir qu’aucune information clé ne fuit lorsque la mémoire est désallouée ultérieurement.

Juste après la boucle (et avant l’appel de OPENSSL_free() ), la mémoire est la suivante:

vidage de la mémoire n ° 3

Tous les éléments ont été réglés sur ff ff ff 7f , asn1_cb étant NULL , aucun appel n’est effectué. La prochaine chose qui se passe est l’appel à OPENSSL_free(*pval) .

Cet appel à free() sur ce qui semble être une mémoire valide et allouée échoue et provoque l’interruption de l’exécution avec le message “HEAP CORRUPTION DETECTED” .

Curieux à ce sujet, je me suis accroché à malloc , realloc et free (comme le permet OpenSSL) pour s’assurer que ce n’était pas une mémoire double libre ou libre sur jamais allouée. Il s’avère que la mémoire à 0x00000053379575E0 est en réalité un bloc de 12 octets qui a été effectivement alloué (et n’a jamais été libéré auparavant).

Je ne sais pas trop ce qui se passe ici: d’après mes recherches, il semble que free() échoue sur un pointeur qui était normalement renvoyé par malloc() . En plus de cela, cet emplacement de mémoire était en train d’être écrit dans quelques instructions avant sans aucun problème qui confirme l’hypothèse que la mémoire soit allouée correctement.

Je sais qu’il est difficile, voire impossible, de déboguer à distance sans toutes les informations, mais je n’ai aucune idée de ce que devraient être mes prochaines étapes.

Ma question est donc la suivante: comment exactement “HEAP CORRUPTION” est-il détecté par le débogueur de Visual Studio? Quelles en sont toutes les causes possibles lorsqu’il provient d’un appel à free() ?

En général, les possibilités comprennent:

  1. Dupliquer gratuitement.
  2. Pré- duplicata gratuit.
  3. (Le plus probable) Votre code a écrit au-delà des limites du bloc de mémoire alloué, avant le début ou après la fin. malloc() et ses amis y ont ajouté des informations de tenue de livres supplémentaires, telles que la taille et probablement une vérification de leur santé mentale, que vous échouerez en écrasant.
  4. Libérer quelque chose qui n’avait pas été malloc() .
  5. Continuer à écrire sur un morceau déjà free() -d.

Je pouvais enfin trouver le problème et le résoudre.

Une instruction s’est avérée être en train d’écrire des octets au-delà du tampon de tas alloué (d’où le 0x00000000 au lieu du 0xfdfdfdfd attendu).

En mode débogage, cet écrasement des gardes de la mémoire rest non détecté jusqu’à ce que la mémoire soit libérée avec free() ou réaffectée avec realloc() . C’est ce qui a provoqué le message HEAP CORRUPTION auquel j’ai fait face.

Je pense qu’en mode de publication, cela aurait pu avoir des effets dramatiques, comme la réécriture d’un bloc de mémoire valide utilisé ailleurs dans l’application.


Pour référence future aux personnes confrontées à des problèmes similaires, voici ce que j’ai fait:

OpenSSL fournit une fonction CRYPTO_set_mem_ex_functions() , définie comme suit:

 int CRYPTO_set_mem_ex_functions(void *(*m) (size_t, const char *, int), void *(*r) (void *, size_t, const char *, int), void (*f) (void *)) 

Cette fonction vous permet d’accrocher et de remplacer des fonctions d’allocation / libération de mémoire dans OpenSSL. La bonne chose est l’ajout des parameters const char *, int qui sont essentiellement renseignés pour vous par OpenSSL et contiennent le nom du fichier et le numéro de ligne de l’atsortingbution.

Armé de cette information, il était facile de trouver l’endroit où le bloc de mémoire était alloué. Je pouvais alors parcourir le code tout en regardant l’inspecteur de mémoire en attente de la corruption du bloc de mémoire.

Dans mon cas, ce qui s’est passé était:

 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() est appelée sur les 3 éléments de séquence pour les initialiser.

ASN1_template_new() appels asn1_item_ex_combine_new() qui effectue ceci:

 if (!combine) *pval = NULL; 

pval étant un ASN1_VALUE** , cette instruction définit 8 octets sur les systèmes Windows x64 au lieu des 4 octets prévus, ce qui entraîne une corruption de la mémoire pour le dernier élément de la liste.

Pour une discussion complète sur la façon dont ce problème a été résolu en amont, voir ce fil .