Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

CoqBlog

.NET is good :-)
{ Blog de coq }

Actualités

Dépassements de capacité en C# : la valeur par défaut de "Check for arithmetic overflow/underflow" pourrait vous surprendre

Contrairement à une idée qui semble répandue, pour les opérations sur des valeurs entières non constantes l'option "Check for arithmetic overflow/underflow" est désactivée par défaut : les dépassements de capacité ne seront pas signalés à l'exécution du code, et ne le seront à la compilation que pour les valeurs constantes. Les opérations et conversions s'exécutent donc par défaut dans un contexte unchecked.
Du moins c'est le cas dans l'environnement de développement Visual Studio et en entrée de csc.exe, ça ne l'est peut-être pas pour d'autres environnements de build ou d'autres compilateurs.

Le contexte de rédaction de cet article est Visual Studio 2010 et .NET 4.0, il se peut que la situation change pour des versions ultérieures de ces produits (même si je n'y crois pas trop).

 

2 cas à distinguer : les opérations/conversions sur valeurs constantes, et les autres

Pour les opérations sur valeurs constantes, c'est-à-dire celles pour lesquelles les valeurs sont connues au moment de la compilation, le compilateur émettra par défaut l'erreur "The operation overflows at compile time in checked mode" s'il détecte une opération qui donnerait lieu à un dépassement de capacité et ce quel que soit le contexte de vérification.

Ainsi le code suivant ne compilera pas :

Int32 sum = Int32.MaxValue + 1;

Il nécessitera l'utilisation du mot clé unchecked pour déclarer explicitement que le dépassement de capacité est voulu pour un bloc de code :

Int32 sum;
unchecked
{
    sum = Int32.MaxValue + 1;
}

ou pour une expression :

Int32 sum = unchecked(Int32.MaxValue + 1);

A noter que le code suivant, malgré le fait qu'on utilise la même constante, compilera sans problème avec la version actuelle du compilateur C# (et produira un résultat erroné sans levée d'exception s'il est compilé dans un contexte unchecked) :

Int32 max = Int32.MaxValue;
Int32 sum = max + 1;

 

Pour les conversions de valeurs constantes, le compilateur émettra aussi une erreur.
Par exemple les codes suivant provoqueront la levée d'une erreur "Constant value '9223372036854775807' cannot be converted to a 'int' (use 'unchecked' syntax to override)" et "Constant value '-1' cannot be converted to a 'uint' (use 'unchecked' syntax to override)" :

Int32 value = (Int32)Int64.MaxValue;
UInt32 value = (UInt32)(-1);

Là encore, remplacer l'utilisation directe de la constante de l'opération par une variable contenant sa valeur permettra de "tromper" le compilateur (et produira aussi un résultat erroné sans levée d'exception s'il est compilé dans un contexte unchecked) :

Int64 largeValue = Int64.MaxValue;
Int32 value = (Int32)largeValue;
Int32 negativeValue = -1;
UInt32 value = (UInt32)negativeValue;

 

Pour les opérations/conversions sur valeurs non constantes la vérification des dépassements de capacité est désactivée par défaut : pas d'erreur à la compilation, pas d'exception System.OverflowException à l'exécution.

 

 

L'obtention de  valeurs erronées est un problème, mais ce n'est pas le seul

Même si l'obtention de valeurs erronées peut se révéler être un gros problème, il y a un autre aspect à ne pas négliger avec ce problème de dépassement de capacité : la boucle infinie.
Qui n'a pas un jour été confronté à une boucle infinie en voulant parcourir la plage de valeurs complète d'un type entier via une boucle ?

Que ce soit un en utilisant "for"

// NE PAS UTILISER CECI (boucle infinie si utilisé dans un contexte unchecked)
// DO NOT USE THAT (infinite loop if used in an unchecked context)
for (Byte currentByte = Byte.MinValue; currentByte <= Byte.MaxValue; currentByte++)
{
    // ...
}

ou "while" / "do...while"

// NE PAS UTILISER CECI (boucle infinie si utilisé dans un contexte unchecked)
// DO NOT USE THAT (infinite loop if used in an unchecked context)
Byte currentByte = Byte.MinValue;
do
{
    // ...
    currentByte++;
} while (currentByte <= Byte.MaxValue);

 

Ca peut-être amusant quand on s'en aperçoit durant les tests (on met ça sur le coup de la fatigue), mais ça l'est beaucoup moins quand on ne s'en aperçoit pas et qu'au lieu du type de code donné en exemple ci-dessus on se trouve en présence de l'utilisation d'une valeur externe pour définir les bornes de la boucle.
Comme par exemple avec une méthode de ce genre (qui reste un exemple bateau avec une implémentation naïve mais cependant probable) :

public class Result
{
    public Boolean OperationSucceeded { get; internal set; }
    public Object OperationResult { get; internal set; }
    public Int32 TriesCount { get; internal set; }
}

public Result TryNTimes(Int32 maxTries)
{
    if (maxTries <= 0)
    {
        throw new ArgumentException("...");
    }

    Result result = new Result();
    Boolean operationSucceeded = false;

    Int32 currentTryNumber = 1;
    do
    {
        // ...

        if (operationSucceeded == true)
        {
            // ...
        }
        else
        {
            currentTryNumber++;
        }
    } while (operationSucceeded == false && currentTryNumber <= maxTries);

    result.OperationSucceeded = operationSucceeded;
    result.TriesCount = currentTryNumber;

    return result;
}

Si la valeur Int32.MaxValue est passée à la méthode TryNTimes, le bloc "do...while" de cette dernière part dans une boucle infinie.
Si les tests ne couvrent pas les bornes extrêmes ça peut s'avérer plutôt dangereux, surtout si les données proviennent de l'extérieur et que les opérations effectuées ne peuvent être couvertes par un timeout : on ouvre la voie à une attaque par déni de service.
Et quand je dis "proviennent de l'extérieur" je parle à la fois des spécifications directes (valeur entière passée en paramètre, dans un fichier etc) ou indirectes (nombre d'octets d'un fichier, etc). Si nous n'utilisons pas directement les valeurs mais effectuons des calculs avec, il se peut aussi que l'attaquant s'arrange pour obtenir la valeur fatidique en sortie de calcul : toujours partir du principe qu'il aura plus d'imagination que nous.

 

 

Les options pour activer la vérification

Avant toute chose, une information qu'il faut absolument garder à l'esprit : l'activation de la vérification des dépassements de capacité est une option qui agit au moment de la compilation (C# vers IL) et non pas à l'exécution.
Il n'y a, à ma connaissance, pas d'équivalent de cette option qui soit activable au moment de la compilation JIT (dans le cas contraire la prise en charge devrait descendre jusqu'à ngen pour que les images générées prennent en charge les 2 cas) : référencer un assembly depuis un autre pour lequel la vérification des dépassements de capacité aura été activée de manière globale ne provoquera pas l'activation de la vérification dans le code de l'assembly référencé.

Il existe en gros 2 manières de changer le contexte de vérification : un qui aura une portée projet, l'autre qui agira de manière plus fine au niveau d'une ou plusieurs expressions.

 

Portée projet : les options de projet Visual Studio / du compilateur csc

Pour activer la vérification de dépassement de capacité pour tout un projet, il suffit de cocher l'option "Check for arithmetic overflow/underflow" dans les propriétés du projet :

Capture d'écran de la fenêtre d'options de build d'un projet C# dans Visual Studio

Dans la majorité des cas on voudra sans doute avoir le même paramétrage sur toutes les configurations de build de la solution, et dans ce cas il suffit de veiller à sélectionner "All Configurations" avant de procéder au changement de valeur.
Pour les cas où on désire n'activer la vérification des dépassements que sur les builds de debug et de test, il faudra alors passer par une affectation plus fine des valeurs.

Cette option changera la façon dont Visual Studio/MSBuild gèrera la valeur de l'option /checked lors de l'appel à csc.exe.

 

Portée plus fine : les mots clés checked / unchecked

Le mot clé checked permet de placer une expression, ou l'ensemble des expressions d'un bloc de code, dans un contexte checked : en cas de dépassement de capacité une exception OverflowException sera levée.
Le mot clé unchecked permet à l'inverse de placer une expression, ou l'ensemble des expressions d'un bloc de code, dans un contexte unchecked : en cas de dépassement de capacité aucune exception ne sera levée.

Il est important de bien noter au moins 2 points sur ces mots clés :

  • Ils surchargent les options définies pour le projet : il est ainsi tout à fait possible d'effectuer des opérations sans vérification de dépassement de capacité dans un projet pour lequel elles sont activées (et vice versa).
    En cas d'imbrication de blocs checked/unchecked autour d'une expression, c'est le contexte le plus profond (celui le plus proche de l'expression) qui sera appliqué.
  • Leur effet ne s'applique qu'aux expressions de leur bloc et ne s'étend pas à celles des méthodes appelées au sein de ce bloc, même si ces dernières font partie du même assembly : pour ces méthodes c'est le contexte qu'elles définissent qui s'appliquera, ou à défaut celui défini pour le projet.

 

 

Impact sur les performances

A ma connaissance le contexte de vérification ne donne pas lieu à autre chose qu'un choix différent au niveau de l'instruction IL qui va effectuer l'opération demandée :

  • Addition
    • add
    • add.ovf
    • add.ovf.un
  • Conversion vers Int32
    • conv.i4
    • conv.ovf.i4
    • conv.ovf.i4.un
  • ...

Une recherche du terme "ovf" sur la liste des OpCodes disponibles lors de l'émission de code donne un aperçu des possibilités.

 

Par exemple dans le cadre d'une addition de 2 entiers signés :

public Int32 Sum(Int32 value1, Int32 value2)
{
    return value1 + value2;
}

Cette méthode compilée dans un contexte unchecked donnera le code IL suivant :

.method public hidebysig instance int32 
        Sum(int32 value1,
           
int32 value2) cil managed
{
  // Code size       4 (0x4)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  ldarg.2
  IL_0002:  add
  IL_0003:  ret
}

Alors que compilée dans un contexte checked elle donnera plutôt ceci :

.method public hidebysig instance int32 
        Sum(int32 value1,
            int32 value2) cil managed
{
  // Code size       4 (0x4)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  ldarg.2
  IL_0002:  add.ovf
  IL_0003:  ret
}

 

Pour une addition de 2 entiers non signés :

public UInt32 Sum(UInt32 value1, UInt32 value2)
{
    return value1 + value2;
}

Cette méthode compilée dans un contexte unchecked donnera le code IL suivant :

.method public hidebysig instance uint32
        Sum(uint32 value1,
            uint32 value2) cil managed
{
  // Code size       4 (0x4)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  ldarg.2
  IL_0002:  add
  IL_0003:  ret
}

Alors que compilée dans un contexte checked elle donnera plutôt ceci :

.method public hidebysig instance uint32
        Sum(uint32 value1,
            uint32 value2) cil managed
{
  // Code size       4 (0x4)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  ldarg.2
  IL_0002:  add.ovf.un
  IL_0003:  ret
}

 

Rien n'est gratuit : l'activation de la vérification des overflows a fatalement un coût.
Il faut mesurer les différences dans le contexte de chaque projet, avec la configuration de build finale (optimisations actives ou non, ...) et de préférence sur la plateforme matérielle finale afin de déterminer si l'activation plus ou moins fine des vérifications apporte suffisamment de bénéfices pour justifier le surcoût.

Si le coût est inacceptable en production, un compromis pourra aussi être d'activer les vérifications sur les binaires de test mais pas sur ceux de production.

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :
Posted: mercredi 12 janvier 2011 00:37 par coq
Classé sous : ,

Commentaires

Pas de commentaires

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01