2 points par GN⁺ 2024-04-27 | 1 commentaires | Partager sur WhatsApp
  • Bun v1.1.5 ajoute bun.report, un rapporteur de crash qui transmet les informations de pile Zig/C++ via une URL d’environ 150 octets sans données personnelles, même en cas de crash ou de panic
  • Les rapporteurs de crash de l’OS et les core dumps existants sont difficiles à appliquer à un outil CLI comme Bun à cause du coût en symboles de débogage, performances, confidentialité et taille de l’exécutable
  • Cette nouvelle approche convertit les adresses brouillées par l’ASLR en adresses relatives au module, puis le serveur restaure les noms de fonctions à l’aide des symboles de débogage correspondant au commit SHA et à la plateforme
  • L’URL contient la plateforme, la sous-commande, le commit SHA, les feature flags, les adresses de pile, le type de crash et le message, les adresses de pile étant encodées de façon compacte en base64 VLQ
  • Aucun code source JavaScript/TypeScript ni variable d’environnement n’est envoyé ; l’équipe Bun ne reçoit que les informations de pile Zig/C++ et quelques métadonnées nécessaires au diagnostic

Pourquoi Bun a créé son propre rapporteur de crash

  • Bun compte plus de 2 600 issues GitHub ouvertes au moment de la rédaction, et certaines sont particulièrement difficiles à reproduire et à déboguer
  • Les services de crash reporting comme Sentry conviennent aux applications et produits SaaS, mais pour un outil CLI comme Bun, l’envoi de core dumps pose de gros problèmes de confidentialité, de performances et de taille d’exécutable
  • Bun v1.1.5 introduit un petit nouveau format pour les rapports de crash Zig et C++
    • Le rapport de crash tient dans une URL d’environ 150 octets
    • Il n’inclut aucune donnée personnelle

Les limites des rapporteurs de crash de l’OS

  • Certains systèmes d’exploitation, comme macOS, disposent d’un rapporteur de crash intégré, mais pour l’exploiter correctement il faut généralement distribuer les symboles de débogage avec l’application
  • Les symboles de débogage augmentent fortement la taille des distributions de Bun
    • Les symboles de débogage Linux pèsent environ 30MB
    • Les symboles de débogage macOS pèsent environ 9MB
    • Les fichiers Windows .pdb dépassent 250MB
  • Exemple pour l’exécutable Bun : llvm-strip le fait passer de 60M à 51M
  • Sans symboles de débogage, lorsqu’un crash survient, la stack trace ne contient souvent que ??? et des adresses, ce qui la rend peu exploitable
  • À cause de l’ASLR (Address space layout randomization), les adresses de fonctions incluent un décalage aléatoire, ce qui empêche de restaurer directement les noms de fonctions

Fonctionnement de bun.report

  • Lorsqu’un crash ou un panic survient dans Bun v1.1.5, Bun affiche un lien bun.report avec la version, la plateforme, les arguments d’exécution, l’utilisation mémoire et le message de crash
  • Quand l’utilisateur ouvre ce lien, il est redirigé vers un formulaire d’issue GitHub prérempli
  • L’URL contient une stack trace remappée et encodée
  • À partir des informations présentes dans l’URL, le serveur reconstruit les adresses de pile et les transforme en rapport de crash lisible par l’équipe Bun

Comment transformer les adresses en stack trace lisible

  • Les adresses de fonction sont des pointeurs vers l’emplacement où le code applicatif est chargé en mémoire, avec un décalage aléatoire ajouté pour des raisons de sécurité
  • L’idée de base consiste à soustraire l’adresse de base (base address) du binaire à l’adresse brute afin d’obtenir une adresse relative
  • L’implémentation réelle est plus complexe à cause des différences d’API selon les plateformes
    • Windows utilise GetModuleHandleExW avec le flag GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, puis prend le pointeur du module comme adresse de base
    • Linux parcourt les modules chargés avec dl_iterate_phdr et utilise dl_phdr_info.dlpi_addr comme adresse de base pour le module contenant l’adresse
    • macOS parcourt les modules avec _dyld_image_count et _dyld_get_image_header, puis récupère le slide ASLR via _dyld_get_image_vmaddr_slide
      • Sur macOS, l’adresse résultante conserve un offset d’image, qui vaut 0x100000000 dans le cas de Bun
      • Cet offset est supprimé pour raccourcir l’URL, mais il doit être réajouté avant le remappage avec llvm-symbolizer
  • Sur Linux et macOS, le premier module correspond au binaire principal de l’application
  • Sous Windows, on peut déterminer s’il s’agit du binaire principal en comparant le nom du module avec peb.ProcessParameters.ImagePathName
  • Bun ne télécharge ni n’analyse localement les symboles de débogage, et confie au serveur le démanglage
    • Le serveur peut mettre les symboles de débogage en cache
    • Il peut démangler une stack trace en quelques secondes
    • Il sert aussi de lien pour ouvrir simultanément une nouvelle issue GitHub

Structure de l’URL bun.report

  • L’URL bun.report encode les informations suivantes
    • Platform : un caractère représentant la plateforme. Par exemple, w pour Windows x86_64, M pour macOS aarch64
    • Subcommand : un caractère représentant une sous-commande comme bun test, bun install ou bun run
    • Commit SHA : le commit SHA de la version actuelle de Bun, utilisé ensuite pour récupérer les symboles de débogage
    • Feature Flags : des indicateurs des API et fonctionnalités utilisées avant le crash
    • Stack Trace Addresses : les adresses calculées à l’étape précédente
    • Crash Type : un caractère représentant le type de crash
    • Crash Message : un message dont le format varie selon le type de crash
  • Le numéro de version présent dans l’URL sert surtout à la lecture humaine, pas au traitement effectif
  • Avec ces seules informations, on peut déjà identifier manuellement certaines caractéristiques du crash
    • L’identifiant w permet de voir immédiatement qu’il s’agit d’un crash Windows
    • Le A2 à la fin de la chaîne permet d’identifier une faute de segmentation

Encodage VLQ pour des URL courtes

  • Les adresses de stack trace sont encodées en nombres base64 Variable Length Quantity (VLQ) afin de garder des URL courtes
  • Le VLQ permet de représenter les petits nombres avec moins de caractères tout en restant capable d’encoder de grands nombres
  • La même technique est utilisée dans les source maps JavaScript pour stocker les numéros de ligne
  • Le serveur redécode les valeurs VLQ en adresses relatives, télécharge les symboles de débogage à partir du hash de commit et de la plateforme, puis démangle les noms de fonctions avec llvm-symbolizer
  • Dans l’exemple de crash, on découvre qu’une assertion a échoué dans dirInfoCachedMaybeLog, une partie du code de résolution de modules sous Windows

Encodage des feature flags

  • L’URL encode aussi un entier 64 bits, où chaque bit correspond à l’utilisation d’une fonctionnalité précise de Bun
  • Ces flags donnent des indices sur les API et systèmes susceptibles d’avoir contribué au crash
    • Si un fichier .env est chargé automatiquement, la fonctionnalité dotenv est activée
    • Si fetch() est utilisé, la fonctionnalité fetch est activée
  • Bun suit l’utilisation des fonctionnalités dans un conteneur de variables globales, et chaque API incrémente la valeur correspondante en interne pour signaler son usage
  • Grâce à la métaprogrammation à la compilation de Zig, la liste des fonctionnalités est parcourue pour générer dynamiquement une packed struct utilisant un bit par fonctionnalité
  • Avec inline for, il est possible de parcourir cette liste à la compilation tout en effectuant le réglage effectif des bits à l’exécution
  • Lorsqu’une nouvelle fonctionnalité est ajoutée à la struct Features existante, le rapporteur de crash la prend en charge sans code répétitif supplémentaire
  • La même approche pourrait être reproduite en C ou avec des macros Rust, mais dans l’implémentation de Bun, le comptime de Zig est utilisé d’une manière plus simple et plus lisible

Différence avec les core dumps

  • Les core dumps contiennent bien plus d’informations, mais ils sont volumineux, nécessitent des symboles de débogage pour être utiles et peuvent embarquer beaucoup de données sensibles ou confidentielles
  • La nouvelle méthode de rapport de Bun évite d’envoyer du code source JavaScript/TypeScript, des variables d’environnement et d’autres informations sensibles
  • Au lieu de tout envoyer par défaut, elle ne transmet que les stack traces Zig/C++ et quelques détails susceptibles d’être utiles au diagnostic
  • Si des informations supplémentaires sont nécessaires, elles peuvent être demandées séparément à l’utilisateur
  • Par rapport à l’ancienne situation où il ne restait que des adresses non mappées, cela facilite nettement le diagnostic des crashes pour l’équipe Bun

Démo

  • Une petite webapp permettant de tester le rapporteur de crash est disponible sur bun.report
  • En ajoutant /view à la fin de n’importe quelle URL de rapport de crash, on ouvre l’écran correspondant de cette webapp

1 commentaires

 
GN⁺ 2024-04-27
Avis sur Hacker News
  • Si la raison d’utiliser cette méthode au lieu d’une trace de pile classique est d’éviter de distribuer des symboles de débogage de plusieurs Mo, on dirait qu’ils ont ignoré une meilleure option : n’inclure que les noms de fonctions dans la table de débogage
    C’est bien préférable au fait de devoir passer par un service web pour consulter une trace de pile, et ce n’est pas seulement théorique : c’est déjà implémenté dans LLVM : https://clang.llvm.org/docs/UsersManual.html#cmdoption-gline...

    • La raison principale d’utiliser cette approche plutôt qu’une trace de pile classique n’est pas la taille des symboles de débogage, mais le fait que presque personne n’a assez de patience pour publier un rapport de crash sur GitHub Issues
      Si une simple URL préremplit presque automatiquement les informations nécessaires, cela devient suffisamment facile pour que les développeurs envoient réellement des rapports de crash. La taille comptait aussi, car on voulait éviter tout inconvénient pour l’utilisateur, mais l’essentiel est de rendre tout le processus extrêmement simple
    • Dire qu’ils ont « ignoré une meilleure option » ou que c’est « évidemment mieux » paraît un peu catégorique. Ils connaissaient probablement cette possibilité
      Dans ce cas d’usage, devoir consulter la trace de pile via un service web n’est pas un gros inconvénient. C’est presque la même chose que d’obfusquer/minifier un bundle JavaScript frontend, d’envoyer les source maps à Sentry, puis de laisser Sentry reconstituer les traces de pile venant du navigateur de l’utilisateur. L’utilisateur n’allait de toute façon jamais voir cette trace de pile, et personnellement je n’ai aucun problème à la consulter dans Sentry. Sinon, je ne pourrais même pas la voir du tout
    • Critiquer en disant qu’il « suffirait » « évidemment » de faire autrement, sans connaître le contexte de la discussion ni les compromis envisagés, ça peut paraître un peu arrogant
      Il y a plusieurs façons de proposer une alternative
    • Sur macOS/iOS, on peut aussi distribuer un binaire Mach-O ne contenant que la section LC_FUNCTION_STARTS
      C’est ainsi que, sur ces plateformes, la symbolisation parvient à retrouver les noms de fonctions des bibliothèques système sans symboles de débogage complets
    • Cela peut quand même devenir assez volumineux. Personnellement, je pense que ça en vaut la peine et je l’inclus en général systématiquement, mais la plupart des logiciels ne le font pas
  • C’est excellent et très créatif. Beaucoup de projets pourraient s’en inspirer. L’idée clé est d’enregistrer la trace de pile avec un compteur ordinal relatif par exécutable/objet partagé
    Bun étant, à ma connaissance, lié statiquement, sur un système à liaison dynamique il faudrait probablement préfixer chaque compteur ordinal normalisé avec un petit identifiant numérique de l’objet partagé

    • Ce n’est pas une approche entièrement nouvelle. C’est courant dans des environnements comme les jeux, où on ne peut pas distribuer les symboles, lorsqu’un crash se produit sur le PC d’un joueur
      Par exemple, le crash reporter d’Unreal Engine peut envoyer ce format simple depuis des années, et il permet de reconstituer avec assez de précision la fonction et le numéro de ligne de chaque frame de pile. En général, on préfère tout de même les mini-dumps, car disposer aussi des variables de pile donne des indices supplémentaires sur ce qui s’est passé
  • Microsoft est vraiment très fort pour ce genre de choses. Dans SQL Server, ils utilisaient des mini-dumps dépourvus de données personnelles, et c’était à la fois très compact et extrêmement utile
    Déjà à l’époque, il y a 15 ans, les dumps complets d’un SQL Server en production étaient des fichiers tellement énormes qu’ils étaient difficiles à déplacer

    • Je me demande si c’était pour des services internes à Microsoft ou pour des environnements déployés chez les clients. Si c’était le second cas, comment savaient-ils ce qui relevait des informations personnellement identifiables ?
  • J’observe Bun depuis des années depuis le premier tweet lié à Zig, et je me suis mis à l’utiliser récemment ; ça fonctionne simplement, sans complications particulières

  • Bun est assez séduisant. Je l’ai essayé sur quelques petits projets d’exemple, c’est rapide, et j’aime bien le fait qu’il combine gestion de paquets et runtime JavaScript
    En revanche, j’utilise Dependabot sur la plupart de mes projets sérieux. De ce que je sais, le support de Bun dans Dependabot est en cours ou au moins discuté dans plusieurs issues du dépôt, donc j’attends qu’il soit publié avant de l’adopter

    • Nous aussi, nous avons hésité à cause de l’absence de support de Dependabot avant de migrer, puis nous avons découvert que Renovate fonctionnait avec Bun et constituait une alternative suffisante pour l’instant
      Aucun regret. Les gains cumulés apportés par les accélérations et la nette amélioration de l’expérience développeur valent vraiment ce qu’on en attendait
  • Peu de gens se rendront compte du soin apporté à ce genre de détails. J’aime ce que cela montre de l’attention portée par l’équipe de Bun à son craft

  • Bun est impressionnant, mais récemment j’ai essayé de créer un serveur HTTP/2 avec Fastify et ça n’a pas marché
    J’ai obtenu l’erreur node:http2 createServer is not yet implemented in Bun, et l’issue pointée par le message concerne en réalité le support du client HTTP/2. Ce support client a déjà été publié dans la v1.0.13 : https://bun.sh/blog/bun-v1.0.13#http2-client-support
    Le message NotImplementedError devrait être modifié pour pointer vers l’issue côté serveur : https://github.com/oven-sh/bun/issues/8823
    Le support du serveur HTTP/2 fait partie des demandes de fonctionnalités les plus votées : https://github.com/oven-sh/bun/issues?q=is%3Aissue+is%3Aopen...
    Quand cette fonctionnalité arrivera, beaucoup plus de gens pourront sans doute passer à Bun

    • C’est l’état actuel de Bun. On attend une implémentation ; une fois terminée, on découvre qu’il faut encore d’autres implémentations d’API ; puis, en attendant à nouveau, ça finit par arriver mais ça plante dans divers cas limites ; et ainsi de suite
      Bun en est encore à un stade bien trop précoce de son cycle de vie. Cela dit, j’ai beaucoup d’espoir pour le projet
  • Je me demande s’il y a vraiment des gens qui utilisent Bun. Est-ce aussi bien qu’on le dit ?

    • Je ne l’ai pas encore utilisé en production, mais pour des scripts ponctuels et des side projects, c’était excellent
      Dans un environnement TypeScript sur Node, configurer ts-node, ts-jest, le support ESM, le top-level await, etc., est inutilement fastidieux. Les versions récentes de Node ont réduit une partie de cette friction, mais ce n’est toujours pas aussi simple que bun init. J’apprécie aussi beaucoup l’API shell de Bun : https://bun.sh/blog/the-bun-shell
    • C’est correct si vous n’avez pas besoin d’un REPL ou si vous ne comptez pas utiliser de modules natifs. Il y a bien un REPL, mais il a toujours plus de 6 secondes de latence à chaque mise à jour, ce qui le rend très pénible
      Les messages d’erreur sont aussi nettement moins bons que ceux de Node. Je l’ai utilisé un moment, mais maintenant —loader tsx sur Node fait tout ce que je veux sans les inconvénients. Pour des serveurs simples, par exemple avec WebSocket, si vous êtes sûr de ne pas avoir besoin de modules natifs, Bun peut valoir le coup. J’exploite d’ailleurs quelques services comme ça
    • J’ai commencé à l’utiliser dès la sortie de la 1.0 et je ne suis jamais revenu en arrière. Je l’adopte maintenant sur tous mes projets
    • Je l’utilise comme exécuteur de développement et de tests sur un projet de langage de programmation d’environ 15 000 lignes, et jusqu’ici je n’ai eu aucun problème spécifique à Bun
      La vitesse de démarrage instantanée reste encore étonnante
    • Je l’utilise récemment et c’est vraiment très bien. Ce genre d’amélioration de la qualité de vie où l’on n’a plus à se soucier de la compilation TypeScript est vraiment appréciable, et c’est rapide
      Il manque encore des choses, mais pour moi c’est déjà meilleur que Node
  • Cet article ressemble aussi à une excellente étude de cas sur Zig. Très intéressant

  • Avec Bun, il faut télécharger 37 paquets avant de pouvoir utiliser le REPL. Sans connexion Internet, on ne peut même pas lancer le REPL
    En exécutant bun repl, j’obtiens une erreur disant que le téléchargement du manifeste du paquet bun-repl a échoué. Ce n’est pas un gros problème, mais je m’attendais à ce qu’un simple exécutable unique placé dans le PATH fonctionne immédiatement sans installation, et j’en avais beaucoup d’espoir

    • Le REPL n’a pas encore été traité comme une priorité. Le REPL actuel est en fait le paquet npm bun-repl implémenté par la communauté
      En interne, bun repl fait la même chose que bunx bun-repl