Que s’est-il passé ?
Firefox dispose de plusieurs serveurs et d’une infrastructure gérant plusieurs services internes. Ceux-ci incluent les mises à jour, la télémétrie, la gestion des certificats, les rapports de plantage et d’autres fonctionnalités analogues. Cette infrastructure est hébergée par différents fournisseurs de services dans le cloud qui utilisent des répartiteurs de charge afin d’équilibrer la charge entre les serveurs. Pour ces services hébergés sur Google Cloud Platform (GCP), ces répartiteurs de charge possèdent des paramètres relatifs au protocole HTTP qu’ils doivent communiquer. L’un de ces paramètres concerne la prise en charge de HTTP/3 et dispose de trois états : « Activé », « Désactivé » ou « Automatique (par défaut) ». Nos répartiteurs de charge étaient réglés sur « Automatique (par défaut) » et le 13 janvier 2022 à 07 h 28 UTC, GCP a déployé un changement imprévu pour utiliser HTTP/3 par défaut. Firefox utilisant HTTP/3 lorsqu’il est pris en charge, à partir de cet instant, certaines connexions de Firefox à l’infrastructure de services ont utilisé HTTP/3 plutôt que le protocole HTTP/2, utilisé jusqu’alors.
Peu après, nous avons remarqué un pic de plantages via notre outil de rapport sur les plantages et nous avons également reçu différents signalements, tant à l’intérieur qu’à l’extérieur de Mozilla, décrivant l’absence de réponse du navigateur.
La file des rapports de plantage à traiter a augmenté jusqu’à atteindre environ 300 000 rapports non traités.
Lors de la procédure de réponse à incident, nous avons rapidement découvert que le client plantait au sein d’une requête réseau vers un des services internes de Firefox. Toutefois, nous n’avions aucune explication sur la raison du déclenchement de ce problème à cet instant, ni sur sa portée. Nous avons continué à chercher l’élément déclencheur : un changement avait dû se produire pour que le problème commence. Nous avons déterminé que nous n’avions pas diffusé des mises à jour ou des changements de configuration qui auraient pu causer ce problème. En même temps, nous gardions en tête que HTTP/3 était activé depuis Firefox 88 et était activement utilisé par certains sites web populaires.
Bien que nous ne puissions pas le voir, nous suspections alors un genre de changement « invisible » déployé par l’un de nos fournisseurs qui aurait modifié, d’une façon ou d’une autre, le comportement des répartiteurs de charge. Après un examen approfondi, aucun de nos paramètres n’avait changé. Nous avons alors découvert, grâce aux journaux, que, pour une raison inconnue, les répartiteurs de charge pour notre service de télémétrie servaient désormais des connexions HTTP/3 alors qu’ils ne le faisaient pas auparavant. Nous avons désactivé de façon explicite HTTP/3 sur GCP à 09 h 12 UTC. Cela a débloqué nos utilisatrices et utilisateurs, mais nous n’étions pas certains de la cause principale, et sans la connaître, il nous était impossible de dire si cela aurait un impact sur d’autres connexions HTTP/3.
¹ Certains services hautement critiques, comme les mises à jour, utilisent un marqueur spécial, beConservative
, qui empêche l’utilisation de toute technologie pour leurs connexions (par exemple HTTP/3).
Un cocktail d’ingrédients spéciaux
Il est vite devenu clair que certaines circonstances particulières devaient être réunies pour que le plantage se produise. Nous avons réalisé différents tests avec plusieurs outils et services distants et nous ne sommes pas parvenus à reproduire le problème, même avec une connexion normale au serveur de recette pour la télémétrie (un serveur uniquement utilisé pour tester les déploiements, que nous avions laissé avec sa configuration originale à des fins de tests). Avec Firefox, en revanche, nous pouvions reproduire le problème avec le serveur de recette.
En déboguant, nous avons trouvé « l’ingrédient secret » qui nous manquait pour que le bug se produise. Toutes les connexions HTTP/3 passaient par Necko, notre pile réseau. Toutefois, les composants Rust qui ont besoin d’un accès direct au réseau n’utilisent pas Necko directement, ils l’appellent à travers une bibliothèque intermédiaire intitulée viaduct
.
Pour comprendre l’intérêt de cette nuance, nous devons d’abord comprendre certains aspects internes de Necko et notamment les requêtes d’upload HTTP/3. Pour de telles requêtes, les API haut niveau de Necko vérifient si l’en-tête Content-Length
est présent et, s’il ne l’est pas, l’ajoutent automatiquement. Le code HTT/3 de plus bas niveau repose ensuite sur ce traitement afin de déterminer la taille de la requête. Cela fonctionne correctement pour le contenu web et les autres requêtes de notre code.
En revanche, lorsque les requêtes passent d’abord par viaduct
, ce dernier convertira en minuscules chaque en-tête avant de le passer à Necko. Et c’est là qu’est le problème : les vérifications d’API de Necko ne sont pas sensibles à la casse tandis que le code bas niveau pour HTTP/3 est sensible à la casse. Ainsi, si du code ajoutait l’en-tête Content-Length
et passait la requête via viaduct
, celle-ci passerait les vérifications des API Necko mais le code HTTP/3 ne trouverait pas l’en-tête.
En l’occurrence, le module de télémétrie est actuellement le seul composant de Firefox sur ordinateur à base de Rust qui utilise la pile réseau et qui ajoute un en-tête Content-Length
. C’est pour cela que les personnes qui ont désactivé la télémétrie ont vu le problème résolu bien qu’il ne soit pas lié à la fonctionnalité de télémétrie même et qu’il aurait pu être déclenché par ailleurs.
Un chemin de code spécifique était nécessaire pour déclencher le problème dans l’implémentation du protocole HTTP/3.
² Il s’agit d’API internes, qui ne sont pas accessibles depuis le contenu web.
La boucle infinie
Nous avons : le changement sur les répartiteurs de charge qui est en place, un chemin de code spécifique dans un nouveau service Rust désormais actif. Il reste l’ingrédient final pour déclencher le problème auprès des utilisateurs et utilisatrices qui était plongé dans le code HTTP/3 de Necko.
Lors de la gestion d’une requête, le code cherchait l’en-tête de façon sensible à la casse et échouait lorsque l’en-tête avait été mis en minuscules par viaduct
. Sans l’en-tête, la requête était considérée comme complète par le code de Necko, le corps de la requête n’était alors pas envoyé. Toutefois, ce code ne finissait son exécution lorsqu’il n’y avait plus de contenu supplémentaire à envoyer. Cet état inattendu a provoqué une boucle infinie plutôt que de déclencher une erreur. Comme l’ensemble des requêtes réseau passent par un seul thread de socket, cette boucle bloquait toute communication réseau ultérieure, rendant Firefox sans réaction, incapable de charger du contenu web.
Les leçons apprises
Comme c’est souvent le cas, le problème était beaucoup plus complexe que ce qu’il paraissait à première vue et de nombreux facteurs contributifs ont été réunis. Certains des facteurs principaux que nous avons identifiés comprennent :
- Le déploiement imprévu de HTTP/3 par défaut par GCP. Nous travaillons activement avec eux afin d’améliorer la situation. Nous avons conscience qu’une annonce (comme c’est généralement le cas) n’aurait pas complètement réduit le risque d’incident. Elle aurait en revanche déclenché des expérimentations (par exemple avec un environnement de recette) et un déploiement plus contrôlés.
- Le paramètre « Automatique (par défaut) » de nos répartiteurs de charge, plutôt qu’un choix explicite, a permis que ce déploiement ait lieu automatiquement. Nous passons en revue toutes les configurations de nos services pour éviter des erreurs similaires à l’avenir.
- La combinaison particulière de HTTP/3 et de
viaduct
sur Firefox pour ordinateur n’était pas couverte par notre système d’intégration continue. Bien que nous ne puissions pas tester l’ensemble des combinaisons de configurations et de composants possibles, le choix de la version de HTTP est un changement plutôt majeur qui aurait dû être testé. Il en va de même de l’utilisation d’une couche réseau supplémentaire commeviaduct
. Les tests HTTP/3 actuels couvrent le comportement bas niveau et celui de Necko tel qu’ils sont utilisés pour du contenu web. Nous devrions exécuter plus de tests systèmes avec différentes versions de HTTP, cela aurait pu révéler ce problème.
Nous étudions également un plan d’action pour que le navigateur soit plus résilient face à de tels problèmes et pour que la réponse à incident soit plus rapide. Apprendre le plus possible de cet incident nous aidera à améliorer la qualité de nos produits. Nous remercions toutes les personnes qui ont envoyé leurs rapports de plantage, qui ont travaillé avec nous sur Bugzilla ou qui ont aidé les autres à contourner le problème.
À propos de Christian Holler
Christian est responsable technique de Firefox et ingénieur sécurité expérimenté à Mozilla.
Merci à Mozinet et cajuteq pour la relecture !
Comme la version originale, cette traduction est disponible sous la licence CC By-SA 3.0.