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.
-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.