1 points par GN⁺ 2024-04-05 | 1 commentaires | Partager sur WhatsApp
  • Avec GCC 14, -fanalyzer suit plusieurs chemins d’exécution du code C par exécution symbolique et se concentre sur la détection, à la compilation, de défauts difficiles à repérer avant l’exécution
  • Les améliorations se répartissent entre détection des boucles infinies, visualisation des dépassements de tampon, suivi des chaînes C et activation par défaut de l’analyse de contamination, avec l’objectif de rendre les diagnostics plus compréhensibles
  • -Wanalyzer-out-of-bounds va au-delà d’un simple avertissement en montrant, via des diagrammes textuels, la relation entre le tampon et l’emplacement d’écriture, et indique aussi à quel endroit, au milieu d’un caractère, le dépassement se produit dans une chaîne UTF-8
  • Le nouvel attribut null_terminated_string_arg(PARAM_IDX) signale à l’analyseur et aux lecteurs du code qu’une API attend une chaîne terminée par nul, ce qui met mieux en évidence les chemins où un tampon non terminé par nul est transmis
  • L’analyse de contamination est activée par défaut lorsque -fanalyzer est sélectionné, ce qui permet d’identifier plus facilement les flux où une valeur contrôlée par un attaquant est utilisée comme taille, index, décalage, etc. sans contrôle de borne supérieure

Périmètre des problèmes traités par -fanalyzer

  • -fanalyzer est une passe d’analyse statique de GCC qui tente de trouver des défauts à la compilation en exécutant symboliquement plusieurs chemins d’exécution du code source C
  • Les améliorations de GCC 14 ont été présentées avant la sortie officielle d’avril 2024 ; au moment de la rédaction, la sortie de GCC 14.1 était attendue courant avril 2024
  • La préversion de GCC 14.0 est déjà utilisée dans Fedora 40 Beta
  • Les exemples peuvent être testés dans Compiler Explorer avec les nouvelles options du compilateur

Détection des boucles infinies simples

  • GCC 14 ajoute un nouvel avertissement -Wanalyzer-infinite-loop
  • L’exemple de code contient des boucles for imbriquées : la condition de la seconde boucle est j < n, mais l’expression d’incrémentation est restée i++
    • Cela correspond à une erreur où la première boucle for a été copiée, sans remplacer le i de l’incrémentation par j
    • Sur ce chemin, l’analyseur suit continuellement la branche vraie de j < n et signale les itérations de boucle
  • La sortie de diagnostic actuelle est plus facile à comprendre en lisant les événements numérotés de (1) à (5) dans l’ordre
  • Pour GCC 15, l’amélioration de la lisibilité au moyen de techniques comme de l’art ASCII mettant en évidence les chemins de flot de contrôle reste un souhait
  • Ce code peut être exécuté dans cet exemple Compiler Explorer

Visualisation textuelle des dépassements de tampon

  • Dans GCC 13, l’analyseur a commencé à prendre en charge les contrôles de bornes via -Wanalyzer-out-of-bounds
  • Dans GCC 14, il peut produire un diagramme textuel des relations spatiales d’un dépassement de tampon prédit
  • Dans un exemple qui exécute strcpy(buf, "hello") sur char buf[10], puis appelle strcat(buf, " world!"), un dépassement de tampon sur la pile est détecté
    • L’ancien message indiquait que buf avait une capacité de 10 octets et qu’une écriture hors limites se produisait de l’octet 10 à l’octet 12
    • Le diagramme de GCC 14 montre à la fois le tampon de destination rempli par strcpy et l’octet NUL de fin existant qui sert de point de départ à strcat
  • Même dans des exemples de chaînes non ASCII, il suit la représentation UTF-8 et indique l’emplacement du dépassement
    • Le code utilisé en exemple copie "サツキ" dans char buf[11], puis y concatène "メイ"
    • Le diagramme montre que le dépassement se produit au milieu du caractère , c’est-à-dire U+30E1
  • Le code correspondant est disponible dans les exemples chaîne ASCII et chaîne non ASCII

Suivi renforcé des opérations sur les chaînes C

  • L’analyseur de GCC 14 améliore le suivi des opérations sur les chaînes C en simulant les API qui parcourent un tampon à la recherche de l’octet de terminaison nul
  • Il émet un avertissement s’il existe un chemin où le pointeur désigne un tampon non terminé par nul et est transmis à cette API
  • Le nouvel attribut de fonction null_terminated_string_arg(PARAM_IDX) indique à l’analyseur et aux lecteurs du code qu’un paramètre donné doit être une chaîne terminée par nul
  • Dans l’exemple, example_fn(const char *p) porte les attributs null_terminated_string_arg(1) et nonnull
    • char str[3] = "abc"; ne dispose pas d’espace pour contenir l’octet de terminaison nul
    • Lors de l’appel example_fn(str), l’analyseur signale une lecture excessive de tampon sur la pile d’un octet au-delà de la fin de str
    • Le diagnostic inclut aussi une note indiquant que le premier argument de example_fn doit être un pointeur vers une chaîne terminée par nul
  • Cet exemple peut être exécuté dans Compiler Explorer

Activation par défaut de l’analyse de contamination

Exemple d’analyse du noyau avec CVE-2011-2210

  • Un extrait lié à CVE-2011-2210 dans le noyau Linux est utilisé comme exemple d’analyse de contamination
  • En ajoutant __attribute__((tainted_args)) à la macro __SYSCALL_DEFINEx, on indique à l’analyseur que les arguments de osf_getsysinfo proviennent d’une frontière de confiance et doivent être traités comme des valeurs contaminées
  • L’analyseur de GCC 14 avertit que, dans copy_to_user(buffer, hwrpb, nbytes), la valeur contrôlée par l’attaquant nbytes est utilisée comme taille sans contrôle de borne supérieure
    • Le diagnostic indique que nbytes n’a fait l’objet que d’un contrôle de borne inférieure dans if (nbytes < sizeof(*hwrpb))
    • Le troisième paramètre de copy_to_user est indiqué comme paramètre de taille par l’attribut access(write_only, 1, 3)
  • Le problème vient du fait que la condition de validation est écrite if (nbytes < sizeof(*hwrpb))
  • Le travail consistant à exécuter l’analyseur sur le noyau, à trouver des vulnérabilités et à corriger les faux positifs de l’analyseur se poursuit

1 commentaires

 
GN⁺ 2024-04-05
Commentaires Hacker News
  • Pour moi, fanalyzer est l’une des fonctionnalités décisives de GCC face à Clang. Il explique les erreurs, ce qui rend la programmation en C bien plus facile, et ses messages d’erreur commencent à me faire penser à Rust par leur côté convivial pour les développeurs

    • Je sais que Rust attire particulièrement l’attention sur HN pour sa sécurité mémoire et ses bonnes abstractions, mais je me demande quelle part de sa popularité vient de ses messages d’erreur
      L’une des principales raisons pour lesquelles on abandonne l’apprentissage d’une technologie, ce sont souvent des erreurs frustrantes ou peu claires. Je m’éloigne un peu du sujet, mais je voulais dire que j’aime le C et que de meilleurs messages d’erreur, au niveau de Rust, le rendraient encore meilleur
    • Clang a aussi un outil similaire, Clang Static Analyzer : https://clang-analyzer.llvm.org/
    • J’ai eu exactement l’expérience inverse. Clang affiche très souvent de bien meilleurs messages d’erreur que GCC, certaines alertes ou erreurs attrapent davantage de cas, et clang-tidy fournit une bien meilleure analyse statique
    • Ça me rappelle l’une des raisons pour lesquelles je détestais autant le C++. Au lieu d’un simple error: missing semicolon, je me retrouvais avec plus de 1000 lignes de messages sur l’instanciation de templates
    • Ça me paraît assez surprenant. Je me demande ce que l’analyseur de GCC détecte de plus que l’analyseur statique de Clang ne signale pas déjà
      J’ai essayé l’analyseur de GCC plusieurs fois, mais je n’ai pas trouvé de bon frontend qui rende sa sortie agréable à lire. Clang propose pas mal d’options, comme une sortie HTML assez correcte, CodeChecker, l’intégration Xcode, etc. Je me demande donc comment vous lisez la sortie côté GCC. En plus, GCC semble produire bien plus de faux positifs que Clang
  • Il y a 36 autres commentaires dans un autre fil : https://news.ycombinator.com/item?id=39918278
    « GCC 14 Boasts Nice ASCII Art for Visualizing Buffer Overflows (phoronix.com) », publié il y a 2 heures

  • Il y a quelques mois, j’ai créé un petit utilitaire Linux. C’était un shim drop-in qui remplaçait n’importe quel exécutable ; lorsqu’il était appelé, il se faisait passer pour le programme d’origine, forkait l’original et se branchait sur stdout/stderr
    La sortie d’erreur était envoyée à un assistant GPT personnalisé connaissant le contexte du programme, qui reformulait l’erreur d’origine dans une forme plus lisible par un humain avant de la renvoyer vers stderr du shim. Je l’utilisais parce que j’en avais assez de lire les dumps imbriqués des compilateurs GCC/Clang autour des concepts/templates, mais on pouvait en fait l’utiliser avec n’importe quel programme. Ça marchait bien, mais je suis tombé gravement malade et je n’ai pas pu continuer ; si quelqu’un reprenait l’idée pour en faire quelque chose de plus propre et général, ce pourrait être un beau projet

  • J’aimerais vraiment un meilleur format de sortie pour les résultats d’analyse. Dans sa forme actuelle, c’est l’enfer pour les lecteurs d’écran

    • Pour information, j’ai implémenté la sortie SARIF dans GCC 13, et on peut par exemple l’afficher dans VS Code via un plugin. En revanche, l’ASCII art n’y figure pas
      Un exemple de sortie est visible ici : https://godbolt.org/z/aan6Kfxds
      J’ai simplement ajouté l’option de ligne de commande -fdiagnostics-format=sarif-stderr au premier exemple de l’article. J’ai aussi expérimenté une sortie des diagrammes en SVG, mais ce n’était pas assez abouti pour l’intégrer dans GCC 14
  • Le code d’origine renvoyait -1 si nbytes < sizeof(*hwrpb), et -2 si copy_to_user(buffer, hwrpb, nbytes) échouait. La correction appliquée a consisté à vérifier nbytes > sizeof(*hwrpb), mais à mon avis la vraie correction serait copy_to_user(buffer, hwrpb, sizeof(*hwrpb))
    Copier depuis le pointeur hwrpb avec une taille autre que sizeof(*hwrpb) n’a pas de sens

    • Si l’appelant passe nbytes = 4 et que sizeof(hwrpb) vaut 16 octets, alors 12 octets de trop seront copiés depuis le tampon de l’appelant, ce qui peut lire une mémoire qui ne lui appartient pas. À éviter, selon moi
      Une meilleure solution serait de ne copier que le minimum d’octets pris en charge à la fois par l’appelant et l’appelé. Par exemple, nbytes = MIN(nbytes, sizeof(hwrpb));. En supposant que les informations de version dans hwrpb->size soient respectées, cela permettrait d’assurer la compatibilité ascendante et descendante même si une partie de la structure n’est pas initialisée
    • Oui. Mais puisque la taille du tampon est fournie, cela n’a pas plus de sens d’écrire au-delà de la fin du tampon de l’appelant, donc on ne peut pas non plus passer une valeur supérieure à nbytes
  • C’est vraiment excellent. Le volume de travail fourni a l’air énorme. La difficulté me semble comparable à l’introduction de fat pointers / vues de tableaux dans la bibliothèque standard et dans le standard C

  • -Wstringop-overflow produit tellement de faux positifs que c’est l’une des premières alertes que je désactive. Je doute qu’une variante de l’analyseur fasse mieux

    • Ça revient un peu à retirer la batterie du détecteur de monoxyde de carbone parce qu’il bippe sans arrêt et vous donne mal à la tête ou envie de dormir
  • Très sympa. Je ne fais plus beaucoup de développement C ces jours-ci, donc je me demande à quelle fréquence strcpy et strcat sont encore utilisés. La dernière fois que j’ai regardé, ils étaient presque aussi tabous que goto
    Bien sûr, je sais qu’en développement kernel, goto est souvent privilégié. Je me demande dans quelle mesure l’analyse des chaînes C est réellement utile

    • Dans certains contextes, utiliser goto est clairement la bonne solution, et même élégante. Essayer de l’éviter à tout prix peut produire un code laid, embrouillé et difficile à maintenir. Ce n’est pas fréquent, mais il y a des usages légitimes
      Des fonctions comme strcpy sont moins recommandées, mais il existe des cas où leur utilisation est garantie correcte tant que des invariants plus forts — par exemple au niveau du langage — ne sont pas violés. Si ces invariants sont déjà rompus, le problème est de toute façon bien plus grave. C’est rare, mais on peut aussi soutenir qu’une alternative nominalement plus sûre sera parfois un peu moins efficace sans réel bénéfice
    • Tant qu’on l’utilise comme équivalent logique à des constructions de programmation structurée absentes du C, certains usages de goto restent idiomatiques en C. Il faut être prudent, bien sûr, mais enfin, c’est du C
      En revanche, je n’aime pas du tout longjmp
    • Il n’y a aucun problème avec un usage simple de goto. En revanche, la famille strxcpy est complètement catastrophique et ne devrait être utilisée sous aucun prétexte. Le fait qu’elle soit utilisée dans le kernel est effrayant
      Ces fonctions, ainsi que toutes les tentatives ratées de les « corriger », auraient dû être éjectées en orbite
    • goto peut aller si on l’utilise avec discernement. strcpy et strcat aussi, dans le sens où ils « vont » tant qu’on sait que le code est correct, et que s’il ne l’est pas les conséquences peuvent être graves. Malheureusement, cette description s’applique à la majeure partie du C
    • Je ne dirais pas que goto est aussi tabou que strcat ou strcpy. goto, ça va ; en revanche, strcat et strcpy utilisés sans malloc de taille exacte dans le même scope, c’est presque une mauvaise odeur de code
  • Très bien. Je suis content de voir que des rapports détaillés expliquant précisément ce qui ne va pas sont fournis partout