2 points par GN⁺ 2024-03-27 | 1 commentaires | Partager sur WhatsApp
  • Le firmware du switch réseau d’Oxide ne démarrait plus après un test de modification du séquencement d’alimentation ; la cause était un bug où la vérification du prêt de mémoire IPC du noyau Hubris entrait en conflit avec une nouvelle méthode de disposition mémoire
  • Hubris est un système d’exploitation embarqué qui isole les tâches via le MPU ; lors d’un IPC, quand une tâche prête de la mémoire à une autre, le noyau vérifie que cette mémoire se trouve bien dans une zone réellement accessible
  • Le packing de tâches récemment introduit a permis de récupérer 30 % de RAM dans certaines images de firmware, mais l’ancienne vérification échouait car elle supposait que la mémoire prêtée se trouvait dans une seule région MPU
  • La tâche sequencer est morte avec un synthetic memory fault en tentant de prêter au pilote I2C une mémoire contenant l’adresse 0x801bffd, et humility tasks indiquait 115 redémarrages ainsi que l’état mem fault... in syscall
  • Le correctif a consisté à modifier l’algorithme de vérification pour autoriser les prêts traversant plusieurs régions MPU adjacentes ; il a fallu environ 3 heures entre la découverte de la panne et la correction du bug noyau

Un switch réseau qui ne démarre plus

  • Arjen Roodselaar, chez Oxide, testait des modifications du séquencement d’alimentation et de la configuration d’horloge dans le firmware du switch réseau lorsqu’après un changement apparemment mineur, il a rencontré un problème : le switch ne s’allumait plus
  • Une partie du firmware répondait aux requêtes, mais la partie critique chargée du séquenceur d’alimentation semblait bloquée
  • Une erreur de séquencement d’alimentation pouvant endommager réellement le matériel, il fallait d’abord déterminer si le switch était mort ou s’il ne faisait que ne pas répondre

Hubris et la mémoire limitée

  • Hubris est un système d’exploitation destiné aux systèmes profondément embarqués, comme les contrôleurs internes de claviers, et a été créé pour gérer les opérations nécessaires au démarrage des grands processeurs de l’Oxide Rack
  • Un firmware basé sur Hubris est composé de plusieurs programmes compilés séparément, appelés tâches (tasks)
    • Chaque tâche embarque son propre code de bibliothèque standard nécessaire, entre autres
    • Les tâches sont isolées par le MPU matériel afin qu’elles ne puissent pas se faire crasher mutuellement ni corrompre leur mémoire
  • Sur la famille ARM Cortex-M ARMv7-M principalement utilisée, les régions de mémoire protégées doivent avoir une taille égale à une puissance de deux et être alignées sur cette taille
    • Par exemple, si une région de 1024 octets a besoin d’un octet supplémentaire, elle ne devient pas une région de 1025 octets, mais une région de 2048 octets

Les nouvelles frontières créées par le packing de tâches

  • Les premières versions de Hubris utilisaient une approche simple : une région pour la RAM d’une tâche et une région pour sa flash ; cela créait toutefois des espaces vides inutilisables entre les tâches et gaspillait de la mémoire
  • Matt Keeter a amélioré le système de build pour qu’il place les tâches, quand c’est possible, en combinant plusieurs régions de taille puissance de deux
    • Le matériel n’autorise au maximum que 8 régions par tâche
    • Dans certaines images de firmware, cela a permis de récupérer 30 % de RAM
    • Les plus petits appareils, auparavant tellement contraints qu’ils nécessitaient une optimisation à chaque fois, ont retrouvé de la marge
  • Avec ce changement, des frontières de régions MPU imprévisibles peuvent désormais apparaître au milieu de la flash et de la RAM d’une tâche

Les indices laissés par humility tasks

  • Arjen a examiné le switch défaillant avec Humility, le débogueur de Hubris ; le processeur de service chargé du séquencement d’alimentation était vivant et en cours d’exécution, ce qui rendait un problème matériel moins probable
  • La sortie de humility tasks indiquait l’état suivant pour la tâche sequencer
mem fault (precise: 0x801bffd) in syscall (was: wait: reply from i2c_driver/gen0)
  • Cette même tâche avait été redémarrée 115 fois, et dans Hubris, une tâche est presque toujours redémarrée en réaction à un crash
  • La chaîne d’état signifie :
    • mem fault : violation des règles de gestion mémoire
    • precise: 0x801bffd : l’adresse précise en cause est connue
    • in syscall : la tâche n’était pas en train de s’exécuter, elle était dans un appel système
    • was: wait: reply from i2c_driver/gen0 : elle attendait la réponse d’un message envoyé au pilote I2C
  • gen0 signifie que i2c_driver n’avait encore jamais crashé, tandis que sequencer était à la génération 115

IPC Hubris et prêt de mémoire

  • Les tâches Hubris communiquent par messages IPC, qui se comportent comme des appels de fonction
    • La tâche qui envoie le message s’arrête
    • La tâche réceptrice prend le contrôle du CPU
    • Lorsque le résultat revient, la tâche émettrice se réveille
  • L’IPC est conçu pour bien s’aligner avec le modèle de propriété de Rust : une tâche peut prêter une partie de sa propre mémoire à une autre tâche avec un message IPC
  • Une tâche qui interagit avec un périphérique I2C prête sa plage mémoire au pilote de bus I2C, et le pilote lit ou écrit directement dans cette plage
    • Cela réduit le besoin pour le pilote de bus de disposer d’un pool de buffers séparé
    • Cela réduit le nombre de copies de données
  • Mal implémenté, ce mécanisme pourrait devenir une faille de sécurité ; le noyau Hubris interdit donc à une tâche de prêter de la mémoire qu’elle ne possède pas réellement ou à laquelle elle ne peut pas accéder
    • Le serveur reçoit un code d’erreur
    • Le client reçoit un fault et est toujours terminé
    • C’est traité comme une violation d’accès signalant un bug, une corruption ou une possibilité d’exploit

Synthetic fault et cause réelle

  • Hubris distingue les faults en real faults et synthetic faults
    • Un real fault correspond à une violation de règles matérielles, comme le déréférencement d’un pointeur null ou l’écriture dans une zone de code
    • Un synthetic fault correspond à une violation de règles logicielles ajoutées par Hubris, comme l’IPC ou le prêt de mémoire
  • Le fault de sequencer était un synthetic fault survenu pendant le prêt de mémoire au pilote I2C via IPC
  • L’adresse en cause, 0x801bffd, était une adresse flash valide, mais elle présentait un motif étrange : 3 octets avant une frontière de puissance de deux
  • La sortie de humility mem montrait que deux régions flash appartenant à la tâche sequencer se touchaient à 0x801c000
LOW         HIGH           SIZE ATTR   ID TASK
0x08018000 - 0x0801bfff   16kiB r-x--- 17 sequencer
0x0801c000 - 0x0801dfff    8kiB r-x--- 17 sequencer
  • Comme les deux régions appartenaient à la même tâche, le MPU matériel pouvait autoriser l’accès sans problème lors de l’exécution normale du programme, mais la vérification de prêt de mémoire IPC du noyau reposait sur une autre hypothèse

Quand une vieille simplification devient un bug

  • L’ancienne vérification du noyau se contentait de vérifier que toute la slice mémoire à prêter tenait entièrement dans une seule région de la tâche
self.region_table().iter().any(|region| {
    region.covers(slice)
        && region.attributes.contains(desired)
        && !region.attributes.intersects(forbidden)
})
  • Ce code correspondait à la conception de l’époque où il existait une seule région RAM et une seule région flash par tâche
  • Avec l’introduction du packing de tâches, la mémoire d’une même tâche pouvait être divisée en plusieurs régions MPU adjacentes, rendant l’ancienne hypothèse invalide
  • Les accès mémoire ordinaires n’étaient pas affectés, car ils sont vérifiés directement par le MPU matériel ; le problème n’apparaissait que lorsqu’on tentait de prêter cette mémoire via IPC

Une panne créée par la combinaison de deux fonctionnalités

  • Le packing de tâches fonctionne de manière opportuniste
    • Il existe une limite de 8 régions maximum par tâche
    • Les tâches de pilotes matériels utilisent déjà certaines régions à cause des registres memory-mapped
    • Un placement plus intelligent n’est tenté que lorsqu’il reste des slots de régions disponibles
  • Par conséquent, les frontières de régions apparaissent à des emplacements difficiles à prévoir pour l’auteur d’une tâche
  • Une petite variation de taille dans la tâche A peut modifier l’emplacement d’une frontière de région MPU dans une tâche B sans rapport
  • Le simple ajout de code de debug pouvait changer les décisions de placement et les frontières de régions, au point de faire disparaître le crash
  • Matt a immédiatement désactivé le packing de tâches dans le système de build pour permettre à Arjen de produire une image de firmware fonctionnelle, tandis que l’analyse et la correction du bug noyau se poursuivaient

La correction du noyau

  • Le cœur du correctif consistait à modifier l’algorithme de vérification des accès mémoire pour autoriser la mémoire à prêter même lorsqu’elle traverse précisément plusieurs régions MPU adjacentes
  • Le nouvel algorithme a été conçu pour ne parcourir la table des régions qu’une seule fois
    • Hubris cherche à éviter d’exposer des opérations dont la complexité temporelle peut être contrôlée par une tâche
    • Les performances ne devaient dépendre que de la table des régions, de taille fixe, et non de la taille de la mémoire prêtée
    • La table des régions a une taille fixe de 8 entrées
  • Pour cela, le système de build a été modifié afin de trier les régions d’une tâche par adresse croissante
regions.sort_by_key(|i| region_table.get_index(*i).unwrap().1.base);
  • Le commit de correction fait en sorte que le noyau exploite cette propriété de tri pour effectuer des vérifications d’accès moins coûteuses
  • Le code devenu plus complexe a été extrait du cœur du noyau Hubris et déplacé dans un crate plus portable, avec l’ajout de tests unitaires pour les corner cases importants
  • Le nouveau code a permis de réactiver le packing de tâches sans laisser aux développeurs de tâches des crashs difficiles à prévoir

Pourquoi la panne ne s’est pas propagée davantage

  • Tout a commencé avec un switch réseau qui ne démarrait pas, et a abouti à une correction de bug noyau en environ 3 heures
  • Grâce à la fault isolation, seule la tâche sequencer mourait en boucle parmi les 23 tâches isolées composant le firmware du switch, tandis qu’une grande partie des autres composants continuaient de fonctionner
    • Le système de mise à jour du firmware
    • La pile réseau IP pour les interfaces de gestion et de contrôle
    • Plusieurs services réseau, de l’implémentation du protocole echo jusqu’aux interfaces du plan de contrôle du rack
    • I2C, SMBus et PMBus pour les capteurs, les ventilateurs et la surveillance d’autres états système
    • Les pilotes des 32 transceivers QSFP 100G en façade
  • L’IPC Hubris est conçu en partant du principe que d’autres tâches peuvent échouer ; les opérations marquées comme idempotentes peuvent donc être retentées de manière transparente
  • Le bug existant de vérification d’accès mémoire empêchait des accès corrects d’un programme correct, mais n’autorisait pas d’accès incorrects ou malveillants ; il n’avait donc pas d’impact de sécurité
  • sequencer est mort au moment où lui et le pilote I2C partageaient effectivement de la mémoire, mais le pilote I2C a continué de fonctionner sans risque de corruption

Infrastructure de debug et fonctionnement de l’équipe

  • Humility est un débogueur qui a évolué avec le noyau Hubris ; en quelques minutes, Arjen a pu identifier l’emplacement du code ayant crashé jusqu’au numéro de ligne et partager un snapshot indépendant du processeur de service
  • Hubris enregistre en RAM des coredumps compressés des tâches qui crashent, récupérables via le réseau
    • Il est possible d’obtenir un crash dump même sans stockage persistant inscriptible
    • La fonctionnalité de crash dump réside dans une tâche séparée, pas dans le noyau
  • Ces processeurs ne traitent pas les données des workloads clients, mais uniquement du trafic de gestion système, et les rapports de crash ne sont pas téléversés automatiquement
  • La partie indépendante de l’architecture du noyau Hubris représente 1 789 lignes de code et 1 192 lignes de commentaires ; la prise en charge d’ARMv6-M, ARMv7-M et ARMv8-M ajoute 1 075 lignes de code et 534 lignes de commentaires
  • Les concepts du noyau et l’IPC étant simples, lorsqu’un fault pointait vers l’IPC, il n’y avait pas beaucoup d’endroits à vérifier

1 commentaires

 
GN⁺ 2024-03-27
Commentaires sur Hacker News
  • Hubris est vraiment excellent. J’ai lu le code du noyau pendant une trentaine de minutes, et c’est très loin du code C bourré de macros ifdef, avec une préférence pour les noms de variables à deux lettres et peu de commentaires que j’avais pu voir par le passé : c’est écrit de façon très claire
    C’est aussi une bonne lecture avant de dormir, et je recommande d’y jeter un œil : https://github.com/oxidecomputer/hubris/blob/b44e677fb39cde8...

    • Une bonne partie de la culture C semble se résumer à « avoir la flemme d’apprendre à taper à une vitesse correcte », et je trouve ça assez agaçant
      L’espace disque occupé par le code source n’est plus vraiment un problème depuis 40 ans, mais on continue à être radin sur les noms de variables
    • L’IA pourrait faire disparaître cette convention. Si on donne du vieux code C rugueux à une IA, d’un coup toutes les variables peuvent devenir propres et être nommées comme l’utilisateur les aime
      Parce que l’IA aura appris précisément les goûts et les habitudes d’un codeur donné
  • Bon article, mais l’emplacement du commentaire ci-dessous me déçoit un peu
    Le commentaire au-dessus de regions.sort_by_key(|i| region_table.get_index(*i).unwrap().1.base);, qui dit qu’il faut trier par adresse croissante et que le noyau s’appuie sur cette propriété pour rendre les vérifications d’accès peu coûteuses, relève moins des détails de cette fonction que d’un invariant de champ que tous les auteurs doivent respecter et que tous les lecteurs peuvent exploiter
    Il me semblerait donc plus approprié de le mettre dans la chaîne de documentation de TaskDesc::regions : https://github.com/oxidecomputer/hubris/commit/b44e677fb39cd...

    • Cela dit, c’est bien d’avoir le commentaire à côté du code de tri. Sinon, ce tri lui-même pourrait sembler assez surprenant
      La meilleure approche serait probablement de créer une méthode constructeur sur TaskDesc qui trie les régions, afin de faire respecter l’invariant. On voit que le code se complexifie avec le temps ; il semble donc désormais utile de passer un peu de temps à encapsuler cette complexité dans des méthodes
  • C’est l’une des meilleures offres d’emploi que j’aie vues jusqu’ici. J’aime la manière dont le texte glisse naturellement vers la culture d’équipe, puis ajoute à la fin « au fait, on recrute »
    C’est une analyse post-mortem vraiment excellente, et même moi, développeur côté couche applicative, j’ai pu suivre. En plus, je lis justement Rust in Action en ce moment, ce qui me préparait un peu mieux à ce genre de contenu
    C’est toujours agréable de voir quelqu’un qui commente beaucoup son code. La programmation lettrée, ça marche

    • Dommage que cela ne concerne que les États-Unis
  • Il semble que l’épisode précédent soit disponible ici

    1. https://hachyderm.io/@mjk/112157472314396711
    2. https://www.mattkeeter.com/blog/2024-03-25-packing/
  • La partie sur « l’intégration étroite et non hiérarchique de l’équipe » m’a marqué. Ce n’est pas une fonctionnalité de Hubris en soi, mais l’explication selon laquelle il est difficile de séparer Hubris de l’équipe qui l’a créé, et qu’il n’existe pratiquement pas de silos internes dans l’équipe d’ingénierie d’Oxide, est frappante
    J’aimerais en savoir plus sur les raisons qui les ont poussés à créer une culture encourageant l’ouverture, la curiosité et la communication, tout en décourageant les réflexes défensifs, les constructions d’empires et le gatekeeping, ainsi que sur la façon concrète dont ils l’ont mise en place. Je me demande aussi s’il n’y a pas des inconvénients à faire grandir ce type de culture dans une organisation
    Certains choisissent des systèmes hiérarchiques plus stricts, et l’organigramme peut devoir être défini de manière stratégique ; les compromis ne sont donc pas évidents pour moi

    • Il est difficile d’évaluer les valeurs affichées elles-mêmes, mais en général, l’inconvénient des organisations dépourvues de structure fortement définie, c’est qu’une forme de structure de pouvoir apparaît malgré tout
      Si cette structure n’est pas explicite, elle est moins visible publiquement, elle n’est pas choisie intentionnellement et elle est plus difficile à comprendre, en particulier pour les personnes qui ne sont pas à l’aise avec les interactions sociales. À cause de son caractère fantôme, elle peut donc permettre des comportements plus pathologiques et, même sans aller jusque-là, rendre la coordination beaucoup plus difficile
      J’ai vécu ce genre de situation dans plusieurs entreprises. Dans un grand cabinet de conseil, il existait bien une structure de pouvoir officielle, mais elle n’était pas vraiment respectée en pratique ; obtenir une place sur un projet passait moins par des canaux officiels que par le fait de se rapprocher des commerciaux et des managers. Si l’on arrivait à construire le réseau social nécessaire, c’était très bien, mais sinon, ça fonctionnait mal
      Un exemple similaire est « The Tyranny of Structurelessness », une conférence d’une féministe qui avait observé le même phénomène dans des organisations rejetant la hiérarchie parce qu’elles la voyaient comme patriarcale ; il existe aussi des discussions similaires au sujet de Valve, dont la structure interne n’est pas claire. Les projets open source peuvent rencontrer le même problème, et je pense qu’une partie des conflits autour de Rust vient de problématiques semblables
      Cela ne veut pas dire qu’une structure de pouvoir explicite doit forcément être hiérarchique. Les organisations professionnelles traditionnelles sont hiérarchiques, mais la structure d’Oxide pourrait être explicite tout en restant non hiérarchique. Ce genre d’approche fonctionne généralement mieux à petite échelle, et le cabinet de conseil mentionné plus haut était le plus grand exemple que je connaisse d’entreprise fonctionnant de façon assez libre, même s’il existait tout de même certains supports
      Ce n’est pas une dichotomie, mais un spectre. Même sous la structure de pouvoir la plus rigide sur le papier, il existe une structure implicite plus complexe, et c’est la nature des groupes humains
      Je ne crois pas que les structures explicites soient toujours meilleures que les structures implicites. Je ne faisais que parler des inconvénients observables dans les organisations moins explicites ; les structures de pouvoir plus explicites ont elles aussi leurs problèmes. On peut aussi faire le lien avec « seeing like a state » ou avec la question de la lisibilité
  • C’est un excellent article, qui montre en profondeur le processus de débogage d’un problème complexe. Le fait que le reste du système soit resté stable en dit long sur la qualité d’ingénierie de l’équipe d’Oxide
    Personnellement, je l’ai trouvé très inspirant, et je compte appliquer des techniques similaires dans mon travail quotidien

  • Si ce matériel était traité comme un TLB rempli par logiciel, il serait aussi possible de prendre en charge plus de 8 régions

    • Je suppose qu’ils veulent probablement (a) des performances soft real-time et (b) éviter d’introduire un élément central susceptible de nuire à la débogabilité ou à la fiabilité
      Je ne le ferais jamais sauf en dernier recours. La pagination virtuelle est sale, et je ne voudrais laisser aucun doute
    • Je sais que TLB signifie translation lookaside buffer, mais je me demande ce que veut dire « soft fill » ici
  • Ce que fait Oxide est vraiment impressionnant

    • Après Tailscale, voilà maintenant Oxide qui devient l’un de ces projets adorés dont 99 % des gens n’ont pas besoin
  • J’aime tout ce que font les gens d’Oxide, et ceci en fait partie

  • Ils ont appelé leur système d’exploitation Hubris ? Ah, ça… je n’ose même pas le dire

    • Vous serez heureux d’apprendre que le débogueur s’appelle « humility » : https://github.com/oxidecomputer/humility
    • Plus précisément, c’est Brian Cantrill qui a nommé le système d’exploitation hubris
      Quelle personne saine d’esprit écrirait un nouveau système d’exploitation aujourd’hui ? La réponse : quelqu’un qui essaie de résoudre le problème que tous les systèmes d’exploitation ignorent, à savoir celui des contrôleurs de cartes mères et de cartes d’extension, que le système d’exploitation ne contrôle pas et ne peut pas contrôler
    • Ça me paraît assez cohérent avec la marque