Présentation de SAL

Le langage d’annotation de code source (SAL) Microsoft fournit un ensemble d’annotations que vous pouvez utiliser pour décrire comment une fonction utilise ses paramètres, les hypothèses qu’elle fait à leur sujet et les garanties qu’elle apporte quand elle se termine. Les annotations sont définies dans le fichier <sal.h>d’en-tête . L’analyse du code Visual Studio pour C++ utilise des annotations SAL pour modifier son analyse des fonctions. Pour plus d’informations sur sal 2.0 pour le développement de pilotes Windows, consultez les annotations SAL 2.0 pour les pilotes Windows.

En mode natif, C et C++ fournissent uniquement des moyens limités pour les développeurs d’exprimer constamment l’intention et l’invariance. En utilisant des annotations SAL, vous pouvez décrire vos fonctions plus en détail afin que les développeurs qui les consomment puissent mieux comprendre comment les utiliser.

Qu’est-ce que SAL et pourquoi devez-vous l’utiliser ?

Tout simplement indiqué, SAL est un moyen peu coûteux de laisser le compilateur case activée votre code pour vous.

SAL rend le code plus précieux

SAL peut vous aider à rendre votre conception de code plus compréhensible, tant pour les humains que pour les outils d’analyse du code. Prenons cet exemple qui montre la fonction memcpyruntime C :

void * memcpy(
   void *dest,
   const void *src,
   size_t count
);

Pouvez-vous dire ce que fait cette fonction ? Lorsqu’une fonction est implémentée ou appelée, certaines propriétés doivent être conservées pour garantir l’exactitude du programme. En examinant une déclaration telle que celle de l’exemple, vous ne savez pas ce qu’elles sont. Sans annotations SAL, vous devrez vous appuyer sur la documentation ou les commentaires de code. Voici ce que la documentation pour memcpy dire :

«memcpy copie le nombre d’octets de src à dest ; wmemcpy copie le nombre de caractères larges (deux octets). Si la source et la destination se chevauchent, le comportement de memcpy n'est pas défini. Utilisez memmove pour gérer les régions qui se chevauchent.
Important : assurez-vous que la mémoire tampon de destination est de la même taille ou supérieure à la mémoire tampon source. Pour plus d’informations, consultez Éviter les dépassements de mémoire tampon. »

La documentation contient quelques bits d’informations qui suggèrent que votre code doit conserver certaines propriétés pour garantir l’exactitude du programme :

  • memcpy copie les count octets de la mémoire tampon source vers la mémoire tampon de destination.

  • La mémoire tampon de destination doit être au moins aussi grande que la mémoire tampon source.

Toutefois, le compilateur ne peut pas lire la documentation ni les commentaires informels. Il ne sait pas qu’il existe une relation entre les deux mémoires tampons et count, et il ne peut pas non plus deviner efficacement une relation. SAL peut fournir plus de clarté sur les propriétés et l’implémentation de la fonction, comme illustré ici :

void * memcpy(
   _Out_writes_bytes_all_(count) void *dest,
   _In_reads_bytes_(count) const void *src,
   size_t count
);

Notez que ces annotations ressemblent aux informations de la documentation, mais elles sont plus concises et suivent un modèle sémantique. Lorsque vous lisez ce code, vous pouvez rapidement comprendre les propriétés de cette fonction et comment éviter les problèmes de sécurité de dépassement de mémoire tampon. Mieux encore, les modèles sémantiques fournis par SAL peuvent améliorer l’efficacité et l’efficacité des outils d’analyse de code automatisé lors de la découverte précoce des bogues potentiels. Imaginez que quelqu’un écrit cette implémentation buggy de wmemcpy:

wchar_t * wmemcpy(
   _Out_writes_all_(count) wchar_t *dest,
   _In_reads_(count) const wchar_t *src,
   size_t count)
{
   size_t i;
   for (i = 0; i <= count; i++) { // BUG: off-by-one error
      dest[i] = src[i];
   }
   return dest;
}

Cette implémentation contient une erreur courante off-by-one. Heureusement, l’auteur du code incluait l’annotation de taille de mémoire tampon SAL : un outil d’analyse du code pouvait intercepter le bogue en analysant cette fonction seule.

Notions de base de SAL

SAL définit quatre types de paramètres de base, classés par modèle d’utilisation.

Catégorie Annotation de paramètre Description
Entrée à la fonction appelée _In_ Les données sont transmises à la fonction appelée et sont traitées en lecture seule.
Entrée à la fonction appelée et sortie à l’appelant _Inout_ Les données utilisables sont transmises à la fonction et potentiellement modifiées.
Sortie vers l’appelant _Out_ L’appelant fournit uniquement de l’espace pour que la fonction appelée écrive. La fonction appelée écrit des données dans cet espace.
Sortie du pointeur vers l’appelant _Outptr_ Comme la sortie pour l’appelant. La valeur retournée par la fonction appelée est un pointeur.

Ces quatre annotations de base peuvent être rendues plus explicites de différentes façons. Par défaut, les paramètres de pointeur annotés sont supposés être requis. Ils doivent être non NULL pour que la fonction réussisse. La variante la plus couramment utilisée des annotations de base indique qu’un paramètre de pointeur est facultatif, s’il est NULL, la fonction peut toujours réussir à effectuer son travail.

Ce tableau montre comment faire la distinction entre les paramètres obligatoires et facultatifs :

Les paramètres sont requis Les paramètres sont facultatifs
Entrée à la fonction appelée _In_ _In_opt_
Entrée à la fonction appelée et sortie à l’appelant _Inout_ _Inout_opt_
Sortie vers l’appelant _Out_ _Out_opt_
Sortie du pointeur vers l’appelant _Outptr_ _Outptr_opt_

Ces annotations permettent d’identifier les valeurs non initialisées possibles et le pointeur Null non valide utilise de manière formelle et précise. La transmission de LA valeur NULL à un paramètre requis peut entraîner un blocage, ou un code d’erreur « échec » peut être retourné. De l’une ou l’autre manière, la fonction ne peut pas réussir à accomplir son travail.

Exemples SAL

Cette section présente des exemples de code pour les annotations SAL de base.

Utilisation de l’outil d’analyse visual Studio Code pour rechercher des défauts

Dans les exemples, l’outil Visual Studio Code Analysis est utilisé avec des annotations SAL pour rechercher des défauts de code. Voici comment procéder.

Pour utiliser les outils d’analyse de code Visual Studio et sal

  1. Dans Visual Studio, ouvrez un projet C++ qui contient des annotations SAL.

  2. Dans la barre de menus, choisissez Générer, Exécuter l’analyse du code sur la solution.

    Considérez l’exemple _In_ dans cette section. Si vous exécutez l’analyse du code dessus, cet avertissement s’affiche :

    La valeur de paramètre non valide C6387 'pInt' peut être '0' : cela ne respecte pas la spécification de la fonction 'InCallee'.

Exemple : annotation _In_

L’annotation _In_ indique que :

  • Le paramètre doit être valide et ne sera pas modifié.

  • La fonction lit uniquement à partir de la mémoire tampon à élément unique.

  • L’appelant doit fournir la mémoire tampon et l’initialiser.

  • _In_ spécifie « en lecture seule ». Une erreur courante consiste à s’appliquer _In_ à un paramètre qui doit avoir l’annotation à la _Inout_ place.

  • _In_ est autorisé mais ignoré par l’analyseur sur les scalaires non pointeurs.

void InCallee(_In_ int *pInt)
{
   int i = *pInt;
}

void GoodInCaller()
{
   int *pInt = new int;
   *pInt = 5;

   InCallee(pInt);
   delete pInt;
}

void BadInCaller()
{
   int *pInt = NULL;
   InCallee(pInt); // pInt should not be NULL
}

Si vous utilisez Visual Studio Code Analysis sur cet exemple, il valide que les appelants passent un pointeur non Null à une mémoire tampon initialisée pour pInt. Dans ce cas, pInt le pointeur ne peut pas être NULL.

Exemple : annotation _In_opt_

_In_opt_est identique à _In_, sauf que le paramètre d’entrée est autorisé à être NULL et, par conséquent, la fonction doit case activée pour cela.

void GoodInOptCallee(_In_opt_ int *pInt)
{
   if(pInt != NULL) {
      int i = *pInt;
   }
}

void BadInOptCallee(_In_opt_ int *pInt)
{
   int i = *pInt; // Dereferencing NULL pointer 'pInt'
}

void InOptCaller()
{
   int *pInt = NULL;
   GoodInOptCallee(pInt);
   BadInOptCallee(pInt);
}

Visual Studio Code Analysis valide que la fonction case activée s pour NULL avant d’accéder à la mémoire tampon.

Exemple : annotation _Out_

_Out_ prend en charge un scénario courant dans lequel un pointeur non NULL pointant vers une mémoire tampon d’élément est passé et la fonction initialise l’élément. L’appelant n’a pas besoin d’initialiser la mémoire tampon avant l’appel ; la fonction appelée promet de l’initialiser avant de retourner.

void GoodOutCallee(_Out_ int *pInt)
{
   *pInt = 5;
}

void BadOutCallee(_Out_ int *pInt)
{
   // Did not initialize pInt buffer before returning!
}

void OutCaller()
{
   int *pInt = new int;
   GoodOutCallee(pInt);
   BadOutCallee(pInt);
   delete pInt;
}

Visual Studio Code Analysis Tool vérifie que l’appelant transmet un pointeur non NULL à une mémoire tampon et pInt que la mémoire tampon est initialisée par la fonction avant de retourner.

Exemple : annotation _Out_opt_

_Out_opt_est identique à _Out_, sauf que le paramètre est autorisé à être NULL et, par conséquent, la fonction doit case activée pour cela.

void GoodOutOptCallee(_Out_opt_ int *pInt)
{
   if (pInt != NULL) {
      *pInt = 5;
   }
}

void BadOutOptCallee(_Out_opt_ int *pInt)
{
   *pInt = 5; // Dereferencing NULL pointer 'pInt'
}

void OutOptCaller()
{
   int *pInt = NULL;
   GoodOutOptCallee(pInt);
   BadOutOptCallee(pInt);
}

Visual Studio Code Analysis valide que cette fonction case activée s pour NULL avant pInt d’être déréférée et, si pInt elle n’est pas NULL, que la mémoire tampon est initialisée par la fonction avant de retourner.

Exemple : annotation _Inout_

_Inout_ est utilisé pour annoter un paramètre de pointeur qui peut être modifié par la fonction. Le pointeur doit pointer vers des données initialisées valides avant l’appel, et même s’il change, il doit toujours avoir une valeur valide lors du retour. L’annotation spécifie que la fonction peut lire et écrire librement dans la mémoire tampon d’un élément. L’appelant doit fournir la mémoire tampon et l’initialiser.

Remarque

Comme _Out_, _Inout_ doit s’appliquer à une valeur modifiable.

void InOutCallee(_Inout_ int *pInt)
{
   int i = *pInt;
   *pInt = 6;
}

void InOutCaller()
{
   int *pInt = new int;
   *pInt = 5;
   InOutCallee(pInt);
   delete pInt;
}

void BadInOutCaller()
{
   int *pInt = NULL;
   InOutCallee(pInt); // 'pInt' should not be NULL
}

Visual Studio Code Analysis valide que les appelants passent un pointeur non NULL à une mémoire tampon initialisée pour pInt, et que, avant le retour, pInt n’est toujours pas NULL et que la mémoire tampon est initialisée.

Exemple : annotation _Inout_opt_

_Inout_opt_est identique à _Inout_, sauf que le paramètre d’entrée est autorisé à être NULL et, par conséquent, la fonction doit case activée pour cela.

void GoodInOutOptCallee(_Inout_opt_ int *pInt)
{
   if(pInt != NULL) {
      int i = *pInt;
      *pInt = 6;
   }
}

void BadInOutOptCallee(_Inout_opt_ int *pInt)
{
   int i = *pInt; // Dereferencing NULL pointer 'pInt'
   *pInt = 6;
}

void InOutOptCaller()
{
   int *pInt = NULL;
   GoodInOutOptCallee(pInt);
   BadInOutOptCallee(pInt);
}

Visual Studio Code Analysis valide que cette fonction case activée pour NULL avant qu’elle accède à la mémoire tampon, et si pInt elle n’est pas NULL, que la mémoire tampon est initialisée par la fonction avant de retourner.

Exemple : annotation _Outptr_

_Outptr_ est utilisé pour annoter un paramètre destiné à retourner un pointeur. Le paramètre lui-même ne doit pas être NULL, et la fonction appelée retourne un pointeur non NULL dans celui-ci et ce pointeur pointe vers des données initialisées.

void GoodOutPtrCallee(_Outptr_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 5;

   *pInt = pInt2;
}

void BadOutPtrCallee(_Outptr_ int **pInt)
{
   int *pInt2 = new int;
   // Did not initialize pInt buffer before returning!
   *pInt = pInt2;
}

void OutPtrCaller()
{
   int *pInt = NULL;
   GoodOutPtrCallee(&pInt);
   BadOutPtrCallee(&pInt);
}

Visual Studio Code Analysis valide que l’appelant passe un pointeur non NULL pour *pInt, et que la mémoire tampon est initialisée par la fonction avant de retourner.

Exemple : annotation _Outptr_opt_

_Outptr_opt_ est identique à _Outptr_, sauf que le paramètre est facultatif : l’appelant peut passer un pointeur NULL pour le paramètre.

void GoodOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 6;

   if(pInt != NULL) {
      *pInt = pInt2;
   }
}

void BadOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 6;
   *pInt = pInt2; // Dereferencing NULL pointer 'pInt'
}

void OutPtrOptCaller()
{
   int **ppInt = NULL;
   GoodOutPtrOptCallee(ppInt);
   BadOutPtrOptCallee(ppInt);
}

Visual Studio Code Analysis valide que cette fonction case activée s pour NULL avant *pInt d’être déréférée, et que la mémoire tampon est initialisée par la fonction avant de retourner.

Exemple : Annotation _Success_ en combinaison avec _Out_

Les annotations peuvent être appliquées à la plupart des objets. En particulier, vous pouvez annoter une fonction entière. L’une des caractéristiques les plus évidentes d’une fonction est qu’elle peut réussir ou échouer. Mais comme l’association entre une mémoire tampon et sa taille, C/C++ ne peut pas exprimer la réussite ou l’échec des fonctions. En utilisant l’annotation _Success_ , vous pouvez dire à quoi ressemble la réussite d’une fonction. Le paramètre de l’annotation _Success_ est simplement une expression qui, lorsqu’elle est vraie, indique que la fonction a réussi. L’expression peut être tout ce que l’analyseur d’annotation peut gérer. Les effets des annotations après la retour de la fonction ne s’appliquent que lorsque la fonction réussit. Cet exemple montre comment _Success_ interagir avec _Out_ pour faire la bonne chose. Vous pouvez utiliser le mot clé return pour représenter la valeur de retour.

_Success_(return != false) // Can also be stated as _Success_(return)
bool GetValue(_Out_ int *pInt, bool flag)
{
   if(flag) {
      *pInt = 5;
      return true;
   } else {
      return false;
   }
}

L’annotation _Out_ amène Visual Studio Code Analysis à valider que l’appelant transmet un pointeur non NULL à une mémoire tampon, pIntet que la mémoire tampon est initialisée par la fonction avant de retourner.

Bonne pratique sal

Ajout d’annotations au code existant

SAL est une technologie puissante qui peut vous aider à améliorer la sécurité et la fiabilité de votre code. Après avoir appris SAL, vous pouvez appliquer la nouvelle compétence à votre travail quotidien. Dans le nouveau code, vous pouvez utiliser des spécifications basées sur SAL par conception tout au long de ; dans le code plus ancien, vous pouvez ajouter des annotations de manière incrémentielle et ainsi augmenter les avantages chaque fois que vous mettez à jour.

Les en-têtes publics Microsoft sont déjà annotés. Par conséquent, nous vous suggérons que dans vos projets, vous annotez d’abord les fonctions de nœud feuille et les fonctions qui appellent des API Win32 pour tirer le meilleur parti.

Quand puis-je annoter ?

Voici quelques recommandations :

  • Annotez tous les paramètres de pointeur.

  • Annotez les annotations de plage de valeurs afin que l’analyse du code puisse garantir la sécurité de la mémoire tampon et du pointeur.

  • Annoter les règles de verrouillage et les effets secondaires de verrouillage. Pour plus d’informations, consultez Annoter le comportement de verrouillage.

  • Annoter les propriétés du pilote et d’autres propriétés spécifiques au domaine.

Vous pouvez également annoter tous les paramètres pour rendre votre intention claire tout au long et pour faciliter l’case activée que les annotations ont été effectuées.

Voir aussi