web-dev-qa-db-fra.com

Pourquoi les entiers non signés sont-ils sujets aux erreurs?

Je regardais cette vidéo . Bjarne Stroustrup dit que les entiers non signés sont sujets aux erreurs et entraînent des bugs. Vous ne devez donc les utiliser que lorsque vous en avez vraiment besoin. J'ai également lu dans l'une des questions sur Stack Overflow (mais je ne me souviens pas laquelle) que l'utilisation d'entiers non signés peut entraîner des bogues de sécurité .

Comment conduisent-ils à des bugs de sécurité? Quelqu'un peut-il l'expliquer clairement en donnant un exemple approprié?

60
Destructor

Un aspect possible est que les entiers non signés peuvent entraîner des problèmes quelque peu difficiles à repérer dans les boucles, car le dépassement de capacité conduit à de grands nombres. Je ne peux pas compter (même avec un entier non signé!) Combien de fois j'ai fait une variante de ce bug

for(size_t i = foo.size(); i >= 0; --i)
    ...

Notez que, par définition, i >= 0 Est toujours vrai. (Ce qui provoque cela en premier lieu, c'est que si i est signé, le compilateur avertira d'un éventuel débordement avec le size_t De size()).

Il y a d'autres raisons mentionnées Danger - types non signés utilisés ici! , dont la plus forte, à mon avis, est la conversion de type implicite entre signé et non signé.

49
Ami Tavory

Un facteur important est qu'il rend la logique de boucle plus difficile: imaginez que vous voulez itérer sur tout sauf le dernier élément d'un tableau (ce qui se produit dans le monde réel). Vous écrivez donc votre fonction:

void fun (const std::vector<int> &vec) {
    for (std::size_t i = 0; i < vec.size() - 1; ++i)
        do_something(vec[i]);
}

Ça a l'air bien, non? Il compile même proprement avec des niveaux d'avertissement très élevés! ( Live ) Donc, vous mettez cela dans votre code, tous les tests se déroulent bien et vous l'oubliez.

Maintenant, plus tard, quelqu'un arrive et passe un vector vide à votre fonction. Maintenant, avec un entier signé, vous auriez, espérons-le, remarqué le avertissement du compilateur de comparaison de signes , introduit la distribution appropriée et n'avez pas publié le code du buggy en premier lieu.

Mais dans votre implémentation avec l'entier non signé, vous encapsulez et la condition de boucle devient i < SIZE_T_MAX. Catastrophe, UB et crash le plus probable!

Je veux savoir comment ils conduisent à des bugs de sécurité?

C'est aussi un problème de sécurité, en particulier c'est un buffer overflow . Une façon d'exploiter éventuellement cela serait si do_something ferait quelque chose qui peut être observé par l'attaquant. Il pourrait peut-être trouver quelle entrée est entrée dans do_something, et de cette façon, les données auxquelles l'attaquant ne devrait pas pouvoir accéder seraient divulguées de votre mémoire. Ce serait un scénario similaire au bug Heartbleed . (Merci à un monstre à cliquet de l'avoir souligné dans son commentaire .)

36
Baum mit Augen

Je ne vais pas regarder une vidéo juste pour répondre à une question, mais un problème est les conversions déroutantes qui peuvent se produire si vous mélangez des valeurs signées et non signées. Par exemple:

#include <iostream>

int main() {
    unsigned n = 42;
    int i = -42;
    if (i < n) {
        std::cout << "All is well\n";
    } else {
        std::cout << "ARITHMETIC IS BROKEN!\n";
    }
}

Les règles de promotion signifient que i est converti en unsigned pour la comparaison, donnant un grand nombre positif et un résultat surprenant.

23
Mike Seymour

Le gros problème avec un entier non signé est que si vous soustrayez 1 d'un entier non signé 0, le résultat n'est pas un nombre négatif, le résultat n'est pas inférieur au nombre avec lequel vous avez commencé, mais le résultat est la plus grande valeur entière non signée possible .

unsigned int x = 0;
unsigned int y = x - 1;

if (y > x) printf ("What a surprise! \n");

Et c'est ce qui rend les erreurs int non signées sujettes. Bien sûr, un entier non signé fonctionne exactement comme il est conçu pour fonctionner. C'est absolument sûr si vous savez ce que vous faites et ne faites aucune erreur. Mais la plupart des gens font des erreurs.

Si vous utilisez un bon compilateur, vous activez tous les avertissements générés par le compilateur et il vous avertira lorsque vous effectuez des choses dangereuses susceptibles d’être des erreurs.

4
gnasher729

Le problème avec les types entiers non signés est qu'en fonction de leur taille, ils peuvent représenter deux choses différentes:

  1. Types non signés inférieurs à int (par exemple uint8) hold nombres dans la plage 0..2ⁿ-1, et les calculs avec eux se comporteront selon les règles de l'arithmétique des nombres entiers à condition qu'ils ne dépassent pas la plage de int type. Selon les règles actuelles, si un tel calcul dépasse la plage d'un int, un compilateur est autorisé à faire tout ce qu'il veut avec le code, allant même jusqu'à nier les lois du temps et de la causalité (certains compilateurs faites exactement cela!), et même si le résultat du calcul est affecté à un type non signé plus petit que int.
  2. Types non signés unsigned int et des membres de maintien plus grands de l'anneau algébrique enveloppant abstrait d'entiers mod 2ⁿ congruent; cela signifie effectivement que si un calcul sort de la plage 0..2ⁿ-1, le système ajoutera ou soustraira le multiple de 2ⁿ qui serait nécessaire pour remettre la valeur dans la plage.

Par conséquent, étant donné uint32_t x=1, y=2; l'expression x-y peut avoir l'une des deux significations selon que int est supérieur à 32 bits.

  1. Si int est supérieur à 32 bits, l'expression soustrait le nombre 2 du nombre 1, ce qui donne le nombre -1. Notez que bien qu'une variable de type uint32_t ne peut pas contenir la valeur -1 quelle que soit la taille de int, et le stockage de -1 entraînerait une telle variable pour contenir 0xFFFFFFFF, mais à moins ou jusqu'à ce que la valeur soit forcée à un type non signé, il le fera se comportent comme la quantité signée -1.
  2. Si int est de 32 bits ou moins, l'expression donnera un uint32_t valeur qui, ajoutée à uint32_t valeur 2, donnera uint32_t valeur 1 (c'est-à-dire le uint32_t valeur 0xFFFFFFFF).

À mon humble avis, ce problème pourrait être résolu proprement si C et C++ devaient définir de nouveaux types non signés [par ex. unum32_t et uwrap32_t] de telle sorte qu'un unum32_t se comporterait toujours comme un nombre, quelle que soit la taille de int (nécessitant éventuellement l'opération de droite d'une soustraction ou unaire moins pour être promu au prochain type signé plus grand si int est de 32 bits ou moins), tandis qu'un wrap32_t se comporterait toujours comme un membre d'un anneau algébrique (blocage des promotions même si int était supérieur à 32 bits). En l'absence de tels types, cependant, il est souvent impossible d'écrire du code qui est à la fois portable et propre, car le code portable nécessitera souvent des contraintes de type partout.

2
supercat

Les règles de conversion numérique en C et C++ sont un gâchis byzantin. L'utilisation de types non signés vous expose beaucoup plus à ce gâchis que l'utilisation de types purement signés.

Prenons par exemple le cas simple d'une comparaison entre deux variables, l'une signée et l'autre non signée.

  • Si les deux opérandes sont plus petits que int, ils seront tous deux convertis en int et la comparaison donnera des résultats numériquement corrects.
  • Si l'opérande non signé est plus petit que l'opérande signé, les deux seront convertis en type d'opérande signé et la comparaison donnera des résultats numériquement corrects.
  • Si l'opérande non signé est supérieur ou égal à la taille de l'opérande signé et également supérieur ou égal à la taille int, les deux seront convertis en type d'opérande non signé. Si la valeur de l'opérande signé est inférieure à zéro, cela entraînera des résultats numériquement incorrects.

Pour prendre un autre exemple, envisagez de multiplier deux entiers non signés de la même taille.

  • Si la taille de l'opérande est supérieure ou égale à la taille de int, la multiplication aura une sémantique enveloppante définie.
  • Si la taille de l'opérande est inférieure à int mais supérieure ou égale à la moitié de la taille de int, il existe un risque de comportement indéfini.
  • Si la taille de l'opérande est inférieure à la moitié de la taille de int, la multiplication produira des résultats numériquement corrects. Réaffecter ce résultat à une variable du type non signé d'origine produira une sémantique enveloppante définie.
2
plugwash