strcpy ()/strncpy () s'écrase sur un membre de la structure avec un espace supplémentaire lorsque L'optimisation est activée sur Unix?

en écrivant un projet, j'ai rencontré un problème étrange.

C'est le code minimal que j'ai réussi à écrire pour recréer le problème. Je stocke intentionnellement une corde réelle à la place de quelque chose d'autre, avec assez d'espace alloué.

// #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stddef.h> // For offsetof()

typedef struct _pack{
    // The type of `c` doesn't matter as long as it's inside of a struct.
    int64_t c;
} pack;

int main(){
    pack *p;
    char str[9] = "aaaaaaaa"; // Input
    size_t len = offsetof(pack, c) + (strlen(str) + 1);
    p = malloc(len);
    // Version 1: crash
        strcpy((char*)&(p->c), str);
    // Version 2: crash
        strncpy((char*)&(p->c), str, strlen(str)+1);
    // Version 3: works!
        memcpy((char*)&(p->c), str, strlen(str)+1);
    // puts((char*)&(p->c));
    free(p);
  return 0;
}

le code ci-dessus me prête à confusion:

  • avec gcc/clang -O0 , à la fois strcpy() et memcpy() fonctionne sur Linux / WSL, et le puts() ci-dessous donne ce que je saisis.
  • avec clang -O0 sur OSX , le code s'écrase avec strcpy() .
  • avec gcc/clang -O2 ou -O3 sur Ubuntu / Fedora / WSL , le code crashes (!!) à strcpy() , tandis que memcpy() fonctionne bien.
  • avec gcc.exe sur Windows, le code fonctionne bien quel que soit le niveau d'optimisation.

J'ai aussi trouvé d'autres traits du code:

  • (On dirait) l'entrée au minimum de reproduire l'erreur est de 9 octets (y compris la terminaison zéro), ou 1+sizeof(p->c) . Avec cette longueur (ou plus) un accident garanti (Cher moi ...).
  • même si j'alloue de l'espace supplémentaire (jusqu'à 1 Mo) dans malloc() , cela n'aide pas. Les comportements ci-dessus ne changent pas du tout.
  • strncpy() se comporte exactement de la même façon, même avec la longueur correcte fournie à son troisième argument.
  • le pointeur ne semble pas avoir d'importance. Si le membre de structure char *c est changé en long long c (ou int64_t ), le comportement reste le même. (Mise à jour: modification de la déjà).
  • le message d'accident n'a pas l'air régulier. Beaucoup d'informations supplémentaires sont données le long.

    crash

j'ai essayé tous ces Compilateurs et ils n'ont fait aucune différence:

  • GCC 5.4.0 (Ubuntu / Fedora / OS X/ WSL, tous 64 bits)
  • GCC 6.3.0 (Ubuntu seulement)
  • GCC 7.2.0 (Android, norepro???) (Ceci est le GCC de C4droid )
  • Clang 5.0.0 (Ubuntu/OS X)
  • MinGW GCC 6.3.0 (Windows 7/10, les deux x64)

de plus, Cette fonction de copie de chaîne personnalisée, qui ressemble exactement à la fonction standard, fonctionne bien avec n'importe quelle configuration de compilateur mentionnée ci-dessus:

char* my_strcpy(char *d, const char* s){
    char *r = d;
    while (*s){
        *(d++) = *(s++);
    }
    *d = '"151910920"';
    return r;
}

Questions:

  • pourquoi strcpy() échoue-t-il? Comment peut-il?
  • pourquoi ne fonctionne-t-il que si l'optimisation est activée?
  • pourquoi memcpy() échoue quel que soit le niveau -O ??

*si vous voulez discuter de la violation de l'accès des membres de struct, pleast head over here .


Partie de objdump -d 's la sortie de la plante, de l'exécutable (WSL):

objdump


P. Au départ, je veux écrire une structure, dont le dernier élément est un pointeur vers un espace alloué dynamiquement (pour une chaîne). Quand j'écris la structure de fichier, je ne peux pas écrire le pointeur. Je dois écrire la chaîne réelle. Alors j'ai trouvé cette solution: forcer à stocker une corde à la place d'un pointeur.

ne vous plaignez pas non plus de gets() . Je ne l'utilise pas dans mon projet, mais le code d'exemple ci-dessus seulement.

32
demandé sur iBug 2017-11-10 13:04:51

6 réponses

j'ai reproduit ce numéro sur mon Ubuntu 16.10 et j'ai trouvé quelque chose d'intéressant.

lorsqu'il est compilé avec gcc -O3 -o ./test ./test.c , le programme se plantera si l'entrée est plus longue que 8 octets.

après une certaine inversion J'ai trouvé que GCC a remplacé strcpy par memcpy_chk , voir ceci.

// decompile from IDA
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int *v3; // rbx
  int v4; // edx
  unsigned int v5; // eax
  signed __int64 v6; // rbx
  char *v7; // rax
  void *v8; // r12
  const char *v9; // rax
  __int64 _0; // [rsp+0h] [rbp+0h]
  unsigned __int64 vars408; // [rsp+408h] [rbp+408h]

  vars408 = __readfsqword(0x28u);
  v3 = (int *)&_0;
  gets(&_0, argv, envp);
  do
  {
    v4 = *v3;
    ++v3;
    v5 = ~v4 & (v4 - 16843009) & 0x80808080;
  }
  while ( !v5 );
  if ( !((unsigned __int16)~(_WORD)v4 & (unsigned __int16)(v4 - 257) & 0x8080) )
    v5 >>= 16;
  if ( !((unsigned __int16)~(_WORD)v4 & (unsigned __int16)(v4 - 257) & 0x8080) )
    v3 = (int *)((char *)v3 + 2);
  v6 = (char *)v3 - __CFADD__((_BYTE)v5, (_BYTE)v5) - 3 - (char *)&_0; // strlen
  v7 = (char *)malloc(v6 + 9);
  v8 = v7;
  v9 = (const char *)_memcpy_chk(v7 + 8, &_0, v6 + 1, 8LL); // Forth argument is 8!!
  puts(v9);
  free(v8);
  return 0;
}

votre paquet struct fait croire à GCC que l'élément c fait exactement 8 octets de long.

et memcpy_chk échoueront si la longueur de copie est plus grande que le quatrième argument!

il y a donc 2 solutions:

  • modifier votre structure

  • utilisant les options de compilation -D_FORTIFY_SOURCE=0 (aime gcc test.c -O3 -D_FORTIFY_SOURCE=0 -o ./test ) pour désactiver les fonctions de fortification.

    Caution : ceci désactivera complètement la vérification du débordement du tampon dans l'ensemble du programme!!

16
répondu Ayra Faceless 2017-11-11 13:13:24

ce que vous faites est un comportement non défini.

le compilateur est autorisé à supposer que vous n'utiliserez jamais plus de sizeof int64_t pour le membre variable int64_t c . Donc si vous essayez d'écrire plus que sizeof int64_t (alias sizeof c ) sur c , vous aurez un problème hors limite dans votre code. C'est le cas parce que sizeof "aaaaaaaa" > sizeof int64_t .

le point est, même si vous attribuez la taille de mémoire correcte en utilisant malloc() , le compilateur est autorisé à supposer que vous n'utiliserez jamais plus de sizeof int64_t dans votre appel strcpy() ou memcpy() . Parce que vous envoyez l'adresse de c (alias int64_t c ).

TL; DR: vous essayez de copier 9 octets vers un type composé de 8 octets (nous supposons qu'un octet est un octet). (De @Kcvin )

si vous voulez quelque chose de similaire utiliser les membres de réseau flexible de C99:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
  size_t size;
  char str[];
} string;

int main(void) {
  char str[] = "aaaaaaaa";
  size_t len_str = strlen(str);
  string *p = malloc(sizeof *p + len_str + 1);
  if (!p) {
    return 1;
  }
  p->size = len_str;
  strcpy(p->str, str);
  puts(p->str);
  strncpy(p->str, str, len_str + 1);
  puts(p->str);
  memcpy(p->str, str, len_str + 1);
  puts(p->str);
  free(p);
}

Note: pour un devis standard, veuillez vous référer à this answer.

29
répondu Stargateur 2017-11-11 09:31:34

aucune réponse n'a encore expliqué en détail pourquoi ce code peut être ou non un comportement non défini.

la norme n'est pas précisée dans ce domaine, et il existe une proposition active pour la fixer. En vertu de cette proposition, ce code ne serait pas un comportement non défini, et les compilateurs générant le code qui s'écrase ne se conformeraient pas à la norme mise à jour. (Je revoir ce point dans la conclusion de ma le paragraphe ci-dessous).

mais notez que basé sur le discussion de -D_FORTIFY_SOURCE=2 dans d'autres réponses, il semble que ce comportement est intentionnel de la part des développeurs.


je vais en parler fondée sur le fragment de code suivant:

char *x = malloc(9);
pack *y = (pack *)x;
char *z = (char *)&y->c;
char *w = (char *)y;

maintenant, les trois de x z w se réfèrent au même emplacement de mémoire, et auraient la même valeur et la même représentation. Mais le compilateur traite z différemment de x . (Le compilateur traite également w différemment de l'un de ces deux, bien que nous ne savons pas qui comme OP n'a pas examiné ce cas).

ce thème est appelé pointer provenance . Cela signifie la restriction sur l'objet sur lequel une valeur de pointeur peut s'étendre. Le compilateur considère z comme ayant une provenance supérieure à y->c , alors que x a une provenance sur l'ensemble de l'allocation de 9 octets.


la norme C actuelle ne précise pas très bien la provenance. Les règles telles que soustraction de pointeur ne peut se produire entre deux pointeurs pour le même objet de tableau est un exemple de règle de provenance. Une autre règle de provenance est celle qui s'applique au code dont nous discutons, C 6.5.6 / 8:

quand une expression qui a le type entier est ajoutée ou soustraite à un pointeur, le résultat a le type de le pointeur de l'opérande. Si l'opérande pointeur pointe vers un élément d'un objet array, et que le tableau est assez grand, le résultat pointe vers un élément décalé par rapport à l'élément original de telle sorte que la différence des indices des éléments résultants et originaux du tableau soit égale à l'expression entière. En d'autres termes, si l'expression P pointe vers i - e élément d'un objet de tableau, les expressions (P)+N (de manière équivalente, N+(P) ) et (P)-N (où N a la valeur n ) pointent respectivement vers les éléments i+n - th et i−n - th de l'objet array, à condition qu'ils existent. En outre, si l'expression P pointe vers le dernier élément d'un tableau d'objet, l'expression (P)+1 points l'un après le dernier élément de l'objet array, et si l'expression Q points l'un après le dernier élément d'un tableau d'objet, l'expression (Q)-1 pointe sur le dernier élément de l'objet array. Si tant l'opérande de pointeur que le point de résultat pointent vers des éléments du même objet array, ou un après le dernier élément de l'objet array, l'évaluation ne doit pas produire un débordement; sinon, le comportement est non défini. Si le résultat pointe un après le dernier élément de l'objet array, il ne doit pas être utilisé comme opérande d'un opérateur unaire * qui est évalué.

la justification de La vérification de limites de strcpy , memcpy toujours revient à cette règle - ces fonctions sont définies pour se comporter comme si elles étaient une série d'attributions de caractères à partir d'un pointeur de base qui est incrémenté pour obtenir le caractère suivant, et l'incrément d'un pointeur est couvert par (P)+1 comme discuté dans cette règle.

Notez que le terme "l'objet array" peut s'appliquer à un objet qui n'était pas déclaré comme un tableau. Cela est précisé au 6.5.6 / 7:

aux fins des présentes les opérateurs, un pointeur vers un objet qui n'est pas un élément d'un tableau se comporte comme un pointeur vers le premier élément d'un tableau de longueur avec le type de l'objet comme type d'élément.


la grande question Est la suivante: qu'est-ce que "l'objet array" ? Dans ce code, est-ce y->c , *y , ou l'objet 9 octets réel retourné par malloc?

la norme n'apporte aucune lumière sur cette question. Chaque fois que nous avons des objets avec des sous-objets, la norme ne dit pas si le 6.5.6/8 se réfère à l'objet ou au sous-objet.

un autre facteur de complication est que la norme ne fournit pas de définition pour" array " , ni pour"array object". Mais pour faire court, l'objet attribué par malloc est décrit comme" un tableau " à divers endroits dans la norme, ainsi il fait il semble que l'objet de 9 octets ici est un candidat valide pour"l'objet array". (En fait, il s'agit du seulement tel candidat pour le cas de l'utilisation de x pour itérer sur l'allocation de 9 octets, qui, je pense que tout le monde serait d'accord est juridique).


Note: Cette section est très spéculative et je tente de fournir un argument sur la raison pour laquelle la solution choisie par les compilateurs ici n'est pas auto-cohérente

un argument pourrait que &y->c signifie que la provenance est le sous-objet int64_t . Mais cela conduit immédiatement à des difficultés. Par exemple, y a-t-il la provenance de *y ? Si c'est le cas, (char *)y devrait avoir la provenance *y encore, mais alors ceci contredit la règle du 6.3.2.3 / 7 que la fonte d'un pointeur à un autre type et retour devrait retourner le pointeur d'origine (comme longtemps que l'alignement n'est pas violé).

une autre chose qu'il ne couvre pas est le chevauchement de provenance. Peut-on comparer un pointeur inégal à un pointeur de même valeur mais de plus petite provenance (qui est un sous-ensemble de la plus grande provenance) ?

en outre, si nous appliquons ce même principe au cas où le sous-objet est un tableau:

char arr[2][2];
char *r = (char *)arr;    
++r; ++r; ++r;     // undefined behavior - exceeds bounds of arr[0]

arr est défini comme signifiant &arr[0] dans ce contexte, donc, si l' la provenance de &X est X , puis r est en fait limitée à la première rangée du tableau -- peut-être un résultat surprenant.

il serait possible de dire que char *r = (char *)arr; conduit à UB ici, mais char *r = (char *)&arr; ne le fait pas. En fait, j'avais l'habitude de promouvoir ce point de vue dans mes billets il y a de nombreuses années. Mais je ne le fais plus: dans mon expérience d'essayer de défendre cette position, il ne peut tout simplement pas être rendu auto-cohérent, il ya trop de scénarios de problèmes. Et même si elle pouvait être rendue autocohérente, il n'en demeure pas moins que la norme ne la spécifie pas. Au mieux, ils doivent avoir le statut d'une proposition.


pour finir, je recommande la lecture de N2090: clarification de la Provenance des pointeurs (projet de rapport de défaut ou proposition de C2x) .

leur proposition est que la provenance s'applique toujours à une allocation . Cela rend moot toutes les complexités des objets et des sous-objets. Il n'y a pas de sous-répartition. Dans la présente proposition, tous les x z w sont identiques et peuvent être utilisés pour couvrir l'ensemble de l'allocation de 9 octets. IMHO la simplicité de ceci est attrayante, comparée à ce qui a été discuté dans ma section précédente.

10
répondu M.M 2017-11-11 10:42:53

tout cela à cause de -D_FORTIFY_SOURCE=2 s'écraser intentionnellement sur ce qu'il juge dangereux.

certains distros build gcc avec -D_FORTIFY_SOURCE=2 activé par défaut. Certains non. Cela explique toutes les différences entre les différents compilateurs. Probablement ceux qui ne s'écrasent pas normalement le feront si vous construisez votre code avec -O3 -D_FORTIFY_SOURCE=2 .

pourquoi ne fonctionne-t-il que si l'optimisation est activée?

_FORTIFY_SOURCE nécessite une compilation avec optimisation ( -O ) pour garder la trace des tailles d'objet à travers des moulages / attributions de pointeurs. Voir les diapositives de cette présentation pour en savoir plus sur _FORTIFY_SOURCE .

Pourquoi ne strcpy() échoue? Comment peut-il?

gcc appelle __memcpy_chk pour strcpy seulement avec -D_FORTIFY_SOURCE=2 . Ça passe 8 comme la taille de la cible objet, parce que c'est ce qu'il pense que vous voulez dire , ce qu'il peut comprendre à partir du code source que vous avez donné. Même chose pour strncpy appelant __strncpy_chk .

__memcpy_chk abroge volontairement. _FORTIFY_SOURCE peut aller au-delà des choses qui sont UB en C et rejeter les choses qui semblent potentiellement dangereux . Cela lui donne la licence de décider que votre code est dangereux. (Comme d'autres l'ont souligné, un flexible de membre du groupe comme l' le dernier membre de votre struct, et / ou un syndicat avec un membre flexible-array, est comment vous devriez exprimer ce que vous faites en C.)


gcc avertit même que le contrôle échouera toujours:

In function 'strcpy',
    inlined from 'main' at <source>:18:9:
/usr/include/x86_64-linux-gnu/bits/string3.h:110:10: warning: call to __builtin___memcpy_chk will always overflow destination buffer
   return __builtin___strcpy_chk (__dest, __src, __bos (__dest));
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

(de gcc7.2 -O3 -Wall sur le Godbolt compilateur explorer ).


pourquoi memcpy() n'échoue-t-il pas malgré le niveau -O ?

IDK.

gcc pleinement inlines il juste une charge 8B / magasin + une charge 1B / magasin. (Cela ressemble à une optimisation manquée; il devrait savoir que malloc ne l'a pas modifié sur la pile, donc il pourrait simplement le stocker à partir d'immédiats de nouveau au lieu de recharger. (Ou mieux, conservez la valeur 8B dans un registre.)

3
répondu Peter Cordes 2017-11-11 07:02:37

pourquoi compliquer les choses? comportement non défini , dans cette partie:

memcpy((char*)&p->c, str, strlen(str)+1);
puts((char*)&p->c);

warning: passing argument 1 of 'met' from incompatible pointeur de ty pe [-Wincompatible-pointeur-types] puts (&p->c);

vous finissez clairement dans une zone de mémoire non attribuée ou dans un endroit accessible en écriture si vous êtes chanceux...

optimiser ou non peut changer les valeurs des adresses, et cela peut fonctionner (puisque les adresses concordent), ou non. Vous juste ne peut pas faire ce que vous voulez faire (essentiellement mentir au compilateur )

je:

  • allouer juste ce qui est nécessaire pour la structure, ne pas prendre en compte la longueur de la chaîne à l'intérieur, c'est inutile
  • n'utilisez pas gets comme il est dangereux et obsolète
  • utilisez strdup au lieu du code à tendance bug memcpy que vous utilisez puisque vous manipulez des chaînes. strdup n'oubliera pas d'allouer le nul-terminator, et le définira dans la cible pour vous.
  • n'oubliez pas de libérer la chaîne dupliquée
  • lire les Avertissements, put(&p->c) est un comportement non défini

test.C:19: 10: avertissement: passer l'argument 1 de 'puts' à partir du pointeur incompatible ty pe [-Wincompatible-pointeur-types] puts (&p->c);

ma proposition

int main(){
    pack *p = malloc(sizeof(pack));
    char str[1024];
    fgets(str,sizeof(str),stdin);
    p->c = strdup(str);
    puts(p->c);
    free(p->c);
    free(p);
  return 0;
}
2
répondu Jean-François Fabre 2017-11-10 10:30:22

votre pointeur p- > c est la cause du crash.

Initialise d'abord struct avec la taille de "non signé long long" plus la taille de "*p".

Second initialise le pointeur p- > c avec la taille de zone requise. Faire une copie de l'opération: strcpy (P - > C, str);

Enfin libre d'abord libre(p->c) et libre(p).

Je pense que c'est ça.

[Modifier]

j'insiste. La cause de l'erreur est que sa structure seule réserve de l'espace pour le pointeur, mais ne pas affecter le pointeur de contenir les données qui seront copiés.

Prendre un coup d'oeil

int main() 
{
    pack *p;
    char str[1024];
    gets(str);
    size_t len_struc = sizeof(*p) + sizeof(unsigned long long);
    p = malloc(len_struc);
    p->c = malloc(strlen(str));
    strcpy(p->c, str); // This do not crashes!
    puts(&p->c);
    free(p->c);
    free(p);
    return 0;
}

[Edit 2]

Ce n'est pas une façon traditionnelle de stocker des données, mais cela fonctionne:

    pack2 *p;
    char str[9] = "aaaaaaaa"; // Input
    size_t len = sizeof(pack) + (strlen(str) + 1);
    p = malloc(len);
    // Version 1: crash
    strcpy((char*)p + sizeof(pack), str);
    free(p);

-4
répondu lsalamon 2017-11-10 17:33:09