J'ai un package R avec du code compilé C qui est relativement stable depuis un certain temps et est fréquemment testé contre une grande variété de plates-formes et de compilateurs (windows / osx / debian / fedora gcc / clang).
Plus récemment, une nouvelle plateforme a été ajoutée pour tester à nouveau le package:
Logs from checks with gcc trunk aka 10.0.1 compiled from source
on Fedora 30. (For some archived packages, 10.0.0.)
x86_64 Fedora 30 Linux
FFLAGS="-g -O2 -mtune=native -Wall -fallow-argument-mismatch"
CFLAGS="-g -O2 -Wall -pedantic -mtune=native -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection"
CXXFLAGS="-g -O2 -Wall -pedantic -mtune=native -Wno-ignored-attributes -Wno-deprecated-declarations -Wno-parentheses -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection"
À ce stade, le code compilé a rapidement commencé à effectuer des erreurs de segmentation dans ce sens:
*** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'
J'ai pu reproduire le segfault de manière cohérente en utilisant le rocker/r-base
conteneur docker avec gcc-10.0.1
un niveau d'optimisation -O2
. L'exécution d'une optimisation inférieure supprime le problème. L'exécution de toute autre configuration, y compris sous valgrind (-O0 et -O2), UBSAN (gcc / clang), ne montre aucun problème. Je suis également raisonnablement sûr que cela gcc-10.0.0
ne fonctionnait pas, mais je n'ai pas les données.
J'ai exécuté la gcc-10.0.1 -O2
version avec gdb
et j'ai remarqué quelque chose qui me semble étrange:
En parcourant la section en surbrillance, il semble que l'initialisation des deuxièmes éléments des tableaux soit ignorée ( R_alloc
est un wrapper autour de ce malloc
que les ordures collectent automatiquement lors du retour du contrôle à R; la faute de segmentation se produit avant de revenir à R). Plus tard, le programme se bloque lors de l'accès à l'élément non initialisé (dans la version gcc.10.0.1 -O2).
J'ai corrigé cela en initialisant explicitement l'élément en question partout dans le code qui a finalement conduit à l'utilisation de l'élément, mais il aurait vraiment dû être initialisé sur une chaîne vide, ou du moins c'est ce que j'aurais supposé.
Suis-je en train de manquer quelque chose d'évident ou de faire quelque chose de stupide? Les deux sont raisonnablement probables car C est de loin ma deuxième langue . C'est juste étrange que cela apparaisse maintenant, et je ne peux pas comprendre ce que le compilateur essaie de faire.
MISE A JOUR : Instructions pour reproduire ce, bien que cela ne se reproduire tant que debian:testing
conteneur docker a gcc-10
à gcc-10.0.1
. Aussi, ne vous contentez pas d'exécuter ces commandes si vous ne me faites pas confiance .
Désolé, ce n'est pas un exemple reproductible minimal.
docker pull rocker/r-base
docker run --rm -ti --security-opt seccomp=unconfined \
rocker/r-base /bin/bash
apt-get update
apt-get install gcc-10 gdb
gcc-10 --version # confirm 10.0.1
# gcc-10 (Debian 10-20200222-1) 10.0.1 20200222 (experimental)
# [master revision 01af7e0a0c2:487fe13f218:e99b18cf7101f205bfdd9f0f29ed51caaec52779]
mkdir ~/.R
touch ~/.R/Makevars
echo "CC = gcc-10
CFLAGS = -g -O2 -Wall -pedantic -mtune=native -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection
" >> ~/.R/Makevars
R -d gdb --vanilla
Puis dans la console R, après avoir tapé run
pour arriver gdb
à exécuter le programme:
f.dl <- tempfile()
f.uz <- tempfile()
github.url <- 'https://github.com/brodieG/vetr/archive/v0.2.8.zip'
download.file(github.url, f.dl)
unzip(f.dl, exdir=f.uz)
install.packages(
file.path(f.uz, 'vetr-0.2.8'), repos=NULL,
INSTALL_opts="--install-tests", type='source'
)
# minimal set of commands to segfault
library(vetr)
alike(pairlist(a=1, b="character"), pairlist(a=1, b=letters))
alike(pairlist(1, "character"), pairlist(1, letters))
alike(NULL, 1:3) # not a wild card at top level
alike(list(NULL), list(1:3)) # but yes when nested
alike(list(NULL, NULL), list(list(list(1, 2, 3)), 1:25))
alike(list(NULL), list(1, 2))
alike(list(), list(1, 2))
alike(matrix(integer(), ncol=7), matrix(1:21, nrow=3))
alike(matrix(character(), nrow=3), matrix(1:21, nrow=3))
alike(
matrix(integer(), ncol=3, dimnames=list(NULL, c("R", "G", "B"))),
matrix(1:21, ncol=3, dimnames=list(NULL, c("R", "G", "B")))
)
# Adding tests from docs
mx.tpl <- matrix(
integer(), ncol=3, dimnames=list(row.id=NULL, c("R", "G", "B"))
)
mx.cur <- matrix(
sample(0:255, 12), ncol=3, dimnames=list(row.id=1:4, rgb=c("R", "G", "B"))
)
mx.cur2 <-
matrix(sample(0:255, 12), ncol=3, dimnames=list(1:4, c("R", "G", "B")))
alike(mx.tpl, mx.cur2)
L'inspection dans gdb montre assez rapidement (si je comprends bien) qui
CSR_strmlen_x
essaie d'accéder à la chaîne qui n'a pas été initialisée.
UPDATE 2 : il s'agit d'une fonction hautement récursive, et en plus de cela, le bit d'initialisation de la chaîne est appelé plusieurs fois. C'est surtout parce que j'étais paresseux, nous n'avons besoin que des chaînes initialisées pour la seule fois où nous rencontrons réellement quelque chose que nous voulons signaler dans la récursivité, mais il était plus facile d'initialiser chaque fois qu'il est possible de rencontrer quelque chose. Je le mentionne car ce que vous verrez ensuite montre plusieurs initialisations, mais une seule d'entre elles (probablement celle avec l'adresse <0x1400000001>) est utilisée.
Je ne peux pas garantir que les éléments que je montre ici sont directement liés à l'élément qui a causé la panne (bien qu'il s'agisse du même accès illégal à l'adresse), mais comme @ nate-eldredge l'a demandé, cela montre que l'élément de tableau n'est pas initialisé juste avant le retour ou juste après le retour dans la fonction appelante. Notez que la fonction appelante est en train d'initialiser 8 d'entre eux, et je les montre tous, avec tous remplis de déchets ou de mémoire inaccessible.
MISE À JOUR 3 , démontage de la fonction en question:
Breakpoint 1, ALIKEC_res_strings_init () at alike.c:75
75 return res;
(gdb) p res.current[0]
$1 = 0x7ffff46a0aa5 "%s%s%s%s"
(gdb) p res.current[1]
$2 = 0x1400000001 <error: Cannot access memory at address 0x1400000001>
(gdb) disas /m ALIKEC_res_strings_init
Dump of assembler code for function ALIKEC_res_strings_init:
53 struct ALIKEC_res_strings ALIKEC_res_strings_init() {
0x00007ffff4687fc0 <+0>: endbr64
54 struct ALIKEC_res_strings res;
55
56 res.target = (const char **) R_alloc(5, sizeof(const char *));
0x00007ffff4687fc4 <+4>: push %r12
0x00007ffff4687fc6 <+6>: mov $0x8,%esi
0x00007ffff4687fcb <+11>: mov %rdi,%r12
0x00007ffff4687fce <+14>: push %rbx
0x00007ffff4687fcf <+15>: mov $0x5,%edi
0x00007ffff4687fd4 <+20>: sub $0x8,%rsp
0x00007ffff4687fd8 <+24>: callq 0x7ffff4687180 <R_alloc@plt>
0x00007ffff4687fdd <+29>: mov $0x8,%esi
0x00007ffff4687fe2 <+34>: mov $0x5,%edi
0x00007ffff4687fe7 <+39>: mov %rax,%rbx
57 res.current = (const char **) R_alloc(5, sizeof(const char *));
0x00007ffff4687fea <+42>: callq 0x7ffff4687180 <R_alloc@plt>
58
59 res.target[0] = "%s%s%s%s";
0x00007ffff4687fef <+47>: lea 0x1764a(%rip),%rdx # 0x7ffff469f640
0x00007ffff4687ff6 <+54>: lea 0x18aa8(%rip),%rcx # 0x7ffff46a0aa5
0x00007ffff4687ffd <+61>: mov %rcx,(%rbx)
60 res.target[1] = "";
61 res.target[2] = "";
0x00007ffff4688000 <+64>: mov %rdx,0x10(%rbx)
62 res.target[3] = "";
0x00007ffff4688004 <+68>: mov %rdx,0x18(%rbx)
63 res.target[4] = "";
0x00007ffff4688008 <+72>: mov %rdx,0x20(%rbx)
64
65 res.tar_pre = "be";
66
67 res.current[0] = "%s%s%s%s";
0x00007ffff468800c <+76>: mov %rax,0x8(%r12)
0x00007ffff4688011 <+81>: mov %rcx,(%rax)
68 res.current[1] = "";
69 res.current[2] = "";
0x00007ffff4688014 <+84>: mov %rdx,0x10(%rax)
70 res.current[3] = "";
0x00007ffff4688018 <+88>: mov %rdx,0x18(%rax)
71 res.current[4] = "";
0x00007ffff468801c <+92>: mov %rdx,0x20(%rax)
72
73 res.cur_pre = "is";
74
75 return res;
=> 0x00007ffff4688020 <+96>: lea 0x14fe0(%rip),%rax # 0x7ffff469d007
0x00007ffff4688027 <+103>: mov %rax,0x10(%r12)
0x00007ffff468802c <+108>: lea 0x14fcd(%rip),%rax # 0x7ffff469d000
0x00007ffff4688033 <+115>: mov %rbx,(%r12)
0x00007ffff4688037 <+119>: mov %rax,0x18(%r12)
0x00007ffff468803c <+124>: add $0x8,%rsp
0x00007ffff4688040 <+128>: pop %rbx
0x00007ffff4688041 <+129>: mov %r12,%rax
0x00007ffff4688044 <+132>: pop %r12
0x00007ffff4688046 <+134>: retq
0x00007ffff4688047: nopw 0x0(%rax,%rax,1)
End of assembler dump.
MISE À JOUR 4 :
Donc, en essayant d'analyser la norme, voici les parties qui semblent pertinentes ( projet C11 ):
6.3.2.3 Conversions Par7> Autres opérandes> Pointeurs
Un pointeur vers un type d'objet peut être converti en pointeur vers un type d'objet différent. Si le pointeur résultant n'est pas correctement aligné 68) pour le type référencé, le comportement n'est pas défini.
Sinon, une fois reconverti, le résultat doit être égal au pointeur d'origine. Lorsqu'un pointeur vers un objet est converti en pointeur vers un type de caractère, le résultat pointe vers l'octet adressé le plus bas de l'objet. Des incréments successifs du résultat, jusqu'à la taille de l'objet, fournissent des pointeurs vers les octets restants de l'objet.
6.5 Expressions Par6
Le type effectif d'un objet pour un accès à sa valeur stockée est le type déclaré de l'objet, le cas échéant. 87) Si une valeur est stockée dans un objet n'ayant pas de type déclaré via une lvalue ayant un type qui n'est pas un type de caractère, alors le type de la lvalue devient le type effectif de l'objet pour cet accès et pour les accès ultérieurs qui ne le sont pas. modifier la valeur stockée. Si une valeur est copiée dans un objet sans type déclaré à l'aide de memcpy ou memmove, ou est copiée en tant que tableau de type de caractère, le type effectif de l'objet modifié pour cet accès et pour les accès ultérieurs qui ne modifient pas la valeur est le type effectif de l'objet à partir duquel la valeur est copiée, si elle en a un. Pour tous les autres accès à un objet n'ayant pas de type déclaré, le type effectif de l'objet est simplement le type de la valeur l utilisée pour l'accès.
87) Les objets alloués n'ont pas de type déclaré.
IIUC R_alloc
renvoie un décalage dans un malloc
bloc ed dont l' double
alignement est garanti , et la taille du bloc après le décalage est de la taille demandée (il y a également une allocation avant le décalage pour les données spécifiques à R). R_alloc
transforme ce pointeur en (char *)
retour.
Section 6.2.5 Par 29
Un pointeur vers void doit avoir les mêmes exigences de représentation et d'alignement qu'un pointeur vers un type de caractère. 48) De même, les pointeurs vers des versions qualifiées ou non qualifiées de types compatibles doivent avoir les mêmes exigences de représentation et d'alignement. Tous les pointeurs vers les types de structure doivent avoir les mêmes exigences de représentation et d'alignement les uns que les autres.
Tous les pointeurs vers les types d'union doivent avoir les mêmes exigences de représentation et d'alignement les uns que les autres.
Les pointeurs vers d'autres types n'ont pas besoin d'avoir les mêmes exigences de représentation ou d'alignement.48) Les mêmes exigences de représentation et d'alignement sont censées impliquer l'interchangeabilité comme arguments pour les fonctions, les valeurs de retour des fonctions et les membres des unions.
La question est donc "sommes-nous autorisés à refondre le (char *)
to (const char **)
et à y écrire (const char **)
". Ma lecture de ce qui précède est que tant que les pointeurs sur les systèmes dans lesquels le code est exécuté ont un alignement compatible avec l' double
alignement, alors ça va.
Sommes-nous en train de violer un "alias strict"? c'est à dire:
6.5 Par 7
Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui a l'un des types suivants: 88)
- un type compatible avec le type effectif de l'objet ...
88) Le but de cette liste est de spécifier les circonstances dans lesquelles un objet peut ou non être replié.
Alors, quel devrait être le compilateur pense que le type effectif de l'objet pointé par res.target
(ou res.current
) est? Vraisemblablement le type déclaré (const char **)
, ou est-ce réellement ambigu? Il me semble que ce n'est pas dans ce cas uniquement parce qu'il n'y a pas d'autre «lvalue» de portée qui accède au même objet.
J'admets que je me bats puissamment pour extraire du sens de ces sections de la norme.
la source
-mtune=native
optimise pour le processeur particulier de votre machine. Ce sera différent pour différents testeurs et peut faire partie du problème. Si vous exécutez la compilation avec,-v
vous devriez pouvoir voir quelle famille de processeurs se trouve sur votre machine (par exemple-mtune=skylake
sur mon ordinateur).disassemble
instruction dans gdb.Réponses:
Résumé: Cela semble être un bogue dans gcc, lié à l'optimisation des chaînes. Un testcase autonome est ci-dessous. Au départ, il y avait un doute quant à savoir si le code est correct, mais je pense que oui.
J'ai signalé le bogue comme PR 93982 . Un correctif proposé a été validé mais il ne le corrige pas dans tous les cas, conduisant au suivi PR 94015 ( lien Godbolt ).
Vous devriez pouvoir contourner le bogue en compilant avec le drapeau
-fno-optimize-strlen
.J'ai pu réduire votre cas de test à l'exemple minimal suivant (également sur godbolt ):
Avec gcc trunk (gcc version 10.0.1 20200225 (experimental)) et
-O2
(toutes les autres options se sont avérées inutiles), l'assembly généré sur amd64 est le suivant:Vous avez donc tout à fait raison que le compilateur ne parvient pas à s'initialiser
res.target[1]
(notez l'absence évidente demovq $.LC1, 8(%rax)
).Il est intéressant de jouer avec le code et de voir ce qui affecte le "bug". Peut-être de manière significative, le fait de changer le type de retour de
R_alloc
levoid *
fait disparaître et vous donne une sortie d'assemblage "correcte". Peut-être moins de manière significative mais plus amusante, changer la chaîne"12345678"
pour qu'elle soit plus longue ou plus courte la fait également disparaître.Discussion précédente, maintenant résolue - le code est apparemment légal.
Ma question est de savoir si votre code est réellement légal. Le fait que vous prenez le
char *
retourné parR_alloc()
et jeté àconst char **
, puis stocker unconst char *
semble que cela pourrait violer la règle stricte de aliasing , commechar
etconst char *
ne sont pas compatibles avec les types. Il y a une exception qui vous permet d'accéder à n'importe quel objet en tant quechar
(pour implémenter des choses commememcpy
), mais c'est l'inverse, et si je comprends bien, ce n'est pas autorisé. Cela fait que votre code produit un comportement indéfini et donc le compilateur peut légalement faire tout ce qu'il veut.Si tel est le cas, le correctif correct serait que R modifie son code afin qu'il
R_alloc()
renvoie à lavoid *
place dechar *
. Il n'y aurait alors pas de problème d'alias. Malheureusement, ce code est hors de votre contrôle, et je ne sais pas comment vous pouvez utiliser cette fonction sans violer l'aliasing strict. Une solution de contournement pourrait être d'interposer une variable temporaire, par exemple,void *tmp = R_alloc(); res.target = tmp;
ce qui résout le problème dans le cas de test, mais je ne sais toujours pas si c'est légal.Cependant, je ne suis pas sûr de cette hypothèse de "strict aliasing", car la compilation avec
-fno-strict-aliasing
laquelle AFAIK est censé permettre à gcc d'autoriser de telles constructions ne fait pas disparaître le problème!Mise à jour. En essayant différentes options, j'ai trouvé que l'un
-fno-optimize-strlen
ou l' autre-fno-tree-forwprop
entraînerait la génération d'un code "correct". En outre, l'utilisation-O1 -foptimize-strlen
génère le code incorrect (mais-O1 -ftree-forwprop
pas).Après un petit
git bisect
exercice, l'erreur semble avoir été introduite dans la validation 34fcf41e30ff56155e996f5e04 .Mise à jour 2. J'ai essayé de creuser un peu dans la source gcc, juste pour voir ce que je pouvais apprendre. (Je ne prétends pas être une sorte d'expert en compilation!)
Il semble que le code
tree-ssa-strlen.c
soit destiné à garder une trace des chaînes apparaissant dans le programme. Pour autant que je sache, le bogue est qu'en regardant l'instruction,res.target[0] = "12345678";
le compilateur confond l' adresse du littéral de chaîne"12345678"
avec la chaîne elle-même. (Cela semble être lié à ce code suspect qui a été ajouté dans la validation susmentionnée, où s'il essaie de compter les octets d'une "chaîne" qui est en fait une adresse, il regarde plutôt ce vers quoi pointe cette adresse.)Il pense donc que l'instruction
res.target[0] = "12345678"
, au lieu de stocker l' adresse de"12345678"
à l'adresseres.target
, stocke la chaîne elle-même à cette adresse, comme si l'instruction l'étaitstrcpy(res.target, "12345678")
. Notez pour ce qui est à venir que cela entraînerait le stockage du zéro final à l'adresseres.target+8
(à ce stade du compilateur, tous les décalages sont en octets).Maintenant, lorsque le compilateur regarde
res.target[1] = ""
, il traite également cela comme s'il l'étaitstrcpy(res.target+8, "")
, le 8 provenant de la taille de achar *
. Autrement dit, comme s'il s'agissait simplement de stocker un octet nul à l'adresseres.target+8
. Cependant, le compilateur "sait" que l'instruction précédente stockait déjà un octet nul à cette adresse! En tant que telle, cette déclaration est "redondante" et peut être supprimée ( ici ).Cela explique pourquoi la chaîne doit comporter exactement 8 caractères pour déclencher le bogue. (Bien que d'autres multiples de 8 puissent également déclencher le bug dans d'autres situations.)
la source
int*
mais pasconst char**
.int *
est également illégal (ou plutôt, le stockage deint
s y est illégal).char*
et travaillez sur x86_64 ... Je ne vois pas d'UB ici, c'est un bug gcc.R_alloc()
, le programme est conforme, quelle que soit l'unité de traductionR_alloc()
définie. C'est le compilateur qui ne parvient pas à se conformer ici.