• # Un peu d'explications sur le bug introduit

    Posté par . En réponse au journal Merci aux développeurs de la GNU libc !. Évalué à 10.

    Je ne vais pas parler ici des considérations politiques mais j'ai trouvé que le bug introduit dans la glibc est particulièrement intéressant. Pour ceux qui n'ont épluché les liens du journal, voici une tentative d'explication.

    Dans gcc 4.6 a été ajouté un attribut de fonction leaf (cf Function Attributes). Cet attribut permet de signifier qu'une fonction ne va fait appel à aucune autre fonction. Lorsque le compilateur voit qu'on appel une fonction "leaf", il sait que toutes les variables de l'unité de compilation courante qui ne sont pas passées à la fonction ne pourront être modifiées par cette fonction et peut ainsi optimiser plus agressivement. Par exemple si j'écris le code suivant :

    static int g_count; /* variable globale */
    __attribute__((__leaf__))
    int calcValue(int);
    int incGCount()
    {
     g_count++;
    }
    int myFunc2()
    {
     int i;
     int result = 0;
     g_count=0;
     for (i=0; i != 3 && g_count != 3; i++)
     {
     result+=calcValue(i);
     ++g_count;
     }
     return result;
    }
    
    

    Ici, calcValue étant marqué comme leaf, on sait que cette fonction ne va appeler aucune fonction de notre unité de compilation, en particulier elle n’appellera pas incGCount() et donc, voyant que i et g_count ont toujours la même valeur, le compilateur peut tout à fait remplacer la fonction myFunc2 par ceci :

    int myFunc2()
    {
     g_count=3;
     return calcValue(0)+calcValue(1)+calcValue(2);
    }
    
    

    Autre observation, les fonctions (entre autre) pthread_mutex_lock et pthread_mutex_unlock ne font appel à aucune fonction (en dehors de leur unité de compilation et elles n’appellent pas non plus de callback), donc elles ont été marquées comme leaf par les mainteneurs de la glibc. Et donc si je veux rendre myFunc2 thread-safe en protégeant l'accès à g_count par un mutex, je ferais :

    int incGCount()
    {
     pthread_mutex_lock(&g_mutex);
     g_count++;
     pthread_mutex_unlock(&g_mutex);
    }
    int myFunc2()
    {
     int i;
     int result = 0;
     pthread_mutex_lock(&g_mutex);
     g_count=0;
     pthread_mutex_unlock(&g_mutex);
     for (i=0; i != 3 && g_count != 3; i++)
     {
     result+=calcValue(i);
     pthread_mutex_lock(&g_mutex);
     g_count++;
     pthread_mutex_unlock(&g_mutex);
     }
    }
    
    

    La fonction peut être optimisée par le compilateur qui sait que ni pthread_mutex_lock ni pthread_mutex_unlock ne vont consulter ou modifier g_count. Or si ces fonctions ne touchent évidemment pas g_count, en revanche le mutex sert à protéger l'accès à g_count et la prise du mutex peut bloquer parce qu'un autre thread a pris le mutex et est en train d'appeler incGCount(). Le compilateur ayant pris comme postulat que g_count n'était pas modifié a pu convertir le code comme ceci :
    int myFunc2()
    {
     pthread_mutex_lock(&g_mutex); pthread_mutex_unlock(&g_mutex);
     pthread_mutex_lock(&g_mutex); pthread_mutex_unlock(&g_mutex);
     pthread_mutex_lock(&g_mutex); pthread_mutex_unlock(&g_mutex);
     pthread_mutex_lock(&g_mutex); pthread_mutex_unlock(&g_mutex);
     
     g_count=3;
     return calcValue(0)+calcValue(1)+calcValue(2);
    }
    
    

    Et là c'est le drame et heap corruption via multi-threaded "git grep".

    Étienne