David Madore's WebLog: Je résous le bug le plus bizarre que j'aie jamais vu

[Index of all entries / Index de toutes les entréesLatest entries / Dernières entréesXML (RSS 1.0) • Recent comments / Commentaires récents]

↓Entry #2213 [older| permalink|newer] / ↓Entrée #2213 [précédente| permalien|suivante] ↓

(mercredi)

Je résous le bug le plus bizarre que j'aie jamais vu

Commençons par raconter un peu ma vie. Vendredi j'ai enfin, et avec immense soulagement, soumis pour publication un article sur lequel mon co-auteur et moi travaillions depuis maintenant environ un an et demi et qui fait finalement 80 pages (si j'ai le courage, j'essaierai de vulgariser un peu le contenu dans ce blog, mais en fait je crois que j'ai surtout envie de penser à autre chose jusqu'à ce que le relecteur nous envoie des zillions de corrections). Ce week-end je suis allé chez mes parents pour me détendre un peu avant d'attaquer la montagne de choses que j'ai mises de côté entre autres pour avoir le temps de finir cet article. Bref.

Les choses ne se sont pas tout à fait passées de façon aussi détendante(?) que je l'avais espéré. D'abord il a plu toute la journée comme les larmes d'une veuve, ce qui n'aide jamais. Ensuite, il y a eu l'affaire mystérieuse d'un dongle wifi qui a disparu corps et âme et au sujet duquel Hercule Poirot enquête sans doute encore (j'avoue que j'avais déjà eu plein de problèmes avec le wifi, mais le coup de l'antenne qui disparaît physiquement, je n'avais pas encore vu). Mais surtout, il y a eu un bug de Firefox, qui semblait déjà étrange au début et qui est finalement devenu le bug le plus bizarre que j'aie jamais rencontré.

Il faut savoir que j'ai un PC chez mes parents (que j'utilise principalement pour faire des backups) dont la config est presque exactement identique à celle de mon PC chez moi — justement pour tenter d'éviter les surprises de choses qui fonctionneraient différemment d'un côté et de l'autre. La principale différence est que celui chez mes parents a un système (GNU/Linux) 32-bits alors que celui chez moi est 64-bits (et encore le noyau est-il 64-bits des deux côtés). Mais à part ça, j'essaie d'installer presque toujours les mêmes packages, de faire les mises à jour en même temps, etc., et mon $HOME est synchronisé entre les deux machines (justement pour servir de sauvegarde). Je compile aussi des Firefox de la même manière sur les deux machines (juste l'un en 32-bits et l'autre en 64-bits), à partir des mêmes sources, et je m'attends donc à ce qu'ils fonctionnent de la même manière.

Et là voilà, j'arrive chez mes parents, je lance mon Firefox, je veux faire une recherche Google (à cause d'un petit problème sur mon mobile, une merdouille qui ne vaut même pas la peine d'être racontée), et je tombe sur un bug bizarre : la barre d'adresse ne marche pas. Plus exactement, je peux taper des choses dedans, il propose des complétions, mais la touche entrée ne fait rien, pas plus que la petite flèche censée aller à l'adresse indiquée, ou même le fait de cliquer sur une complétion — bref, la barre est essentiellement inactive. À part ça, tout semble marcher normalement : la touche entrée marche parfaitement ailleurs dans le navigateur, je peux cliquer sur des liens dans des pages Web, bref, il n'y a en gros que la barre d'adresse qui dysfonctionne.

Évidemment, c'est le genre de symptôme qui peut avoir un million de causes, rien de ce que je peux trouver en ligne n'est susceptible de me concerner. Autant dire que le docteur House voit son patient commencer à faire de la fièvre, il est loin de se douter jusqu'où le lapin blanc va le mener.

Commence donc mon enquête pour essayer de comprendre ce qui se passe. Est-ce peut-être mon profil Firefox qui est corrompu ou un problème avec une extension ou un plugin que j'aurais ? Non, le bug se reproduit encore avec un profil vierge. Mais de façon non complètement reproductible : parfois il cesse d'exister sur un profil donné, et il ne réapparaît alors pas, alors que sur mon profil normal, il persiste. J'explore une forêt de fausses pistes. Je vérifie que le problème se produit encore en lançant Firefox sur un affichage distant (en export DISPLAY, comme on dit en jargon Unix). Je constate un message d'erreur suspect dans la console du navigateur ayant à voir avec l'initialisation des moteurs de recherche (NS_ERROR_FAILURE: Failure'Failure' when calling method: [nsIBrowserSearchService::currentEngine] browser.js:11478) — mais pourquoi celle-ci échouerait-elle ?

Ma première hypothèse porte sur la compilation : on parle d'un Firefox que j'ai compilé moi-même, ce que je fais régulièrement, mais lorsque j'ai compilé celui-là, il y a eu une coupure de courant qui a éteint la machine ; j'ai bien sûr vérifié les systèmes de fichiers, effacé le sous-arbre de compilation et redémarré la compilation à zéro, mais il est possible que le plantage ait eu lieu au moment où la compilation générait un script Python qui sert par la suite, il est possible que celui-ci ait été corrompu par le plantage (typiquement, remplacé par des zéros, il semble que ça arrive sous le système de fichiers xfs), qu'il soit situé dans l'arbre de compilation « permanent » (avec les sources, plutôt que dans l'arbre des résultats de compilation que j'ai effacé pour recommencer à zéro : Firefox fait ça quand il s'agit de scripts qui servent à la compilation elle-même), qu'il soit utilisé pour générer un fichier décrivant les moteurs de recherche et qu'à cause de ça ceux-ci ne s'initialisent pas bien. L'hypothèse était raisonnable (je la mentionne pour montrer le genre de fausses pistes que j'ai pu explorer), mais j'ai vite vérifié qu'elle était fausse : un Firefox officiel compilé par Mozilla présente les mêmes symptômes (pour toute version entre la 30 « stable » et la 33 « nightly »).

Je me convaincs que le symptôme initial (la barre d'adresse qui ne marche pas) est bien lié à l'erreur signalée sur l'échec d'initialisation des moteurs de recherche, mais ça ne m'apprend toujours pas sa cause. Et surtout, pourquoi suis-je le seul à rencontrer ce problème ? (on se doute bien que, même en se limitant aux utilisateurs GNU/Linux avec un système 32-bits, l'ensemble des versions de Firefox entre 30 et 33 est pas mal testé : s'il y avait un bug qui le rendait quasi inutilisable, ça se saurait assez vite).

Dimanche soir, j'ai soumis un bug-report à Bugzilla (#1034984, mais n'allez pas voir tout de suite si vous ne voulez pas vous faire spoiler la fin de l'histoire !). Heureusement, ils sont assez réactifs et m'ont appris l'existence d'une préférence browser.search.log qui permet d'afficher des informations de debug sur ce que fait le mécanisme de recherche. Et là, je découvre qu'il essaye d'ouvrir un fichier /usr/local/opt/firefox-30.0.MOZ/browser/searchplugins/ing.xml qui n'existe pas : ce qui existe, c'est un fichier /usr/local/opt/firefox-30.0.MOZ/browser/searchplugins/bing.xml (décrivant à Firefox le fonctionnement du moteur de recherche Bing). Voici qui fait remonter la cause un peu plus loin dans le trou du lapin blanc, mais le mystère n'en est que plus épais : pourquoi diable mon Firefox aurait-il l'idée de chercher à ouvrir un fichier ing.xml quand il devrait aller prendre bing.xml, et toujours, pourquoi serais-je le seul à rencontrer ce problème ?

En allant fouiller un peu dans le code source du mécanisme de gestion des moteurs de recherche, je comprends un petit peu mieux pourquoi le bug n'était pas déterministe (il y a deux mécanismes d'initialisation, un « synchrone » et un « asynchrone », il est probable que l'utilisation de l'un ou de l'autre ne soit pas déterministe, et c'est l'« asynchrone » qui échoue). La cause est un peu mieux cernée : c'est l'objet OS.File.DirectoryIterator qui renvoie des noms tronqués d'un caractère (du coup, ça donne ce ing.xml, qui n'existe pas et que Firefox n'arrive pas à ouvrir). Je constate que c'est systématique — quel que soit le répertoire que je lui fais lister, il manque toujours le premier caractère des noms de fichier. Mais la raison profonde n'est pas plus claire pour un sou, ni pourquoi je suis le seul concerné.

J'ai passé un temps fou à lancer Firefox dans un nombre incroyable de combinaisons ordinateur×configuration, dans des machines virtuelles ou sur des vrais ordinateurs, et à m'arracher les cheveux : j'ai beau tenter d'utiliser une config absolument identique dans mes machines virtuelles (jusqu'au noyau 64-bits pour un userland 32-bits ; et en essayant de reprendre exactement les versions de toutes les bibliothèques listées dans le /proc/$PID/maps du Firefox en cours d'exécution), impossible de reproduire le bug. Il n'y a apparemment que sur mon PC chez mes parents qu'il se produit !

Une nouvelle option de debug (toolkit.osfile.log) me montre que le bug vient de bien profondément dans Firefox : les threads de travail voient bien passer des noms tronqués d'un caractère. J'en ai appris beaucoup sur la façon de débugguer du JavaScript (et comment débugguer le JavaScript de Firefox lui-même… ce qui n'est malheureusement pas possible dans les threads de travail), et sur la façon dont s'organise le source de ce machin (à ce stade-là, les bouts incriminés étaient principalement ici et ), mais toujours sans comprendre quelle pouvait être la cause profonde du problème, ou de sa rareté.

Et puis, en regardant un commentaire dans le source,

       // Structure |dirent|
       // Building this type is rather complicated, as its layout varies between
       // variants of Unix. For this reason, we rely on a number of constants
       // (computed in C from the C data structures) that give us the layout.
       // The structure we compute looks like
       //  { int8_t[...] before_d_type; // ignored content
       //    int8_t      d_type       ;
       //    int8_t[...] before_d_name; // ignored content
       //    char[...]   d_name;
       //    };

— j'ai eu une illumination (si le struct dirent n'est pas bien interprété, c'est qu'il y a une différence d'ABI quelque part, or quelle pourrait bien être la raison de cette différence d'ABI ?), et surtout, je me suis rendu compte que j'aurais dû bondir plus tôt en consultant la liste des bibliothèques chargées en mémoire par Firefox (/proc/$PID/maps) et en voyant /lib/libc.so.5.4.46 y apparaître.

Le problème, c'est la libc5. Ceux qui connaissent bien Linux auront compris de quoi je veux parler, mais pour les autres, une explication s'impose : la libc, ou bibliothèque C du système, c'est la bibliothèque qui contient les fonctions standard du langage C, et qui sert, de fait, sur un système Unix, d'interface entre le noyau et les programmes utilisateurs (même s'ils ne sont pas écrits en C, dans essentiellement tous les cas, ils s'appuient sur la bibliothèque C). La bibliothèque C de GNU/Linux a une histoire un peu compliquée, mais disons qu'entre environ 1995 et 1999, la version standard de la bibliothèque C de GNU/Linux était appelée libc5, et qu'entre 1997 et 2000 (ça a pris du temps !) s'est effectué une migration vers une nouvelle version, profondément incompatible, appelée libc6 — c'est celle qui est encore utilisée maintenant (il y a eu beaucoup d'évolutions depuis, mais aucune ne marquant un changement incompatible). La libc5 a continué à être un peu maintenue jusque vers 2003, et depuis 2003 on peut la considérer comme complètement obsolète (et elle n'existe carrément pas sur les systèmes 64-bits : il n'y a que sur les 32-bits qu'il y a même la possibilité d'avoir une libc5). Comme je suis du genre conservateur (pour ne pas dire dinosaure), j'ai encore une libc5 fonctionnelle sur mes systèmes GNU/Linux 32-bits, ce qui n'est évidemment pas le cas de tous les gens qui installent des Ubuntu ou autres distributions récentes (et a fortiori tous les systèmes 64-bits).

Et le bug, c'est que l'interface JavaScript de Firefox servant à charger directement des fonctions de la libc (qui n'est elle-même pas énormément utilisée, en fait : il y a toutes sortes d'autres mécanismes d'appel possibles) va chercher assez stupidement n'importe quelle bibliothèque qui s'appelle libc qu'elle trouve, et dans mon cas elle trouve et charge la libc5. Assez inexplicablement, ça ne plante pas tout le navigateur de se retrouver avec deux versions incompatibles de la libc (c'est bien sûr la version moderne, la libc6, qui sert pour presque tout, et avec laquelle Firefox a été compilé), mais dans l'appel à readdir exécuté depuis ce code JavaScript-là, c'est la version de la libc5 qui est utilisée. Or l'interface d'appel diffère de celle de la libc6 (prévue à la compilation) en ce que, dans la structure qui sert à retourner le résultat, l'emplacement du nom de fichier a été décalé d'un octet (j'imagine pour des raisons d'alignement) : du coup, tout était lu de travers par juste un octet.

Et vérification faite, si je retire le fichier /lib/libc.so.5.4.46 de mon système, mon Firefox se remet à marcher normalement.

[Une peluche de Mozilla]Tout de même, la chaîne de causalité est assez impressionnante : la présence de la libc5 et Firefox décidant stupidement de l'ouvrir fait qu'une certaine interface JavaScript vers la libc ne fonctionne pas tout à fait correctement, ce qui a comme conséquence que les noms de fichier sont tronqués d'un caractère, et le seul effet détectable est que le système de moteurs de recherche essaie d'ouvrir ing.xml au lieu du bing.xml qui se trouve dans le répertoire, échoue, et du coup la barre d'adresse ne fonctionne plus ! Et seul quelqu'un ayant encore une libc5 installée sur un système 32-bits pouvait détecter le problème (et encore, il y a peut-être des conditions supplémentaires pour que Firefox décide de la lire, et encore d'autres pour que l'initialisation des moteurs de recherche passe par le chemin asynchrone). Voilà le genre de bugs complètement bizarres et obscurs qui aurait pu rester indétecté pendant longtemps !

Il me semble que j'ai mérité une médaille (en chocolat).

↑Entry #2213 [older| permalink|newer] / ↑Entrée #2213 [précédente| permalien|suivante] ↑

[Index of all entries / Index de toutes les entréesLatest entries / Dernières entréesXML (RSS 1.0) • Recent comments / Commentaires récents]