У меня есть пакет 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"
В этот момент скомпилированный код быстро начал segfaulting по следующим направлениям:
*** 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
и заметил что-то странное для меня:
Проходя по выделенному разделу, кажется, что инициализация вторых элементов массивов пропущена ( R_alloc
это обертка вокруг того, malloc
что сам мусор собирает при возврате управления в R; ошибка происходит до возврата в R). Позже, программа падает, когда происходит доступ к неинициализированному элементу (в версии gcc.10.0.1 -O2).
Я исправил это, явно инициализируя рассматриваемый элемент повсюду в коде, что в конечном итоге привело к использованию элемента, но на самом деле его следовало инициализировать пустой строкой, или, по крайней мере, я бы это предположил.
Я что-то упускаю очевидное или делаю что-то глупое? Оба вполне вероятны, поскольку C - мой второй язык на сегодняшний день . Просто странно, что это сейчас произошло, и я не могу понять, что пытается сделать компилятор.
ОБНОВЛЕНИЕ : инструкции, чтобы воспроизвести это, хотя это будет воспроизводиться только до тех пор, пока debian:testing
контейнер докера имеет 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
пытается получить доступ к строке, которая не была инициализирована.
ОБНОВЛЕНИЕ 2 : это очень рекурсивная функция, и, кроме того, бит инициализации строки вызывается много-много раз. В основном это потому, что я был ленив, нам нужно, чтобы строки инициализировались только для того случая, когда мы действительно сталкиваемся с чем-то, о чем мы хотим сообщить в рекурсии, но инициализировать было легче каждый раз, когда можно встретиться с чем-то. Я упоминаю об этом, потому что то, что вы увидите далее, показывает несколько инициализаций, но используется только одна из них (предположительно, с адресом <0x1400000001>).
Я не могу гарантировать, что материал, который я здесь показываю, напрямую связан с элементом, вызвавшим segfault (хотя это тот же недопустимый доступ к адресу), но, как спросил @ nate-eldredge, он показывает, что элемент массива не является инициализируется либо непосредственно перед возвратом, либо сразу после возврата в вызывающей функции. Обратите внимание, что вызывающая функция инициализирует 8 из них, и я показываю их все, все они заполнены либо мусором, либо недоступной памятью.
ОБНОВЛЕНИЕ 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 ):
6.3.2.3 Преобразования Par7> Другие операнды> Указатели
Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель неправильно выровнен 68) для ссылочного типа, поведение не определено.
В противном случае при обратном преобразовании результат сравнивается равным исходному указателю. Когда указатель на объект преобразуется в указатель на тип символа, результат указывает на младший адресуемый байт объекта. Последовательные приращения результата, вплоть до размера объекта, дают указатели на оставшиеся байты объекта.
6.5 Par6 Выражения
Эффективным типом объекта для доступа к его сохраненному значению является объявленный тип объекта, если таковой имеется. 87) Если значение сохраняется в объекте, у которого нет объявленного типа, через lvalue, имеющий тип, который не является символьным типом, то тип lvalue становится эффективным типом объекта для этого доступа и для последующих доступов, которые не изменить сохраненное значение. Если значение копируется в объект, не имеющий объявленного типа, с использованием memcpy или memmove, или копируется в виде массива символьного типа, то эффективный тип измененного объекта для этого доступа и для последующих доступов, которые не изменяют значение, является действующий тип объекта, из которого копируется значение, если оно есть. Для всех других обращений к объекту, не имеющему объявленного типа, эффективным типом объекта является просто тип lvalue, используемого для доступа.
87) Выделенные объекты не имеют объявленного типа.
IIUC R_alloc
возвращает смещение в malloc
блок ed, который гарантированно будет double
выровнен, и размер блока после смещения имеет запрошенный размер (также имеется распределение перед смещением для R конкретных данных). R_alloc
бросает этот указатель (char *)
на возврат.
Раздел 6.2.5 Пар 29
Указатель на void должен иметь те же требования к представлению и выравниванию, что и указатель на тип символа. 48) Аналогично, указатели на квалифицированные или неквалифицированные версии совместимых типов должны иметь одинаковые требования к представлению и выравниванию. Все указатели на типы конструкций должны иметь те же требования к представлению и выравниванию, что и другие.
Все указатели на типы объединения должны иметь те же требования к представлению и выравниванию, что и другие.
Указатели на другие типы не обязательно должны иметь одинаковые требования к представлению или выравниванию.48) Одинаковые требования к представлению и выравниванию подразумевают взаимозаменяемость аргументов функций, возвращаемых значений функций и членов объединений.
Так что вопрос «мы позволили переделано (char *)
в (const char **)
и записи на него как (const char **)
». Мое прочтение вышеизложенного состоит в том, что если указатели в системах, в которых выполняется код, имеют выравнивание, совместимое с double
выравниванием, то все в порядке.
Мы нарушаем "строгий псевдоним"? то есть:
6.5 Пар 7
Объект должен иметь свое сохраненное значение, доступное только через выражение lvalue, которое имеет один из следующих типов: 88)
- тип, совместимый с эффективным типом объекта ...
88) Цель этого списка - указать те обстоятельства, при которых объект может или не может быть псевдонимом.
Итак, что должен думать компилятор о эффективном типе объекта, на который указывает res.target
(или res.current
)? Предположительно заявленный тип (const char **)
, или это на самом деле неоднозначно? Мне кажется, что это не в этом случае только потому, что в области видимости нет другого «lvalue», который обращается к тому же объекту.
Я признаю, что я изо всех сил стараюсь извлечь смысл из этих разделов стандарта.
источник
-mtune=native
оптимизирует для конкретного процессора, который есть на вашей машине. Это будет отличаться для разных тестеров и может быть частью проблемы. Если вы запустите компиляцию вместе с-v
вами, вы сможете увидеть, какое семейство процессоров находится на вашем компьютере (например,-mtune=skylake
на моем компьютере).disassemble
инструкцию внутри GDB.Ответы:
Описание: Это похоже на ошибку в gcc, связанную с оптимизацией строк. Автономный тестовый пример ниже. Сначала были некоторые сомнения относительно правильности кода, но я думаю, что это так.
Я сообщил об ошибке как PR 93982 . Предложенное исправление было совершено, но оно исправляет его не во всех случаях, что привело к последующей проверке PR 94015 ( ссылка на Godbolt ).
Вы должны быть в состоянии обойти ошибку, компилируя с флагом
-fno-optimize-strlen
.Мне удалось сократить ваш тестовый пример до следующего минимального примера (также на Godbolt ):
С gcc trunk (версия gcc 10.0.1 20200225 (экспериментальная)) и
-O2
(все остальные параметры оказались ненужными), генерируемая сборка на amd64 выглядит следующим образом:Таким образом, вы совершенно правы, что компилятор не может инициализироваться
res.target[1]
(обратите внимание на заметное отсутствиеmovq $.LC1, 8(%rax)
).Интересно поиграть с кодом и посмотреть, что влияет на «баг». Возможно, значительно, если изменить тип возвращаемого значения
R_alloc
на,void *
он исчезнет и даст «правильный» вывод сборки. Может быть, менее существенно, но более забавно, изменение строки"12345678"
на более длинную или короткую также заставляет ее исчезнуть.Предыдущее обсуждение, теперь решенное - код, по-видимому, законный.
У меня вопрос, является ли ваш код на самом деле законным. Тот факт , что вы принимаете
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 предполагается сделать НКУ позволяют такие конструкции, это не делает проблему уйти!Обновить. Попробовав несколько разных опций, я обнаружил, что любой из них
-fno-optimize-strlen
или-fno-tree-forwprop
приведет к созданию «правильного» кода. Кроме того, использование-O1 -foptimize-strlen
дает неверный код (но-O1 -ftree-forwprop
не делает).После небольшого
git bisect
упражнения ошибка, кажется, была введена в коммите 34fcf41e30ff56155e996f5e04 .Обновление 2. Я попытался немного покопаться в исходном коде gcc, чтобы посмотреть, чему я могу научиться. (Я не претендую на звание эксперта по компиляции!)
Похоже, код
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, приходящийся на размер achar *
. То есть как будто он просто хранит нулевой байт по адресуres.target+8
. Однако компилятор «знает», что предыдущий оператор уже сохранил нулевой байт по этому самому адресу! Таким образом, это утверждение является «избыточным» и может быть отброшено ( здесь ).Это объясняет, почему строка должна быть ровно 8 символов, чтобы вызвать ошибку. (Хотя другие кратные 8 также могут вызвать ошибку в других ситуациях.)
источник
int*
но не надоconst char**
.int *
также является незаконным (или, скорее, фактически хранениеint
s там незаконно).char*
и работаете с x86_64 ... Я не вижу здесь UB, это ошибка gcc.R_alloc()
, программа соответствует, независимо от того, в какой единице переводаR_alloc()
определена. Это компилятор, который не соответствует здесь.