- L’erreur de couleur JPG de SerenityOS ressemblait à un problème d’ordre des arguments RGB/BGR, mais elle venait en réalité du fait que
JPGLoaderconfiait l’ordre de composants nécessitant un ordre précis à l’ordre d’itération d’uneHashTable - Avec l’introduction de
malloc_good_size()dansAK+LibC,VectoretHashTablese sont mis à exploiter la taille réelle des chunks malloc, ce qui a modifié le nombre de buckets de la HashTable et révélé un bug caché - Le code existant lisait par hasard les composants JPG
Y,Cb,Crdans le bon ordre ; la correspondance entre le résultat deint_hashet le nombre de buckets masquait une erreur de traitement du flux de Huffman - La recherche de la cause a commencé alors que
JPGLoader.cppn’avait pas été modifié récemment, et pendant un bisect sur 1 000 commits, des changements dans AK ont imposé plusieurs reconstructions complètes de l’OS, soit environ 3 400 fichiers - Le correctif final a consisté à parcourir les composants dans un ordre déterministe ; se contenter de changer l’ordre des arguments de couleur n’aurait été qu’un expédient susceptible de recréer le même problème au prochain changement d’ordre
Une erreur de couleur JPG qui ressemblait à une confusion RGB/BGR
- Dans SerenityOS, l’ouverture d’images JPG provoquait un affichage incorrect des couleurs
- En changeant l’ordre des arguments du constructeur
ColordansJPGLoader.cpp, l’image semblait redevenir normale- Code existant : passage dans l’ordre
Y,Cb,Cr - Changement provisoire : passage dans l’ordre
Cr,Cb,Y
- Code existant : passage dans l’ordre
- Mais le dernier changement non-revert récent de
JPGLoader.cppremontait, d’après Git, à plus d’un mois, et le souvenir était qu’une image de fond JPG s’affichait correctement une à deux semaines auparavant - Il devenait donc probable qu’il ne s’agissait pas d’une simple erreur d’ordre des canaux de couleur, mais qu’un autre changement avait exposé un bug existant
Un bisect rendu difficile par les changements dans AK
- SerenityOS utilise sa propre bibliothèque standard, AK (Agnostic Kit)
- AK joue un rôle proche de la STL C++, mais évolue dans le même dépôt que le code du système d’exploitation
- Quand AK change, la portée de l’impact est large
- La bibliothèque standard est incluse par presque tout le code
- Les templates C++ devant être définis dans les headers, une modification des headers d’AK déclenche de vastes recompilations
- À chaque commit contenant un changement dans AK, il fallait reconstruire tout le système d’exploitation
- Environ 3 400 fichiers au moment de l’écriture
- Pendant le bisect sur une plage de 1 000 commits, une compilation complète a été effectuée 4 à 5 fois sur un ordinateur portable Sandy Bridge Mobile de 2011
ccachen’a pas pu gérer ce cas, et en raison du rythme rapide des changements dans le projet SerenityOS, AK était modifié environ une fois tous les 100 commits
Le problème caché révélé par malloc_good_size()
- Après avoir bisecté 1 000 commits, le changement qui cassait les couleurs JPG a été trouvé non pas dans
JPGLoader, mais du côté deAK+LibC - Le commit qui révélait le problème était
f89e8fb71a4893911ee5125f34bd5bbb99327d33- Titre :
AK+LibC: Implement malloc_good_size() and use it for Vector/HashTable - Date : 15 mai 2021
- Titre :
- Ce commit implémente l’API macOS
malloc_good_size()- Elle renvoie, pour une taille d’allocation demandée, la taille réellement allouée
- Par exemple, si une demande de 35 octets utilise en interne un chunk de 64 octets, les 29 octets restants peuvent être exploités
- Après ce changement,
Vector,HashTableet d’autres structures ont commencé à mieux utiliser la mémoire disponible dans les chunks malloc - Comme l’image JPG s’affichait correctement au commit précédent, la piste s’est resserrée autour de ce changement comme révélateur d’un problème caché existant
Un décodage qui dépendait de la capacité de la HashTable
- Au départ, le soupçon portait sur un éventuel code dans
JPGLoaderou dans du code de plus haut niveau qui dépendrait à tort de la capacité d’unVectorpour y écrire directement - Le changement concernait à la fois
HashTableetVector, tous deux utilisés dans le code deJPGLoader - En supprimant au hasard la ligne appliquant
kmalloc_good_size()côtéHashTable, puis en reconstruisant, le problème a disparu- Le code supprimé était la partie qui ajustait la nouvelle capacité en buckets à la taille réelle allouée
- Ce résultat a confirmé qu’un changement du nombre de buckets de la
HashTableinfluençait le résultat du décodage JPG - Une
HashTablen’étant pas un conteneur destiné à être utilisé comme un flux de données contigu, sa capacité ou son ordre d’itération ne devaient pas être des dépendances du code
La manière dont les composants JPG étaient traités
- L’ancien
JPGLoaderlisait les informations de composants dans la section Start of Frame du fichier JPG et les stockait dans une structureComponent - Chaque
Componentpossédait unserial_idindiquant sa position dans le fichier JPG- L’ordre des composants JPG doit généralement être
Y,Cb,Cr
- L’ordre des composants JPG doit généralement être
- Ces composants étaient stockés dans une
HashTable- Ils étaient ensuite utilisés pour vérifier que l’ordre des composants dans la section Start of Scan correspondait à l’ordre attendu
- À l’étape du décodage, ces composants étaient parcourus afin d’utiliser les informations nécessaires à la transformation des macroblocs
- Le problème venait du fait que des composants pour lesquels l’ordre est important étaient placés dans une
HashTable, puis parcourus avec l’itérateur par défaut
Différence d’ordre d’itération entre le commit cassé et le commit fonctionnel
- Dans le commit produisant les couleurs cassées, la sortie de debug parcourait les composants dans l’ordre suivant
021
- Dans le commit fonctionnel précédent, l’ordre était différent
012
- Cette différence se traduisait par un résultat ressemblant à une inversion des canaux de couleur
- En modifiant manuellement l’ordre des composants avec CxByte, l’erreur suivante est apparue
Huffman stream exhausted. This could be an error!Failed to build Macroblock 3277
- Cette erreur a montré que le décodage JPG était sensible à l’ordre du flux, et a confirmé que l’ordre de parcours des composants était la cause principale
Un ordre de HashTable qui tombait juste par hasard
- La cause fondamentale était le stockage d’objets nécessitant un ordre dans une
HashTable, puis leur parcours via l’itérateur par défaut - Le hash des ID de composants JPG passait par
int_hashpour sélectionner les buckets - Jusqu’alors, deux hasards coïncidaient
- Les résultats de
int_hashpour les valeurs0,1,2étaient stables - Le nombre de buckets de
AK::HashTabletombait juste pour placer les composants dans le bon ordre
- Les résultats de
- Grâce à cette coïncidence,
JPGLoaderlisait le flux de Huffman dans le bon ordre pour chaque composant, et le bug était masqué depuis le début - L’introduction de
malloc_good_size()a modifié le nombre de buckets de laHashTable, ce qui a changé l’ordre des composants et fait apparaître des images où les canaux rouge et bleu étaient inversés
Le correctif final : un parcours déterministe
- Après environ 10 heures de debug, un commit de correction a été créé
- Le commit de correction est
a10ad24c760bfe713f1493e49dff7da16d14bf39- Titre :
LibGfx: Make JPGLoader iterate components deterministically - Date : 31 mai 2021
- Titre :
- Le cœur du correctif était de faire en sorte que
JPGLoaderparcoure les composants dans un ordre déterministe - Changer simplement l’ordre des arguments de
Colorfaisait aussi paraître l’image normale sur le moment, mais un autre changement ultérieur de l’ordre d’itération aurait pu la casser à nouveau - Ce qui ressemblait à une petite erreur d’affichage s’est révélé être un cas où une dépendance incorrecte à l’ordre d’itération d’un conteneur et un changement de taille d’allocation se sont combinés pour exposer un bug
1 commentaires
Commentaires Hacker News
C’est l’une des raisons pour lesquelles beaucoup d’implémentations de tables de hachage introduisent un élément aléatoire dans l’algorithme
Comme l’ordre des éléments change à chaque exécution, si l’on dépend de cet ordre par erreur, le problème apparaît rapidement
Si l’algorithme de hachage est fixe, on peut fabriquer des clés qui se retrouvent dans le même bucket et les exploiter pour une attaque par déni de service ; ce mécanisme aide aussi assez bien à prévenir ce type de problème de sécurité
Je préfère cette approche, parce qu’elle évite de devoir décider à chaque fois si l’on a besoin d’une map triée ou non triée
Il m’est arrivé plusieurs fois de penser qu’une map non triée suffisait, pour découvrir que c’était faux pour des raisons subtiles
Sinon, c’est vraiment une mauvaise idée, car cela rend le débogage d’autres problèmes beaucoup plus difficile
Le hasard n’est pas un ami, c’est un ennemi
Il y a une vingtaine d’années, une attaque contre les serveurs web Java consistait à manipuler les paramètres d’URL pour qu’ils tombent tous dans le même bucket, ce qui provoquait une grosse attaque par déni de service
Si ma mémoire est bonne, les serveurs web PHP ont connu exactement le même problème de sécurité
Cela a été corrigé en ajoutant une graine à la table de hachage, et cette graine était évidemment contrôlable par le développeur. Parce que le hasard n’est pas un ami, c’est un ennemi
Cela ressemble à un cas où un peu plus de débogage aurait fait gagner du temps, plutôt que de faire à l’aveugle une recherche dichotomique par bisect
Les logs affichant l’ordre des composants devaient de toute façon finir par être ajoutés
Le débogage était bon, mais le message de commit est lui aussi excellent
Il condense très bien la cause et le correctif en quelques paragraphes
Si l’on attend assez longtemps, C++ finira aussi par avoir une fonctionnalité équivalente à
malloc_good_sizehttps://github.com/cplusplus/papers/issues/18
Le titre a besoin de [2021]
Ce n’est pas la faute de Gunnar. Le problème vient de ceux qui ont stocké des données ordonnées dans un fichier de hachage
En plusieurs décennies dans ce métier, j’ai vu plusieurs fois des situations où un changement dans l’agencement mémoire révélait un bug caché
À chaque fois, le débogage prend de quelques heures à plusieurs jours
Si la programmation n’était pas difficile, on n’aurait pas besoin de nous. Je ne sais simplement pas combien de temps cette phrase tiendra encore à l’ère des grands modèles de langage
Gunnar a amélioré quelque chose, et cela n’a fait que révéler un problème dans un vieux code cassé
Mais en récompense de cet effort, il se retrouve avec une phrase comme « Gunnar, I like you, but please don't make me go through this again. :^) »
Il me semble que SerenityOS dispose de ressources de test ou de personnes qui s’entraident avec des PC
Avoir compilé SerenityOS 4 ou 5 fois depuis zéro sur un portable Sandy Bridge Mobile de 2011, c’est un peu comme essayer de faire du développement Windows Vista sur un ordinateur sorti entre Windows 3.1 et Windows 95
Depuis 2011, les CPU n’ont pas évolué de façon aussi spectaculaire, alors qu’entre Windows 3.1 et Vista, le x64 s’est démocratisé et les CPU multicœurs sont devenus courants
Vista est sorti à l’international début 2007 ; un CPU vieux de 13 ans au moment de cette sortie daterait donc de 1994, environ un an après l’arrivée du Pentium original
À l’époque, beaucoup de gens utilisaient encore un bon vieux 486 DX2-66 fiable
Le fait qu’un CPU vieux de 13 ans puisse encore servir aujourd’hui sur un projet moderne est assez impressionnant. À l’époque, on aurait difficilement pu en dire autant
J’espère que les CPU qui sortent aujourd’hui resteront utilisables de façon satisfaisante au-delà de 2037
Visual Studio tourne bien, et Photoshop aussi, à part les outils d’IA intégrés au système qui sont juste un tout petit peu lents
J’ai probablement environ 200 onglets Chrome ouverts, avec Slack, WhatsApp et trois navigateurs de test en plus
Pour le montage 4K, j’aimerais que CapCut soit un peu plus rapide, mais il tient très bien sur des projets 2K complexes
Il ne commence à atteindre ses limites que sur des projets After Effects complexes. Ça, il n’aime pas
Il faudra que je fasse une mise à niveau, mais pour une machine récupérée quasiment à la poubelle, c’est plutôt pas mal
En voyant « Alien Lenna », j’ai eu une impression de déjà-vu ; effectivement, c’était un article que j’avais déjà lu et même commenté
https://news.ycombinator.com/item?id=27374942 (2021)