Ich habe ein R-Paket mit C-kompiliertem Code, das seit einiger Zeit relativ stabil ist und häufig gegen eine Vielzahl von Plattformen und Compilern (Windows / OSX / Debian / Fedora GCC / Clang) getestet wird.
In jüngerer Zeit wurde eine neue Plattform hinzugefügt, um das Paket erneut zu testen:
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"
Zu diesem Zeitpunkt begann der kompilierte Code sofort mit dem Segfaulting in dieser Richtung:
*** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'
Ich konnte den Segfault konsistent reproduzieren, indem ich den rocker/r-base
Docker-Container gcc-10.0.1
mit Optimierungsstufe verwendete -O2
. Durch Ausführen einer niedrigeren Optimierung wird das Problem behoben. Das Ausführen eines anderen Setups, einschließlich UBSAN (gcc / clang) unter valgrind (sowohl -O0 als auch -O2), zeigt überhaupt keine Probleme. Ich bin mir auch ziemlich sicher, dass dies unterging gcc-10.0.0
, aber ich habe keine Daten.
Ich habe die gcc-10.0.1 -O2
Version mit ausgeführt gdb
und etwas bemerkt, das mir seltsam erscheint:
Beim Durchlaufen des hervorgehobenen Abschnitts wird anscheinend die Initialisierung der zweiten Elemente der Arrays übersprungen ( R_alloc
ist ein Wrapper um malloc
den Selbstmüll, der bei der Rückgabe der Steuerung an R gesammelt wird; der Segfault tritt vor der Rückkehr zu R auf). Später stürzt das Programm ab, wenn auf das nicht initialisierte Element (in der Version gcc.10.0.1 -O2) zugegriffen wird.
Ich habe dies behoben, indem ich das betreffende Element überall im Code explizit initialisiert habe, was schließlich zur Verwendung des Elements führte, aber es hätte wirklich mit einer leeren Zeichenfolge initialisiert werden müssen, oder zumindest hätte ich das angenommen.
Vermisse ich etwas Offensichtliches oder mache ich etwas Dummes? Beides ist ziemlich wahrscheinlich, da C bei weitem meine zweite Sprache ist . Es ist nur seltsam, dass dies gerade aufgetaucht ist, und ich kann nicht herausfinden, was der Compiler versucht zu tun.
UPDATE : Anweisungen , dies zu reproduzieren, obwohl dies nur so lange reproduzieren als debian:testing
Docker Behälter gcc-10
an gcc-10.0.1
. Auch nicht nur diese Befehle ausführen , wenn Sie mir nicht trauen .
Dies ist leider kein minimal reproduzierbares Beispiel.
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
Dann in der R-Konsole, nachdem Sie eingegeben haben run
, um gdb
das Programm auszuführen:
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)
Die Überprüfung in gdb zeigt ziemlich schnell (wenn ich das richtig verstehe), dass
CSR_strmlen_x
versucht wird, auf die Zeichenfolge zuzugreifen, die nicht initialisiert wurde.
UPDATE 2 : Dies ist eine sehr rekursive Funktion, und außerdem wird das String-Initialisierungsbit viele, viele Male aufgerufen. Dies ist meistens b / c. Ich war faul. Wir müssen die Zeichenfolgen nur einmal initialisieren, wenn wir tatsächlich auf etwas stoßen, das wir in der Rekursion melden möchten. Es war jedoch einfacher, jedes Mal zu initialisieren, wenn es möglich ist, auf etwas zu stoßen. Ich erwähne dies, weil das, was Sie als nächstes sehen werden, mehrere Initialisierungen zeigt, aber nur eine davon (vermutlich die mit der Adresse <0x1400000001>) verwendet wird.
Ich kann nicht garantieren, dass das hier gezeigte Material direkt mit dem Element zusammenhängt, das den Segfault verursacht hat (obwohl es sich um denselben illegalen Adresszugriff handelt), aber wie @ nate-eldredge gefragt hat, zeigt es, dass das Array-Element nicht vorhanden ist entweder kurz vor der Rückkehr oder kurz nach der Rückkehr in der aufrufenden Funktion initialisiert. Beachten Sie, dass die aufrufende Funktion 8 davon initialisiert, und ich zeige sie alle, wobei alle entweder mit Müll oder unzugänglichem Speicher gefüllt sind.
UPDATE 3 , Demontage der betreffenden Funktion:
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.
UPDATE 4 :
Der Versuch, den Standard hier zu analysieren, scheint also relevant zu sein ( C11-Entwurf ):
6.3.2.3 Par7-Konvertierungen> Andere Operanden> Zeiger
Ein Zeiger auf einen Objekttyp kann in einen Zeiger auf einen anderen Objekttyp konvertiert werden. Wenn der resultierende Zeiger für den referenzierten Typ nicht korrekt ausgerichtet ist 68), ist das Verhalten undefiniert.
Andernfalls wird das Ergebnis bei erneuter Konvertierung gleich dem ursprünglichen Zeiger verglichen. Wenn ein Zeiger auf ein Objekt in einen Zeiger auf einen Zeichentyp konvertiert wird, zeigt das Ergebnis auf das niedrigste adressierte Byte des Objekts. Aufeinanderfolgende Inkremente des Ergebnisses bis zur Größe des Objekts ergeben Zeiger auf die verbleibenden Bytes des Objekts.
6.5 Par6-Ausdrücke
Der effektive Typ eines Objekts für den Zugriff auf seinen gespeicherten Wert ist der deklarierte Typ des Objekts, falls vorhanden. 87) Wenn ein Wert in einem Objekt ohne deklarierten Typ über einen Wert mit einem Typ gespeichert wird, der kein Zeichentyp ist, wird der Typ des Werts zum effektiven Typ des Objekts für diesen Zugriff und für nachfolgende Zugriffe, die dies nicht tun Ändern Sie den gespeicherten Wert. Wenn ein Wert mit memcpy oder memmove in ein Objekt ohne deklarierten Typ kopiert oder als Array mit Zeichentyp kopiert wird, ist der effektive Typ des geänderten Objekts für diesen Zugriff und für nachfolgende Zugriffe, die den Wert nicht ändern, der effektiver Typ des Objekts, von dem der Wert kopiert wird, falls vorhanden. Bei allen anderen Zugriffen auf ein Objekt ohne deklarierten Typ ist der effektive Typ des Objekts einfach der Typ des für den Zugriff verwendeten l-Werts.
87) Zugeordnete Objekte haben keinen deklarierten Typ.
IIUC R_alloc
gibt einen Versatz in einen malloc
ed-Block zurück, dessen double
Ausrichtung garantiert ist , und die Größe des Blocks nach dem Versatz entspricht der angeforderten Größe (es gibt auch eine Zuordnung vor dem Versatz für R-spezifische Daten). R_alloc
Wirkt diesen Zeiger (char *)
bei der Rückkehr.
Abschnitt 6.2.5 Abs. 29
Ein Zeiger auf void muss die gleichen Darstellungs- und Ausrichtungsanforderungen haben wie ein Zeiger auf einen Zeichentyp. 48) Ebenso müssen Zeiger auf qualifizierte oder nicht qualifizierte Versionen kompatibler Typen dieselben Darstellungs- und Ausrichtungsanforderungen haben. Alle Zeiger auf Strukturtypen müssen die gleichen Darstellungs- und Ausrichtungsanforderungen haben.
Alle Zeiger auf Vereinigungstypen müssen dieselben Darstellungs- und Ausrichtungsanforderungen haben.
Zeiger auf andere Typen müssen nicht dieselben Darstellungs- oder Ausrichtungsanforderungen haben.48) Dieselben Darstellungs- und Ausrichtungsanforderungen sollen Austauschbarkeit als Argumente für Funktionen, Rückgabewerte von Funktionen und Gewerkschaftsmitglieder implizieren.
Die Frage ist also „wir sind die neu zu fassen erlaubt (char *)
zu (const char **)
und schreiben , um es als (const char **)
“. Ich habe oben gelesen, dass es in double
Ordnung ist, solange Zeiger auf den Systemen, auf denen der Code ausgeführt wird, mit der Ausrichtung kompatibel sind .
Verstoßen wir gegen "striktes Aliasing"? dh:
6.5 Abs. 7
Auf einen gespeicherten Wert eines Objekts darf nur über einen lvalue-Ausdruck zugegriffen werden, der einen der folgenden Typen hat: 88)
- ein Typ, der mit dem effektiven Typ des Objekts kompatibel ist ...
88) Mit dieser Liste sollen die Umstände angegeben werden, unter denen ein Objekt möglicherweise einen Alias aufweist oder nicht.
Was sollte der Compiler also für den effektiven Typ des Objekts halten, auf das res.target
(oder res.current
) zeigt? Vermutlich der deklarierte Typ (const char **)
, oder ist dieser tatsächlich mehrdeutig? Ich habe das Gefühl, dass dies in diesem Fall nicht nur deshalb der Fall ist, weil es keinen anderen "Wert" im Bereich gibt, der auf dasselbe Objekt zugreift.
Ich gebe zu, ich habe große Mühe, diesen Abschnitten des Standards Sinn zu entziehen.
-mtune=native
Optimiert für die jeweilige CPU Ihres Computers. Dies ist für verschiedene Tester unterschiedlich und kann Teil des Problems sein. Wenn Sie die Kompilierung mit ausführen -v
, sollten Sie sehen können, welche CPU-Familie sich auf Ihrem Computer befindet (z. B. -mtune=skylake
auf meinem Computer).
disassemble
Anweisung in gdb verwenden.