g cc -10.0.1 Specifi c Segfault - PullRequest
       19

g cc -10.0.1 Specifi c Segfault

23 голосов
/ 26 февраля 2020

У меня есть R пакет с C скомпилированным кодом, который был относительно стабильным в течение долгого времени и часто тестируется на широком спектре платформ и компиляторов (windows / osx / debian / fedora gcc / clang).

В последнее время была добавлена ​​новая платформа для повторного тестирования пакета:

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"

В этот момент скомпилированный код сразу начал сегрегировать по следующим строкам:

 *** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'

Мне удалось последовательно воспроизвести segfault с помощью контейнера rocker/r-base docker с gcc-10.0.1 с уровнем оптимизации -O2. Запуск более низкой оптимизации избавляет от проблемы. Запуск любой другой установки, в том числе и под valgrind (как -O0, так и -O2), UBSAN (gcc / clang), не показывает никаких проблем. Я также вполне уверен, что это работает под gcc-10.0.0, но у меня нет данных.

Я запустил версию gcc-10.0.1 -O2 с gdb и заметил кое-что, что мне кажется странным:

gdb vs code

При переходе по выделенному разделу кажется, что инициализация вторых элементов массивов пропускается (R_alloc является оберткой вокруг malloc, что само по себе мусор собирается при возврате управления в R, ошибка происходит перед возвратом в R). Позже, программа падает, когда происходит доступ к неинициализированному элементу (в версии g cc .10.0.1 -O2).

Я исправил это, явно инициализировав соответствующий элемент везде в коде, который в конечном итоге это привело к использованию элемента, но на самом деле его следовало инициализировать пустой строкой, или, по крайней мере, это то, что я бы предположил.

Я что-то упускаю из виду или совершаю глупости? Оба вполне вероятны, так как C мой второй язык на далеко . Просто странно, что это только что появилось, и я не могу понять, что пытается сделать компилятор.


UPDATE : инструкции по воспроизведению этого, хотя это будет воспроизводить только до тех пор, пока debian:testing docker контейнер имеет gcc-10 в gcc-10.0.1. Кроме того, не следует просто запускать эти команды, если вы мне не доверяете .

Извините, это не минимальный воспроизводимый пример.

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

Тогда в Консоль R, после ввода run, чтобы получить gdb для запуска программы:

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)

Проверка в GDB довольно быстро показывает (если я правильно понимаю), что CSR_strmlen_x пытается получить доступ к строке, которая не был инициализирован.

UPDATE 2 : это очень рекурсивная функция, и, кроме того, бит инициализации строки вызывается много, много раз. В основном это b / c Я был ленив, нам нужно, чтобы строки инициализировались только для того момента, когда мы действительно сталкиваемся с чем-то, о чем мы хотим сообщить в рекурсии, но проще было инициализировать каждый раз, когда можно встретиться с чем-то. Я упоминаю об этом, потому что то, что вы увидите далее, показывает несколько инициализаций, но используется только одна из них (предположительно, с адресом <0x1400000001>).

Я не могу гарантировать, что я показ здесь напрямую связан с элементом, который вызвал segfault (хотя это тот же недопустимый доступ к адресу), но, как спросил @ nate-eldredge, он показывает, что элемент массива не инициализируется ни перед возвратом, ни сразу после возврата в вызывающая функция. Обратите внимание, что вызывающая функция инициализирует 8 из них, и я показываю их все, все они заполнены либо мусором, либо недоступной памятью.

enter image description here

ОБНОВЛЕНИЕ 3 , разборка рассматриваемой функции:

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.

ОБНОВЛЕНИЕ 4 :

Итак, попытка разобрать стандарт здесь являются частями это кажется уместным ( C11 draft ):

6.3.2.3 Преобразования Par7> Другие операнды> Указатели

Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если полученный указатель неверен выровненный 68) для ссылочного типа поведение не определено.
В противном случае при обратном преобразовании результат будет сравниваться равным исходному указателю. Когда указатель на объект преобразуется в указатель на символьный тип, результат указывает на младший адресуемый байт объекта. Последовательные приращения результата, вплоть до размера объекта, дают указатели на оставшиеся байты объекта.

6.5 Выражения Par6

Эффективный тип Объект для доступа к его сохраненному значению является объявленным типом объекта, если таковой имеется. 87) Если значение сохраняется в объекте, у которого нет объявленного типа, через lvalue, имеющий тип, который не является символьным типом, то тип lvalue становится эффективным типом объекта для этого доступа и для последующих доступов, которые не изменить сохраненное значение. Если значение копируется в объект, не имеющий объявленного типа, с использованием memcpy или memmove, или копируется как массив символьного типа, то эффективный тип измененного объекта для этого доступа и для последующих доступов, которые не изменяют значение, является действующий тип объекта, из которого копируется значение, если оно есть. Для всех других обращений к объекту, не имеющему объявленного типа, эффективным типом объекта является просто тип lvalue, используемого для доступа.

87) У выделенных объектов нет объявленного типа .

IIU C R_alloc возвращает смещение в malloc ed блок, который гарантированно выровнен double, а размер блока после смещения соответствует запрошенному размер (есть также распределение перед смещением для данных R, специфицированных c). R_alloc возвращает этот указатель на (char *) при возврате.

Раздел 6.2.5 Par 29

Указатель на void должен иметь те же требования к представлению и выравниванию, что и указатель на тип персонажа. 48) Аналогично, указатели на квалифицированные или неквалифицированные версии совместимых типов должны иметь одинаковые требования к представлению и выравниванию. Все указатели на типы структуры должны иметь те же требования к представлению и выравниванию, что и друг друга.
Все указатели на типы объединения должны иметь те же требования к представлению и выравниванию, что и другие.
Указатели на другие типы не обязательно должны иметь одинаковое представление. или требования к выравниванию.

48) Те же требования к представлению и выравниванию подразумевают взаимозаменяемость аргументов функций, возвращаемых значений из функций и членов объединений.

Таким образом, вопрос msgstr "нам разрешено переписать (char *) в (const char **) и записать в него как (const char **)". Мое прочтение вышеизложенного состоит в том, что если указатели в системах, в которых выполняется код, имеют выравнивание, совместимое с выравниванием double, то это нормально.

Нарушаем ли мы "строгий псевдоним"? то есть:

6.5 Par 7

Доступ к сохраненному значению объекта должен осуществляться только через выражение lvalue одного из следующих типов: 88)

- тип, совместимый с действующим типом объекта ...

88) Цель этого списка - указать те обстоятельства, при которых объект может или не может иметь псевдоним.

* 1136 Итак, что должен думать компилятор о эффективном типе объекта, на который указывает res.target (или res.current)? Предположительно заявленный тип (const char **), или это на самом деле неоднозначно? Мне кажется, что это не в этом случае только потому, что в области видимости нет другого «lvalue», который обращается к тому же объекту.

Я признаю, что изо всех сил пытаюсь извлечь смысл из этих разделов стандарта.

1 Ответ

22 голосов
/ 27 февраля 2020

Резюме: Это ошибка в g cc, связанная с оптимизацией строк. Автономный тестовый пример ниже. Сначала были некоторые сомнения относительно правильности кода, но я думаю, что это так.

Я сообщил об ошибке как PR 93982 . Предложенное исправление было совершено , но оно исправляет его не во всех случаях, что привело к последующей проверке PR 94015 ( Godbolt link ).

Вы должны быть в состоянии обойти ошибку, скомпилировав с флагом -fno-optimize-strlen.


Я смог сократить ваш тестовый пример до следующего минимального примера (также на godbolt ):

struct a {
    const char ** target;
};

char* R_alloc(void);

struct a foo(void) {
    struct a res;
    res.target = (const char **) R_alloc();
    res.target[0] = "12345678";
    res.target[1] = "";
    res.target[2] = "";
    res.target[3] = "";
    res.target[4] = "";
    return res;
}

С g cc trunk (g cc версия 10.0.1 20200225 (экспериментальная)) и -O2 (все остальные параметры оказались ненужными), сгенерированная сборка на amd64 выглядит следующим образом:

.LC0:
        .string "12345678"
.LC1:
        .string ""
foo:
        subq    $8, %rsp
        call    R_alloc
        movq    $.LC0, (%rax)
        movq    $.LC1, 16(%rax)
        movq    $.LC1, 24(%rax)
        movq    $.LC1, 32(%rax)
        addq    $8, %rsp
        ret

Так что вы совершенно правы, что компилятор не может инициализировать res.target[1] (обратите внимание на заметное отсутствие movq $.LC1, 8(%rax)).

Интересно поиграйте с кодом и посмотрите, что влияет на «баг». Возможно, значительно, если изменить тип возвращаемого значения с R_alloc на void *, он уйдет на go и даст «правильный» вывод сборки. Может быть, менее значительным, но более забавным, изменение строки "12345678" на более длинную или короткую также приводит к ее удалению go.


Предыдущее обсуждение, теперь разрешенное - код, по-видимому, допустим .

У меня вопрос, является ли ваш код на самом деле законным. Тот факт, что вы берете char *, возвращаемое R_alloc() и приводите его к const char **, а затем сохраняете const char *, кажется, что он может нарушать правило строгого алиасинга , как char и const char * не совместимые типы. Существует исключение, которое позволяет вам получить доступ к любому объекту как char (для реализации таких вещей, как memcpy), но это наоборот, и, насколько я понимаю, это не разрешено. Это заставляет ваш код вызывать неопределенное поведение, и поэтому компилятор может на законных основаниях делать все, что ему захочется.

Если это так, исправление будет правильным для R, который изменит свой код так, что R_alloc() вернет void * вместо char *. Тогда не было бы проблемы с наложением. К сожалению, этот код находится вне вашего контроля, и мне не ясно, как вы можете использовать эту функцию вообще, не нарушая строгий псевдоним. Обходной путь может заключаться во вставке временной переменной, например, void *tmp = R_alloc(); res.target = tmp;, которая решает проблему в тестовом примере, но я все еще не уверен, допустимо ли это.

Однако я не уверен в этом "строгом" гипотеза псевдонима, потому что компиляция с -fno-strict-aliasing, которую AFAIK должен сделать g cc допускающим такие конструкции, не делает проблему go прочь!


Обновление. Попробовав несколько разных опций, я обнаружил, что либо -fno-optimize-strlen, либо -fno-tree-forwprop приведет к генерации "правильного" кода. Кроме того, использование -O1 -foptimize-strlen дает неправильный код (но -O1 -ftree-forwprop - нет).

После небольшого git bisect упражнения, похоже, ошибка была введена в commit 34fcf41e30ff56155e996f5e04 .


Обновление 2. I попытался немного покопаться в источнике g cc, чтобы посмотреть, чему я могу научиться. (Я не претендую на звание эксперта по компилятору!)

Похоже, код в tree-ssa-strlen.c предназначен для отслеживания появления строк в программе. Насколько я могу судить, ошибка в том, что при взгляде на оператор res.target[0] = "12345678"; компилятор связывает адрес строкового литерала "12345678" с самой строкой. (Похоже, что это связано с этим подозрительным кодом , который был добавлен в вышеупомянутом коммите, где, если он пытается подсчитать байты "строки", которая на самом деле является адресом, он вместо этого смотрит на этот адрес указывает на.)

Таким образом, он считает, что оператор res.target[0] = "12345678" вместо сохранения адреса из "12345678" по адресу res.target сохраняет саму строку по этому адресу , как будто заявление было strcpy(res.target, "12345678"). Обратите внимание, что еще впереди, это приведет к тому, что конечный ноль будет храниться по адресу res.target+8 (на данном этапе в компиляторе все смещения в байтах).

Теперь, когда компилятор смотрит на res.target[1] = "", он также обрабатывает это так, как если бы он был strcpy(res.target+8, ""), 8 - это размер char *. То есть как будто он просто хранит нулевой байт по адресу res.target+8. Однако компилятор «знает», что предыдущий оператор уже сохранил нулевой байт по этому самому адресу! Таким образом, этот оператор является «избыточным» и может быть отброшен ( здесь ).

Это объясняет, почему длина строки должна быть ровно 8 символов, чтобы вызвать ошибку. (Хотя другие кратные 8 также могут вызвать ошибку в других ситуациях.)

...