Utilisation de l'apprentissage automatique dans l'analyse statique du code source du programme

Utilisation de l'apprentissage automatique dans l'analyse statique du code source du programme

L'apprentissage automatique est profondément enraciné dans divers domaines de l'activité humaine: de la reconnaissance vocale au diagnostic médical. La popularité de cette approche est si grande qu'ils essaient de l'utiliser dans la mesure du possible. Certaines tentatives pour remplacer les approches classiques par des réseaux de neurones ne réussissent pas aussi bien. Jetons un œil à l'apprentissage automatique du point de vue de la création d'analyseurs de code statique efficaces pour détecter les bogues et les vulnérabilités potentielles.

On demande souvent à l'équipe PVS-Studio si nous voulons commencer à utiliser l'apprentissage automatique pour trouver des erreurs dans le code source des programmes. Réponse courte: oui, mais très limitée. Nous pensons qu'avec l'utilisation de l'apprentissage automatique dans les problèmes d'analyse de code, il existe de nombreux pièges. Dans la deuxième partie de l'article, nous en parlerons. Commençons par un examen des nouvelles solutions et idées.

De nouvelles approches


Actuellement, il existe déjà de nombreuses implémentations d'analyseurs statiques basés sur ou utilisant l'apprentissage automatique, y compris l'apprentissage en profondeur et la PNL pour la détection d'erreurs. Non seulement les passionnés, mais aussi les grandes entreprises, comme Facebook, Amazon ou Mozilla, ont attiré l'attention sur le potentiel de l'apprentissage automatique lors de la recherche d'erreurs. Certains projets ne sont pas des analyseurs statiques à part entière, mais seulement entre les deux, trouvent des erreurs spécifiques lors des validations.

Fait intéressant, presque tous sont positionnés comme des produits révolutionnaires qui, avec l'aide de l'intelligence artificielle, changeront le processus de développement.


Prenons quelques exemples bien connus:

  1. Deepcode
  2. Infer, Sapienz, SapFix
  3. Enhardir
  4. Source {d}
  5. Clever-Commit, Commit Assistant
  6. CodeGuru

Deepcode


Deep Code est un outil de recherche de vulnérabilités dans le code de programmes écrits en Java, JavaScript, TypeScript et Python, dans lequel l'apprentissage automatique est présent en tant que composant. Selon Boris Paskalev, plus de 250 000 règles fonctionnent déjà. Cet outil est formé sur la base des modifications apportées par les développeurs au code source des projets ouverts (un million de référentiels). La société elle-même affirme que son projet est grammatical pour les développeurs.



Essentiellement, cet analyseur compare votre solution avec sa base de données de projets et vous offre la meilleure solution estimée à partir de l'expérience d'autres développeurs.

En mai 2018, les développeurs ont écrit que la prise en charge du langage C ++ était en cours de préparation, mais ce langage n'est toujours pas pris en charge. Bien qu'il soit indiqué sur le site lui-même qu'une nouvelle langue peut être ajoutée en quelques semaines, car une seule étape dépend de la langue - l'analyse.





Un groupe de publications sur les méthodes sur lesquelles l'analyseur est basé est également publié sur le site.

Inférer


Facebook essaie assez largement d'introduire de nouvelles approches dans ses produits. Ils n'ont pas ignoré leur attention et leur apprentissage automatique. En 2013, ils ont acheté une startup qui développait un analyseur statique basé sur une machine. Et en 2015, le code source du projet est devenu ouvert .

Infer est un analyseur statique pour les projets écrits en Java, C, C ++ et Objective-C, développé par Facebook. Selon le site, il est également utilisé dans Amazon Web Services, Oculus, Uber et d'autres projets populaires.

Infer est actuellement capable de détecter les erreurs liées au déréférencement d'un pointeur nul, les fuites de mémoire. Infer est basé sur la logique de Hoar, la logique de séparation et la bi-abduction, ainsi que sur la théorie de l'interprétation abstraite. L'utilisation de ces approches permet à l'analyseur de diviser le programme en petits blocs (morceaux) et de les analyser indépendamment les uns des autres.

Vous pouvez essayer d'utiliser Infer sur vos projets, cependant, les développeurs avertissent que bien que sur les projets Facebook, les hits utiles représentent 80% des résultats, sur d'autres projets, un faible nombre de faux positifs n'est pas garanti. Certaines des erreurs qu'Infer ne peut pas encore trouver, mais les développeurs travaillent sur l'introduction de tels déclencheurs:

  • sortir du tableau;
  • exceptions de transtypage;
  • fuite de données non vérifiées;
  • race race condition.

Sapfix


SapFix est un outil d'édition automatisé. Il reçoit des informations de Sapienz, un outil d'automatisation des tests et de l'analyseur statique Infer, et sur la base des derniers changements et messages, Infer choisit l'une des nombreuses stratégies pour corriger les erreurs.



Dans certains cas, SapFix annule tout ou partie des modifications. Dans d'autres cas, il essaie de résoudre le problème en générant un correctif à partir de son ensemble de modèles de correctifs. Cet ensemble est formé à partir des modèles d'édition compilés par les programmeurs eux-mêmes à partir de l'ensemble des modifications déjà effectuées une fois. Si un tel modèle ne corrige pas l'erreur, SapFix essaie d'ajuster le modèle à la situation, en apportant de petites modifications dans l'arborescence de syntaxe abstraite jusqu'à ce qu'une solution potentielle soit trouvée.

Mais une solution potentielle ne suffit pas, donc SapFix recueille plusieurs solutions qui sont sélectionnées sur la base de trois questions: y a-t-il des erreurs de compilation, y a-t-il un crash, la modification introduit-elle de nouveaux plantages. Une fois les modifications entièrement testées, les correctifs sont envoyés pour examen au programmeur qui décide laquelle des modifications résout le mieux le problème.

Enhardir


Embold est une plateforme de démarrage pour l'analyse statique du code source des programmes, qui avant le changement de nom s'appelait Gamma. L'analyse statique est effectuée sur la base de nos propres diagnostics, ainsi que sur la base d'analyseurs intégrés tels que Cppheck, SpotBugs, SQL Check et autres.



En plus des diagnostics eux-mêmes, l'accent est mis sur la possibilité d'afficher visuellement des infographies par la charge de la base de code et de visualiser facilement les erreurs trouvées, ainsi que de rechercher la possibilité de refactoring. De plus, cet analyseur dispose d'un ensemble d'anti-patterns qui vous permet de détecter des problèmes dans la structure du code au niveau des classes et des méthodes, et de diverses métriques pour calculer la qualité du système.



L'un des principaux avantages est la solution intelligente et le système de suggestion de révision qui, en plus des diagnostics habituels, vérifient les révisions en fonction des informations sur les modifications précédentes.



À l'aide de NLP, Embold décompose le code en parties et recherche les interconnexions et les dépendances entre les fonctions et les méthodes entre elles, ce qui économise du temps de refactoring.



Ainsi, Embold offre principalement une visualisation pratique des résultats d'analyse de votre code source par divers analyseurs, ainsi que ses propres diagnostics, dont certains sont basés sur l'apprentissage automatique.

Source {d}


La source {d} est la plus ouverte en termes de mise en œuvre à partir des analyseurs que nous avons examinés. C'est également une solution open source . Sur leur site Web, vous pouvez (en échange de votre adresse e-mail) obtenir un livret avec une description des technologies qu'ils utilisent. En outre, il contient un lien vers la base de publications qu'ils ont collectée concernant l'utilisation du machine learning pour l'analyse de code, ainsi qu'un référentiel avec un ensemble de données pour la formation sur le code. Le produit lui-même est une plate-forme entière pour analyser le code source et le produit logiciel, et se concentre plutôt non pas sur les développeurs, mais sur le lien des gestionnaires. Parmi ses capacités, il y a une fonction pour identifier le volume de la dette technique, les goulots d'étranglement dans le processus de développement et d'autres statistiques mondiales sur le projet.



Ils fondent leur approche de l'analyse de code assistée par machine sur l'hypothèse naturelle, formulée dans l'article " Sur le caractère naturel du logiciel ".

«Les langages de programmation, en théorie, sont complexes, flexibles et puissants, mais les programmes que les vraies personnes écrivent sont en fait simples et assez répétitifs, et contiennent donc des propriétés statistiques utiles et prévisibles qui peuvent être exprimées en statistiques modèles de langage et utilisation pour les tâches de développement logiciel. ”

Sur la base de cette hypothèse, plus la base de code pour la formation de l'analyseur est grande, plus les propriétés statistiques ressortent et plus les métriques obtenues avec la formation seront précises.

Pour analyser le code, la source {d} utilise le service Babelfish, qui peut analyser un fichier de code dans n'importe quelle langue disponible, obtenir un arbre de syntaxe abstrait et le convertir en arbre de syntaxe universel.



Cependant, la source {d} ne recherche pas d'erreurs dans le code. Sur la base de l'arborescence, en utilisant l'apprentissage automatique sur la base de l'ensemble du projet, la source {d} révèle comment le code est formaté, quel style de codage est utilisé dans le projet et lors de la validation, et si le nouveau code ne correspond pas au style de code du projet, il apporte les modifications appropriées.





La formation est guidée par plusieurs éléments de base: espaces, tabulations, sauts de ligne, etc.



Vous pouvez en savoir plus à ce sujet dans leur publication: " STYLE-ANALYZER: correction des incohérences de style de code avec des algorithmes interprétables non supervisés ".

En général, source {d} est une large plate-forme de collecte d'une grande variété de statistiques sur le code source et le processus de développement de projet, du calcul de l'efficacité des développeurs à l'identification des coûts de temps pour les révisions de code.

Engagement intelligent


Clever-Commit est un analyseur créé par Mozilla en collaboration avec Ubisoft. Il est basé sur l' étude CLEVER (Combining Levels of Bug Prevention and Resolution Techniques) d'Ubisoft, et son Commit Assistant basé sur le produit, qui identifie les validations suspectes susceptibles de contenir une erreur. Étant donné que CLEVER est basé sur la comparaison de codes, il indique non seulement un code dangereux, mais fait également des suggestions sur d'éventuelles corrections. Selon la description, dans 60 à 70% des cas, Clever-Commit trouve des zones problématiques et avec la même fréquence propose des corrections correctes. En général, il y a peu d'informations sur ce projet et sur les erreurs qu'il peut trouver.

CodeGuru


Et plus récemment, la liste des analyseurs utilisant le machine learning a été reconstituée avec un produit d'Amazon appelé CodeGuru. Ce service est basé sur l'apprentissage automatique, ce qui vous permet de trouver des erreurs dans le code, ainsi que d'identifier des sections coûteuses. Jusqu'à présent, l'analyse est uniquement pour le code Java, mais ils écrivent sur la prise en charge d'autres langages à l'avenir. Bien qu'il ait été annoncé récemment, le PDG d'AWS (Amazon Web Services) Andy Jassi dit qu'il l'utilise depuis longtemps sur Amazon lui-même.

Le site indique que la formation a été dispensée sur la base de code d'Amazon elle-même, ainsi que sur plus de 10 000 projets open source.

En fait, le service est divisé en deux parties: CodeGuru Reviewer, formé à la recherche de règles associatives et à la recherche d'erreurs dans le code, et CodeGuru Profiler, qui surveille les performances des applications.



En général, peu d'informations ont été publiées sur ce projet. Le site indique que pour apprendre à détecter les écarts par rapport aux "meilleures pratiques", Reviewer analyse les bases de code Amazon et recherche les pull-requests contenant des appels d'API AWS. Il examine ensuite les modifications apportées et les compare avec les données de la documentation, qui sont analysées en parallèle. Le résultat est un modèle de «meilleures pratiques».

Il est également dit que les recommandations de code personnalisé s'améliorent après avoir reçu des commentaires sur les recommandations.

La liste des erreurs auxquelles Reviewer répond est plutôt floue, car aucune documentation spécifique pour les erreurs n'a été publiée:
  • Meilleures pratiques AWS
  • Accès simultané
  • Fuites de ressources
  • Fuite d'informations confidentielles
  • "Meilleures pratiques" communes pour le codage

Notre scepticisme


Examinons maintenant le problème de la recherche d'erreurs à travers les yeux de notre équipe, qui développe des analyseurs statiques depuis de nombreuses années. Nous voyons un certain nombre de problèmes de haut niveau dans l'application de la formation, dont nous voulons parler. Mais au début, nous divisons grossièrement toutes les approches ML en deux types:

  1. Entraînez manuellement un analyseur statique à rechercher divers problèmes à l'aide d'exemples de code synthétique et réel;
  2. Entraînez les algorithmes sur un grand nombre de codes open source (GitHub) et modifiez l'historique, après quoi l'analyseur lui-même commencera à détecter les erreurs et même à suggérer des corrections.

Nous parlerons de chaque direction séparément, car elles présenteront diverses lacunes inhérentes. Après quoi, je pense, les lecteurs comprendront pourquoi nous ne nions pas la possibilité de l'apprentissage automatique, mais ne partageons pas non plus l'enthousiasme.

Remarque Nous regardons du point de vue du développement d'un analyseur statique universel à usage général. Nous nous concentrons sur le développement d'un analyseur qui ne se concentre pas sur une base de code spécifique, mais que toute équipe peut utiliser dans n'importe quel projet.

Formation sur les analyseurs statiques manuels


Supposons que nous voulions utiliser ML pour que l'analyseur commence à rechercher des anomalies de la forme suivante dans le code:

if (A == A) 

Il est étrange de comparer une variable avec elle-même. Nous pouvons écrire de nombreux exemples de code correct et incorrect et entraîner l'analyseur à rechercher de telles erreurs. De plus, il est possible d'ajouter de vrais exemples d'erreurs déjà trouvées aux tests. La question, bien sûr, est de savoir où obtenir ces exemples. Mais nous considérerons que c'est possible. Par exemple, nous avons accumulé un certain nombre d'exemples de telles erreurs: V501 , V3001 , V6001 .

Alors, est-il possible de rechercher de tels défauts dans le code en utilisant des algorithmes d'apprentissage automatique? Tu peux. Mais ce n'est pas clair pourquoi faire ça!

Voir, afin de former l'analyseur, nous devons consacrer beaucoup d'efforts à la préparation d'exemples pour la formation. Ou balisez le code des applications réelles, indiquant où jurer et où non. Dans tous les cas, beaucoup de travail devra être fait, car il devrait y avoir des milliers d'exemples de formation. Ou des dizaines de milliers.

Après tout, nous voulons rechercher non seulement les cas (A == A), mais aussi:

  • si (X && A == A)
  • si (A + 1 == A + 1)
  • si (A [i] == A [i])
  • si ((A) == (A))
  • et ainsi de suite.



Voyons maintenant comment un diagnostic aussi simple serait implémenté dans PVS-Studio:

 void RulePrototype_V501(VivaWalker &walker, const Ptree *left, const Ptree *right, const Ptree *operation) { if (SafeEq(operation, "==") && SafeEqual(left, right)) { walker.AddError(" , !", left, 501, Level_1, "CWE-571"); } } 

Et c'est tout. Aucun exemple de base de formation nécessaire!

À l'avenir, les diagnostics devraient être appris à prendre en compte un certain nombre d'exceptions et à comprendre que l'on devrait jurer (A [0] == A [1-1]). Cependant, tout cela est très facile à programmer. Mais juste avec la base d'exemples de formation, tout ira mal.

Notez que dans les deux cas, un système de test, la rédaction de documentation, etc. seront toujours nécessaires. Cependant, l'effort pour créer un nouveau diagnostic est clairement du côté de l'approche classique, où la règle est simplement codée en dur dans le code.

Regardons maintenant une autre règle. Par exemple, que le résultat de certaines fonctions doit être utilisé. Cela n'a aucun sens de les appeler sans utiliser leur résultat. Voici certaines de ces fonctionnalités:
  • malloc
  • memcmp
  • chaîne :: vide

En général, c'est ce que font les diagnostics de V530 implémentés dans PVS-Studio.

Donc, nous voulons rechercher des appels à de telles fonctions où le résultat de leur travail n'est pas utilisé. Pour ce faire, vous pouvez générer de nombreux tests. Et nous pensons que tout fonctionnera bien. Mais encore une fois, il n'est pas clair pourquoi cela est nécessaire.

L'implémentation des diagnostics V530 avec toutes les exceptions dans l'analyseur PVS-Studio comprend 258 lignes de code, dont 64 lignes sont des commentaires. De plus, il y a un tableau avec des annotations de fonctions, où il est noté que leur résultat doit être utilisé. Il est beaucoup plus facile de reconstituer ce tableau que de créer des exemples synthétiques.

La situation sera encore pire avec les diagnostics qui utilisent l'analyse de flux de données. Par exemple, l'analyseur PVS-Studio peut suivre la valeur des pointeurs, ce qui permet de trouver une telle fuite de mémoire:

 uint32_t* BnNew() { uint32_t* result = new uint32_t[kBigIntSize]; memset(result, 0, kBigIntSize * sizeof(uint32_t)); return result; } std::string AndroidRSAPublicKey(crypto::RSAPrivateKey* key) { .... uint32_t* n = BnNew(); .... RSAPublicKey pkey; pkey.len = kRSANumWords; pkey.exponent = 65537; // Fixed public exponent pkey.n0inv = 0 - ModInverse(n0, 0x100000000LL); if (pkey.n0inv == 0) return kDummyRSAPublicKey; // <= .... } 

Un exemple est tiré de l'article " Chrome: fuites de mémoire ". Si la condition (pkey.n0inv == 0) est remplie , alors la fonction se termine sans libérer le tampon, dont le pointeur est stocké dans la variable n .

Du point de vue de PVS-Studio, rien de compliqué. L'analyseur a étudié la fonction BnNew et s'est rappelé qu'elle renvoie un pointeur sur un bloc de mémoire allouée. Dans une autre fonction, il a remarqué qu'une situation est possible où le tampon n'est pas libéré et le pointeur vers celui-ci est perdu lorsque la fonction se termine.

Un algorithme général de suivi des valeurs fonctionne. Peu importe comment le code est écrit. Peu importe ce qu'il y a d'autre dans la fonction qui n'est pas lié à l'utilisation des pointeurs. L'algorithme est universel et le diagnostic V773 trouve beaucoup d'erreurs dans divers projets. Voyez à quel point les fragments de code sont différents là où les erreurs sont détectées!

Nous ne sommes pas des experts en apprentissage automatique, mais il semble qu'il y aura de gros problèmes. Il existe une quantité incroyable de façons d'écrire du code avec des fuites de mémoire. Même si la machine est formée pour suivre la valeur des variables, il sera nécessaire de la former pour comprendre qu'il existe des appels de fonction.

On soupçonne que tant d'exemples seront nécessaires pour la formation que la tâche deviendra intimidante. Nous ne disons pas qu'il est irréalisable. Nous doutons que les coûts de création d'un analyseur soient payants.

Analogie. Une analogie vient à l'esprit avec une calculatrice, où au lieu de diagnostics, il est nécessaire de programmer des opérations arithmétiques. Nous sommes sûrs que vous pouvez apprendre à une calculatrice basée sur ML à bien additionner les nombres en introduisant une base de connaissances sur le résultat des opérations 1 + 1 = 2, 1 + 2 = 3, 2 + 1 = 3, 100 + 200 = 300, etc. Comme vous le savez, l'opportunité de développer une telle calculatrice est une grande question (si une subvention n'est pas allouée pour cela :). Une calculatrice beaucoup plus simple, plus rapide, plus précise et plus fiable peut être écrite en utilisant l'opération "+" ordinaire dans le code.

Conclusion La méthode fonctionnera. Mais à notre avis, cela n'a pas de sens pratique. Le développement prendra plus de temps et le résultat sera moins fiable et précis, surtout s'il s'agit de la mise en œuvre de diagnostics complexes basés sur l'analyse du flux de données.

Apprendre de beaucoup d'open source


Eh bien, nous avons trouvé des exemples synthétiques manuels, mais il y a GitHub. Vous pouvez suivre l'historique des validations et dériver des modèles de modifications / corrections de code. Ensuite, vous pouvez signaler non seulement des sections du code suspect, mais peut-être même suggérer un moyen de le corriger.

Si vous vous arrêtez à ce niveau de détail, alors tout semble bien. Le diable, comme toujours, est dans les détails. Parlons de ces détails.

La première nuance. Source de données.

Les modifications sur GitHub sont assez chaotiques et variées. Les gens sont souvent trop paresseux pour effectuer des validations atomiques et apporter plusieurs modifications au code à la fois. Vous savez vous-même comment cela se produit: ils ont corrigé l'erreur, et en même temps refactorisé un peu ("Et ici j'ajouterai le traitement d'un tel cas en même temps ..."). Même alors, il peut ne pas être clair pour une personne si ces changements sont liés les uns aux autres ou non.

Le problème est de savoir comment distinguer les erreurs réelles de l'ajout de nouvelles fonctionnalités ou autre chose. Vous pouvez, bien sûr, planter 1 000 personnes manuellement pour marquer les validations. Les utilisateurs devront indiquer qu'ils ont corrigé l'erreur ici, en effectuant une refactorisation ici, de nouvelles fonctionnalités ici, des exigences modifiées ici, etc.

Ce balisage est-il possible? Possible. Mais faites attention à la rapidité avec laquelle le changement se produit. Au lieu d '«apprendre l'algorithme lui-même sur la base de GitHub», nous discutons déjà de la façon de surprendre des centaines de personnes pendant longtemps. Les coûts de main-d'œuvre et le coût de création d'un outil augmentent fortement.

Vous pouvez essayer d'identifier automatiquement où exactement les erreurs ont été corrigées. Pour ce faire, vous devez analyser les commentaires sur les validations, faire attention aux petites modifications locales, qui sont très probablement exactement la révision d'erreur. Il est difficile de dire dans quelle mesure vous pouvez rechercher automatiquement les correctifs de bogues. Dans tous les cas, il s'agit d'une tâche importante nécessitant une recherche et une programmation séparées.

Donc, nous n'avons pas encore atteint la formation, mais il y a déjà des nuances :).

La deuxième nuance. Lag en développement.

Les analyseurs qui seront formés sur la base de bases de données telles que GitHub seront toujours sujets à un syndrome tel que le «retard mental». En effet, les langages de programmation changent avec le temps.

C # 8.0 a introduit les types de référence Nullable pour aider à gérer les exceptions de référence Null (NRE). JDK 12 introduit une nouvelle instruction switch ( JEP 325 ). En C ++ 17, il est devenu possible d'exécuter des constructions conditionnelles au stade de la compilation ( constexpr if ). Et ainsi de suite.

Les langages de programmation évoluent. De plus, comme C ++ sont très rapides et actifs. De nouveaux modèles y apparaissent, de nouvelles fonctions standard sont ajoutées, etc. Avec de nouvelles fonctionnalités, de nouveaux modèles d'erreur apparaissent également que nous aimerions également identifier à l'aide de l'analyse de code statique.

Et ici, la méthode d'enseignement considérée a un problème: le motif d'erreur est peut-être déjà connu, il y a un désir de l'identifier, mais il n'y a rien à apprendre.

Examinons ce problème avec un exemple spécifique. La boucle for basée sur une plage est apparue en C ++ 11. Et vous pouvez écrire le code suivant, en itérant sur tous les éléments du conteneur:

 std::vector<int> numbers; .... for (int num : numbers) foo(num); 

Le nouveau cycle a entraîné un nouveau modèle d'erreur. Si le conteneur est modifié à l'intérieur de la boucle, cela entraînera l'invalidation des itérateurs «fantômes».

Considérez le code incorrect suivant:

 for (int num : numbers) { numbers.push_back(num * 2); } 

Le compilateur le transformera en quelque chose comme ceci:

 for (auto __begin = begin(numbers), __end = end(numbers); __begin != __end; ++__begin) { int num = *__begin; numbers.push_back(num * 2); } 

Pendant l'opération push_back , l' invalidation des itérateurs __begin et __end peut se produire si l'allocation de mémoire à l'intérieur du vecteur se produit. Le résultat sera un comportement de programme non défini.

Ainsi, le modèle d'erreur est connu et décrit depuis longtemps dans la littérature. L'analyseur PVS-Studio le diagnostique à l'aide des diagnostics V789 et a déjà trouvé de vraies erreurs dans les projets ouverts.

Combien de temps y aura-t-il suffisamment de nouveau code sur GitHub pour remarquer ce modèle? Bonne question ... Vous devez comprendre que si la boucle for basée sur une plage apparaît, cela ne signifie pas que tous les programmeurs ont immédiatement commencé à l'utiliser massivement. Cela peut prendre des années avant que beaucoup de code n'apparaisse en utilisant une nouvelle boucle. De plus, de nombreuses erreurs doivent être commises, puis elles doivent être corrigées afin que l'algorithme puisse remarquer le modèle dans les modifications.

Combien d'années devraient s'écouler? Cinq? Dix?

Dix, c'est trop, et sommes-nous pessimistes? Pas du tout. Huit ans se sont écoulés au moment où cet article a été écrit, car la boucle basée sur la plage est apparue en C ++ 11. Mais jusqu'à présent, seuls trois cas d' une telle erreur ont été enregistrés dans notre base de données. Trois erreurs, ce n'est pas beaucoup et pas peu. Aucune conclusion ne doit être tirée de leur nombre. L'essentiel est que vous puissiez confirmer qu'un tel modèle d'erreur est réel et qu'il est logique de le détecter.

Comparez maintenant ce montant, par exemple, avec ce modèle d'erreur: le pointeur est déréférencé avant la vérification . Au total, lors de la vérification des projets open-source, nous avons déjà identifié 1716 de ces cas.

Peut-être que vous ne devriez pas du tout chercher des erreurs de boucle basées sur la plage? Non. Seuls les programmeurs sont inertiels, et cet opérateur gagne très lentement en popularité. Progressivement, il y aura beaucoup de code avec sa participation, et, par conséquent, il y aura également plus d'erreurs.

Très probablement, cela ne se produira qu'après 10-15 ans à partir du moment où C ++ 11 est apparu. Et maintenant une question philosophique. Connaissant déjà le modèle d'erreur, nous attendrons simplement de nombreuses années jusqu'à ce que de nombreuses erreurs s'accumulent dans les projets ouverts?

Si la réponse est «oui», alors il est possible de diagnostiquer raisonnablement à tous les analyseurs sur la base du ML le diagnostic de «retard mental».

Si la réponse est non, que dois-je faire? Il n'y a pas d'exemples. Pour les écrire manuellement? Mais ensuite, nous revenons au chapitre précédent, où nous avons envisagé d'écrire à une personne de nombreux exemples d'apprentissage.

Cela peut être fait, mais encore une fois la question de l'opportunité se pose. L'implémentation des diagnostics V789 avec toutes les exceptions dans l'analyseur PVS-Studio ne comprend que 118 lignes de code, dont 13 lignes sont des commentaires. C'est-à-dire Il s'agit d'un diagnostic très simple qui peut être facilement pris et programmé de manière classique.

Une situation similaire sera avec toutes les autres innovations qui apparaissent dans toutes les autres langues. Comme on dit, il y a quelque chose à penser.

La troisième nuance. La documentation

Un élément important de tout analyseur statique est la documentation qui décrit chaque diagnostic. Sans lui, l'utilisation de l'analyseur sera extrêmement difficile, voire impossible. Dans la documentation de PVS-Studio, nous avons une description de chaque diagnostic, qui fournit un exemple de code erroné et comment le corriger. Il existe également un lien vers CWE où vous pouvez lire une autre description du problème. Et tout de même, parfois quelque chose est incompréhensible pour les utilisateurs, et ils nous posent des questions de clarification.

Dans le cas des analyseurs statiques, qui sont basés sur des algorithmes d'apprentissage automatique, le problème de documentation est en quelque sorte étouffé. Il est supposé que l'analyseur indique simplement un endroit qui lui semble suspect et peut-être même suggère comment le réparer. La décision d'apporter ou non un changement appartient à la personne. Et là ... ahem ... Ce n'est pas facile de prendre une décision, de ne pas pouvoir lire, sur la base de quoi l'analyseur semble méfiant à l'un ou l'autre endroit du code.

Bien sûr, dans certains cas, tout sera évident. Supposons que l'analyseur pointe vers ce code:

 char *p = (char *)malloc(strlen(src + 1)); strcpy(p, src); 

Et proposera de le remplacer par:

 char *p = (char *)malloc(strlen(src) + 1); strcpy(p, src); 

Il est immédiatement clair que le programmeur a scellé et ajouté 1 au mauvais endroit. Par conséquent, moins de mémoire sera allouée.

Ici, sans documentation, tout est clair. Mais ce ne sera pas toujours le cas.

Imaginez que l'analyseur pointe silencieusement vers ce code:

 char check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); } 

Et suggère de changer le type de la valeur de retour de char en int:

 int check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); } 

Il n'y a aucune documentation pour l'avertissement. Et apparemment, le texte de l'avertissement lui-même, tel que nous le comprenons, ne le sera pas non plus, si nous parlons d'un analyseur complètement indépendant.

Que faire Quelle est la différence? Dois-je faire un tel remplacement?

En principe, ici, vous pouvez tenter votre chance et accepter de corriger le code. Bien qu'accepter des modifications sans les comprendre, c'est une pratique banale ... :) Vous pouvez regarder la description de la fonction memcmp et lire que la fonction retourne des valeurs int : 0 supérieures à zéro et inférieures à zéro. Mais tout de même, il n'est peut-être pas clair pourquoi apporter des modifications si le code fonctionne déjà correctement.

Maintenant, si vous ne savez pas à quoi sert une telle modification, lisez la description des diagnostics du V642 . Il devient immédiatement clair qu'il s'agit d'une véritable erreur. De plus, cela peut entraîner une vulnérabilité.

Peut-être que l'exemple ne semblait pas convaincant. Après tout, l'analyseur a proposé un code susceptible d'être meilleur. Ok Regardons un autre exemple de pseudocode, cette fois, pour un changement, en Java.

 ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj.state = 200; out.writeObject(obj); out.close(); 

Il y a une sorte d'objet. Il est sérialisé. Ensuite, l'état de l'objet change et il est à nouveau sérialisé. Tout semble aller bien. Imaginez maintenant que l'analyseur, soudain, n'aime pas ce code, et il suggère de le remplacer par:

 ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj = new SerializedObject(); //    obj.state = 200; out.writeObject(obj); out.close(); 

Au lieu de modifier l'objet et de le réenregistrer, un nouvel objet est créé et il est déjà sérialisé.

Il n'y a pas de description du problème. Pas de documentation. Le code est devenu plus long. Pour une raison quelconque, la création d'un nouvel objet a été ajoutée. Êtes-vous prêt à effectuer une telle modification dans votre code?

Vous direz que ce n'est pas clair. En effet, ce n'est pas clair. Et donc ce sera tout le temps incompréhensible. Travailler avec un tel analyseur "silencieux" sera une étude sans fin pour tenter de comprendre pourquoi l'analyseur n'aime pas quelque chose.

S'il y a de la documentation, alors tout devient transparent. La classe java.io.ObjectOuputStream , qui est utilisée pour la sérialisation, met en cache les objets accessibles en écriture. Cela signifie que le même objet ne sera pas sérialisé deux fois. Une fois que la classe sérialise l'objet, et la deuxième fois, elle écrit simplement un lien vers le même premier objet dans le flux. En savoir plus: V6076 - La sérialisation récurrente utilisera l'état d'objet mis en cache à partir de la première sérialisation.

Nous espérons avoir pu expliquer l’importance de la documentation. Et maintenant la question. Comment la documentation d'un analyseur basé sur ML apparaîtra-t-elle?

Lorsqu'un analyseur de code classique est développé, tout est simple et clair. Il y a un certain schéma d'erreurs. Nous le décrivons dans la documentation et implémentons les diagnostics.

Dans le cas de ML, l'inverse est vrai. Oui, l'analyseur peut remarquer une anomalie dans le code et la signaler. Mais il ne sait rien de l'essence du défaut. Il ne comprend pas et ne dira pas pourquoi le code ne peut pas être écrit comme ça. Ce sont des abstractions de trop haut niveau. Ensuite, l'analyseur doit également apprendre à lire et à comprendre la documentation des fonctions.

Comme je l'ai dit, étant donné que le sujet de la documentation est traité dans des articles sur l'apprentissage automatique, nous ne sommes pas prêts à en parler davantage. Juste une autre grande nuance que nous avons apportée pour examen.

Remarque On peut faire valoir que la documentation est facultative. L'analyseur peut se référer à de nombreux exemples de correctifs sur GitHub et une personne, en regardant les validations et les commentaires à leur sujet, trouvera ce qui est quoi. Oui. Mais l'idée n'a pas l'air attrayante. Au lieu d'un assistant, l'analyseur agit comme un outil qui va encore plus troubler le programmeur.

La quatrième nuance. Langues hautement spécialisées.

L'approche décrite n'est pas applicable aux langages hautement spécialisés pour lesquels l'analyse statique peut également être extrêmement utile. La raison en est que GitHub et d'autres sources n'ont tout simplement pas une base de code source suffisamment grande pour fournir une formation efficace.

Considérez ceci avec un exemple spécifique. Pour commencer, accédez à GitHub et recherchez des référentiels pour le langage Java populaire.

Résultat: langue: «Java»: 3 128 884 résultats de référentiel disponibles

Prenons maintenant le langage spécialisé "1C Enterprise" utilisé dans les applications comptables publiées par la société russe 1C .

Résultat: langue: «1C Enterprise»: 551 résultats de référentiel disponibles

Peut-être que des analyseurs pour cette langue ne sont pas nécessaires? Sont nécessaires. Il existe un besoin pratique pour l'analyse de tels programmes, et des analyseurs correspondants existent déjà. Par exemple, il existe un plug-in SonarQube 1C (BSL) fabriqué par Silver Bullet .

, - , .

. C, C++, #include .

, ML, , Java, JavaScript, Python. . C C++ - , .

, /, , C C++ . «» .

c/cpp- . , GitHub, - cpp- . , ML.

, . GitHub . , . , . , .cpp- .

. . . , , . .

. :

 bool Class::IsMagicWord() { return m_name == "ML"; } 

:

 bool Class::IsMagicWord() { return strcmp(m_name, "ML") == 0; } 

, (x == «y») strcmp(x, «y»)?

, , m_name . , , :

 class Class { .... char *m_name; }; class Class { .... std::string m_name; }; 

, . , ( std::string ).

, , .h . , . , C C++.

- , , , C C++.

, . , , . , cpp-.

. (, , ). , . , , .



, GitHub . , , . - . - , . . « ». , , .cpp (.i) . .

, , , . , . . , - , , .

, . . C C++ , GitHub, . , .

Remarque , . GitHub C++ , .cpp . :).

, C C++ .

. .

, .

V789 , Range-based for loop. , , . , , , . , :

 std::vector<int> numbers; .... for (int num : numbers) { if (num < 5) { numbers.push_back(0); break; // , , return } } 

, . . PVS-Studio 26 .

, . , , , .

, . , , , ML. C'est-à-dire .

. .

, . (, WinAPI, ..).

C, strcmp , . GitHub, available code results:

  • strcmp — 40,462,158
  • stricmp — 1,256,053

, . , , , :
  • , . .
  • , NULL. .
  • , . .
  • Et ainsi de suite.

? Non. « ». « » . Top50 . , , , 100 , . , , , . , - Amazon.com , 130 « ».

. , . , :

  • g_ascii_strncasecmp — 35,695
  • lstrcmpiA — 27,512
  • _wcsicmp_l — 5,737
  • _strnicmp_l — 5,848
  • _mbscmp_l — 2,458
  • etc.

, , . . . , , . « ».

PVS-Studio . , C ++ 7200 . :

  • WinAPI
  • C,
  • (STL),
  • glibc (GNU C Library)
  • Qt
  • MFC
  • zlib
  • libpng
  • OpenSSL
  • etc.

, . . , .

. ML? , .

, , ML, . , . , strcmp malloc .

C . , . , , , .

, _fread_nolock . , , fread . . , . , . :

 int buffer[10]; size_t n = _fread_nolock(buffer, size_of(int), 100, stream); 

PVS-Studio:

 C_"size_t _fread_nolock" "(void * _DstBuf, size_t _ElementSize, size_t _Count, FILE * _File);" ADD(HAVE_STATE | RET_SKIP | F_MODIFY_PTR_1, nullptr, nullptr, "_fread_nolock", POINTER_1, BYTE_COUNT, COUNT, POINTER_2). Add_Read(from_2_3, to_return, buf_1). Add_DataSafetyStatusRelations(0, 3); 

, , , , . , write-only . . .

ML. GitHub . 15000 . . :

 #define fread_unlocked _fread_nolock 

?

  1. . .
  2. , , . , , . .
  3. , , . , . ML :). .

, ML .

, ML, , , , . , , , .

. , , WinAPI. , , ? , Google, , . , . _fread_nolock , . , , C++. , 20.

, . , memmove . - :

 void *memmove (void *dest, const void *src, size_t len) { return __builtin___memmove_chk(dest, src, len, __builtin_object_size(dest, 0)); } 

__builtin___memmove_chk ? intrinsic , . .

memmove - : . , - .

Ok, . , . , ML , , .

. . . , , . , AI? , AI , . , . , 20 .



, , . . , .
  • . , , . , - . Un exemple. C++ auto_ptr . unique_ptr .
  • . , C C++ , . , . , . , long Windows 32/64 32 . Linux 32/64 . , . -. , , . , ( ). , .
  • . ML, , , . C'est-à-dire , — , , . , . , , — , . . , / , , , . . : " PVS-Studio: ". , , .

Conclusions


, , . ML , , ( ) . , , ML .

, , ML. , , .

, ML . , . ML «» .

, , , , .

ML, .

PS


, - , ML, .

Licornes luddites


, . PVS-Studio. ML. , . , , , if- :). , :).

, -, .

Merci de votre attention. " PVS-Studio ".



, : Andrey Karpov, Victoria Khanieva. Machine Learning in Static Analysis of Program Source Code .

Source: https://habr.com/ru/post/fr484208/


All Articles