У меня есть пакет 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.