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.
quelle
-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.Antworten:
Zusammenfassung: Dies scheint ein Fehler in gcc zu sein, der mit der Zeichenfolgenoptimierung zusammenhängt. Ein in sich geschlossener Testfall ist unten. Anfangs gab es einige Zweifel, ob der Code korrekt ist, aber ich denke, das ist es.
Ich habe den Fehler als PR 93982 gemeldet . Ein vorgeschlagener Fix wurde festgeschrieben , aber nicht in allen Fällen, was zum Follow-up PR 94015 ( Godbolt-Link ) führte.
Sie sollten in der Lage sein, den Fehler zu umgehen, indem Sie mit dem Flag kompilieren
-fno-optimize-strlen
.Ich konnte Ihren Testfall auf das folgende minimale Beispiel reduzieren (auch auf Godbolt ):
Mit gcc trunk (gcc version 10.0.1 20200225 (experimentell)) und
-O2
(alle anderen Optionen erwiesen sich als unnötig) lautet die generierte Assembly auf amd64 wie folgt:Sie haben also völlig Recht, dass der Compiler nicht initialisiert werden kann
res.target[1]
(beachten Sie das auffällige Fehlen vonmovq $.LC1, 8(%rax)
).Es ist interessant, mit dem Code zu spielen und zu sehen, was den "Bug" betrifft. Wenn Sie den Rückgabetyp von
R_alloc
auf ändern ,void *
wird er möglicherweise nicht mehr angezeigt, und Sie erhalten eine "korrekte" Baugruppenausgabe. Vielleicht weniger bedeutend, aber amüsanter: Wenn Sie die Saite"12345678"
entweder länger oder kürzer ändern, verschwindet sie auch.Vorherige Diskussion, jetzt gelöst - der Code ist anscheinend legal.
Die Frage, die ich habe, ist, ob Ihr Code tatsächlich legal ist. Die Tatsache , dass Sie das nehmen
char *
von zurückR_alloc()
und warf es aufconst char **
, und speichern Sie dann eineconst char *
scheint , wie es die verletzen könnten strenge Aliasing - Regel , wiechar
undconst char *
nicht kompatible Typen. Es gibt eine Ausnahme, mit der Sie auf jedes Objekt als zugreifen könnenchar
(um Dinge wiememcpy
) zu implementieren , aber dies ist umgekehrt, und so gut ich es verstehe, ist dies nicht zulässig. Dadurch erzeugt Ihr Code ein undefiniertes Verhalten, und der Compiler kann legal alles tun, was er will.Ist dies der Fall, wäre die richtige fix sein für R , ihren Code zu ändern , so dass
R_alloc()
Renditenvoid *
stattchar *
. Dann würde es kein Aliasing-Problem geben. Leider liegt dieser Code außerhalb Ihrer Kontrolle, und mir ist nicht klar, wie Sie diese Funktion überhaupt verwenden können, ohne das strikte Aliasing zu verletzen. Eine Problemumgehung könnte darin bestehen, eine temporäre Variable einzufügen, z. B.void *tmp = R_alloc(); res.target = tmp;
die das Problem im Testfall löst, aber ich bin mir immer noch nicht sicher, ob es legal ist.Ich bin mir jedoch dieser "strengen Aliasing" -Hypothese nicht sicher, da das Kompilieren mit
-fno-strict-aliasing
, mit dem AFAIK gcc solche Konstrukte zulassen soll , das Problem nicht beseitigt!Aktualisieren. Beim Ausprobieren verschiedener Optionen stellte ich fest, dass entweder "korrekter" Code generiert wird
-fno-optimize-strlen
oder-fno-tree-forwprop
wird. Außerdem-O1 -foptimize-strlen
ergibt die Verwendung den falschen Code (aber-O1 -ftree-forwprop
nicht).Nach einer kleinen
git bisect
Übung scheint der Fehler in Commit 34fcf41e30ff56155e996f5e04 eingeführt worden zu sein .Update 2. Ich habe versucht, ein wenig in die gcc-Quelle zu graben, um zu sehen, was ich lernen kann. (Ich behaupte nicht, irgendein Compiler-Experte zu sein!)
Es sieht so aus, als ob der Code in
tree-ssa-strlen.c
dazu gedacht ist, die im Programm angezeigten Zeichenfolgen zu verfolgen. Soweit ich das beurteilen kann, besteht der Fehler darin, dassres.target[0] = "12345678";
der Compiler beim Betrachten der Anweisung die Adresse des Zeichenfolgenliteral"12345678"
mit der Zeichenfolge selbst verknüpft. ( Dies scheint mit diesem verdächtigen Code zu tun zu haben, der im oben genannten Commit hinzugefügt wurde. Wenn versucht wird, die Bytes einer "Zeichenfolge" zu zählen, die tatsächlich eine Adresse ist, wird stattdessen geprüft, auf was diese Adresse verweist.)Es wird also angenommen, dass die Anweisung
res.target[0] = "12345678"
, anstatt die Adresse von"12345678"
an der Adresse zures.target
speichern, die Zeichenfolge selbst an dieser Adresse speichert, als ob die Anweisung wärestrcpy(res.target, "12345678")
. Beachten Sie für die Zukunft, dass dies dazu führen würde, dass die nachfolgende Null an der Adresse gespeichert wirdres.target+8
(zu diesem Zeitpunkt im Compiler sind alle Offsets in Bytes).Wenn der Compiler nun betrachtet
res.target[1] = ""
, behandelt er dies ebenfalls so, als wäre esstrcpy(res.target+8, "")
die 8, die von der Größe von a kommtchar *
. Das heißt, als würde einfach ein Nullbyte an der Adresse gespeichertres.target+8
. Der Compiler "weiß" jedoch, dass die vorherige Anweisung bereits ein Null-Byte an genau dieser Adresse gespeichert hat! Daher ist diese Anweisung "redundant" und kann ( hier ) verworfen werden .Dies erklärt, warum die Zeichenfolge genau 8 Zeichen lang sein muss, um den Fehler auszulösen. (Obwohl andere Vielfache von 8 den Fehler auch in anderen Situationen auslösen können.)
quelle
int*
aber nichtconst char**
.int *
auch illegal (oder besser gesagt, das Speichern vonint
s dort ist illegal).char*
x86_64 gegossen und daran gearbeitet ... Ich sehe hier keine UB, dies ist ein gcc-Fehler.R_alloc()
ist das Programm konform, unabhängig davon, in welcher ÜbersetzungseinheitR_alloc()
definiert ist. Es ist der Compiler, der hier nicht konform ist.