[Перевод] Выявление ошибок в работе с памятью в C и C++: Сравниваем Sanitizers и Valgrind

0d2d6d62928378eaedbba6b28bd16f95.png

В этой статье вашему вниманию представлено сравнение двух инструментов для поиска ошибок в работе с памятью в программах, написанных на memory-unsafe (небезопасных при работе с памятью) языках — Sanitizers и Valgrind. Эти два инструмента работают совершенно по-разному. Поэтому, хоть Sanitizers (разработанный инженерами Google) имеет ряд преимуществ перед Valgrind, у каждого из них есть свои сильные и слабые стороны. Следует сразу отметить, что проект Sanitizers имеет название во множественном числе, потому что он состоит из нескольких инструментов, которые мы рассмотрим в этой статье в совокупности.

В отличии от Java, Python и подобных им языков с безопасным доступом к памяти, языкам с прямым доступом к памяти, таким как C и C++, нужны специальные инструменты для выявления ошибок в работе с ней. В memory-unsafe языке довольно легко по ошибке записать данные в конец буфера памяти или считать память после того, как она была высвобождена. Программы, содержащие такие ошибки, могут большую часть времени работать совершенно нормально, завершаясь крашем лишь в редких ситуациях. Вылавливать такие ошибки самому очень сложно, поэтому для этого нужны специальные инструменты.

Начать можно с того, что Valgrind замедляет работу программ гораздо сильнее, чем Sanitizers. Программа, запущенная под Valgrind, может работать от 20 до 50 раз медленнее, чем в обычном режиме. Это может стать серьезным препятствием для программ с интенсивными вычислениями. Замедление работы в Sanitizers обычно укладывается в 2–4 раза в сравнении с обычной работой. Вместо Valgrind вы можете указать использование Sanitizers во время компиляции.

Краткая инструкция к Sanitizers

Приведенный ниже список рекомендаций подытоживает некоторые сведения из этой статьи и может пригодиться читателям, уже знакомым с Sanitizers:

  • Опции Clang/GCC:

-fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all -fsanitize=float-divide-by-zero -fsanitize=float-cast-overflow -fno-sanitize=null -fno-sanitize=alignment
  • Для LLDB/GDB и для предотвращения очень коротких стек-трейсов и, как правило, ложного обнаружения утечек:

$ export ASAN_OPTIONS=abort_on_error=1:fast_unwind_on_malloc=0:detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1
$ export G_SLICE=always-malloc G_DEBUG=gc-friendly
  • Опция Clang для перехвата чтения неинициализированной памяти: -fsanitize=memory. Эту опцию нельзя комбинировать с -fsanitize=address.

  • В rpmbuild .spec файлах следует дополнительно использовать: -Wp,-U_FORTIFY_SOURCE.

Преимущества в производительности Sanitizers 

Valgrind использует динамическое, а не статическое инструментирование во время компиляции, что приводит к большим накладным расходам, которые могут быть нецелесообразны для приложений с интенсивным использованием процессора. Sanitizers использует статическое инструментирование и позволяет выполнять аналогичные проверки с меньшими накладными расходами.

В таблице 1 представлено довольно подробное сравнение, демонстрирующее возможности и влияние на время выполнения программы обоих наборов инструментов.

В моих тестах Valgrind тратит 23 секунды при запуске на чтение системных файлов отладочной информации. Эту нагрузку можно временно снизить, переименовав каталог /usr/lib/debug. Только не забудьте переименовать его обратно! В противном случае системная отладочная информация может пропасть и в то же время перестать устанавливаться (так как она уже установлена):

$ sudo mv /usr/lib/debug /usr/lib/debug-x; sleep 1h; sudo mv /usr/lib/debug-x /usr/lib/debug

Тесты на замедление были суомпилироанны с помощью следующей команды:

$ clang++ -g -O3 -march=native -ffast-math ...
clang-11.0.0-2.fc33.x86_64
valgrind-3.16.1-5.fc33.x86_64

Команды использовали обычные -fsanitize=address, -fsanitize=undefined или -fsanitize=thread без каких-либо дополнительных опций, которые я предлагал в инструкции к Sanitizers.

В этой статье используется компилятор Clang. GCC имеет такую же поддержку Sanitizers, за исключением того, что в компиляторе отсутствует поддержка -fsanitize=memory (мы обсуждим это в разделе MSAN: чтение неинициализированной памяти).

Установка debuginfo

В примерах, приведенных в этой статье, предполагается, что файлы *-debuginfo.rpm уже установлены. Вы можете установить их с помощью команды dnf debuginfo-install packagename. В некоторых версиях Red Hat Enterprise Linux (RHEL) вместо dnf нужно использовать yum. Когда вы запустите программу в gdb, она сразу же сообщит вам, если вам не хватает каких-либо RPM:

$ cat >vector.cpp <<'EOF'
// Здесь должна быть ваша программа, которую вы отлаживаете,
// этот код с std::vector является лишь примером.
#include 
int main() {
  std::vector v;
}
EOF
$ clang++ -o vector vector.cpp -Wall -g
$ gdb ./vector
Reading symbols from ./vector...
(gdb) start
...
Missing separate debuginfos, use: dnf debuginfo-install glibc-2.32-2.fc33.x86_64
...
Missing separate debuginfos, use: dnf debuginfo-install libgcc-10.2.1-9.fc33.x86_64 libstdc++-10.2.1-9.fc33.x86_64
(gdb) quit
$ sudo dnf debuginfo-install glibc-2.32-2.fc33.x86_64
...
$ sudo dnf debuginfo-install libgcc-10.2.1-9.fc33.x86_64 libstdc++-10.2.1-9.fc33.x86_64
...

Пример ситуации: Переполнение буфера

Ниже мы видим вполне себе нормальную программу, за исключением наличия ошибки выхода за пределы буфера. Хотя ошибка здесь выглядит очевидной, в реальных программах такие ошибки куда более скрыты и их трудно найти. Разработчики на языке C обычно используют Valgrind Memcheck, который может отловить большинство подобных ошибок. Запуск программы с Valgrind Memcheck показывает:

$ cat >overrun.c <<'EOF'
#include 
int main() {
  char *p = malloc(16);
  p[24] = 1; // выход за пределы буфера, в p всего 16 байт
  free(p); // free(): некорректный указатель
  return 0;
}
EOF
$ clang -o overrun overrun.c -Wall -g
$ valgrind ./overrun
...
==60988== Invalid write of size 1
==60988==    at 0x401154: main (overrun.c:4)
==60988==  Address 0x4a52058 is 8 bytes after a block of size 16 alloc'd
==60988==    at 0x4839809: malloc (vg_replace_malloc.c:307)
==60988==    by 0x401147: main (overrun.c:3)

Примечание: Вы можете посмотреть на этот код и его вывод в Compiler Explorer.

Поскольку программа прекрасно работает без Valgrind, можно подумать, что никакой ошибки там может и не быть. После добавления кода, имитирующего нетривиальную программу, пример падает даже при чистом запуске. Обратите внимание, что сообщение free(): invalid pointer поступает из glibc (стандартной системной библиотеки GNU C), а не из инструментации Sanitizers или Valgrind:

$ cat >overrun.c <<'EOF'
#include 
int main() {
  char *p = malloc(16);
  char *p2 = malloc(16);
  p[24] = 1; // выход за пределы буфера, в p всего 16 байт
  free(p2); // free(): некорректный указатель
  free(p);
  return 0;
}
EOF
$ clang -o overrun overrun.c -Wall -g
$ ./overrun
free(): invalid pointer
Aborted

Примечание: Вы можете посмотреть на этот код и его вывод в Compiler Explorer.

Инструментом в Sanitizers, соответствующим использованию Valgrind для этой ошибки, является AddressSanitizer. Разница в том, что если Valgrind работает с обычными исполняемыми файлами, то AddressSanitizer требует перекомпиляции кода, который затем выполняется напрямую без дополнительных инструментов:

$ clang -o overrun overrun.c -Wall -g -fsanitize=address
$ ./overrun
=================================================================
==61268==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000028 at pc 0x0000004011b8 bp 0x7fff37c8aa70 sp 0x7fff37c8aa68
WRITE of size 1 at 0x602000000028 thread T0
    #0 0x4011b7 in main overrun.c:4
    #1 0x7f4c94a2d1e1 in __libc_start_main ../csu/libc-start.c:314
    #2 0x4010ad in _start (overrun+0x4010ad)


0x602000000028 is located 8 bytes to the right of 16-byte region [0x602000000010,0x602000000020)
allocated by thread T0 here:
    #0 0x7f4c94c7b3cf in __interceptor_malloc (/lib64/libasan.so.6+0xab3cf)
    #1 0x401177 in main overrun.c:3
    #2 0x7f4c94a2d1e1 in __libc_start_main ../csu/libc-start.c:314


SUMMARY: AddressSanitizer: heap-buffer-overflow overrun.c:4 in main
...

Примечание: Вы можете посмотреть на этот код и его вывод в Compiler Explorer.

ASAN: Выход за границы переменных стека

Поскольку Valgrind не требует перекомпиляции программы, он не может обнаружить некоторые некорректные обращения к памяти. Одной из таких ошибок является обращение к памяти вне диапазона автоматических (локальных) переменных и глобальных переменных (см. документацию по AddressSanitizer Stack Out of Bounds).

Поскольку Valgrind подключается к работе только во время выполнения программы, он отлавливает и отслеживает память от выделений malloc. К сожалению, выделение переменных в стеке является неотъемлемой частью уже скомпилированной программы без вызова каких-либо внешних функций, таких как malloc, поэтому Valgrind не может выяснить, является ли обращение к стековой памяти корректным. Sanitizers, с другой стороны, проверяют весь код во время компиляции, когда компилятор еще знает, к какой именно переменной в стеке пытается обратиться программа и каковы правильные границы в стека для этой переменной:

$ cat >stack.c <<'EOF'
int main(int argc, char **argv) {
  int a[100];
  return a[argc + 100];
}
EOF
$ clang -o stack stack.c -Wall -g -fsanitize=address
$ ./stack
==88682==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff54f500f4 at pc 0x0000004f4c51 bp 0x7fff54f4ff30 sp 0x7fff54f4ff28
READ of size 4 at 0x7fff54f500f4 thread T0
    #0 0x4f4c50 in main /tmp/stack.c:3:10
    #1 0x7f9983c7e1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #2 0x41c41d in _start (/tmp/stack+0x41c41d)


Address 0x7fff54f500f4 is located in stack of thread T0 at offset 436 in frame
    #0 0x4f4a9f in main /tmp/stack.c:1


  This frame has 1 object(s):
    [32, 432) 'a' (line 2) <== Memory access at offset 436 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /tmp/stack.c:3:10 in main
...
$ clang -o stack stack.c -Wall -g
$ valgrind ./stack
...
(nothing found by Valgrind)

Примечание: Вы можете посмотреть этот код и его вывод в Compiler Explorer.

ASAN: Выход за границы глобальных переменных

Как и в случае с переменными в стеке, Valgrind не может обнаружить выход за границы глобальной переменной, поскольку не перекомпилирует программу (см. документацию AddressSanitizer Global Out of Bounds).

Я уже описал выше, почему Valgrind не может отловить такие ошибки. Вот результаты работы AddressSanitizer и Valgrind:

$ cat >global.c <<'EOF'
int a[100];
int main(int argc, char **argv) {
  return a[argc + 100];
}
EOF
$ clang -o global global.c -Wall -g -fsanitize=address
$ ./global
=================================================================
==88735==ERROR: AddressSanitizer: global-buffer-overflow on address 0x000000dcee74 at pc 0x0000004f4b04 bp 0x7ffd5292b580 sp 0x7ffd5292b578
READ of size 4 at 0x000000dcee74 thread T0
    #0 0x4f4b03 in main /tmp/global.c:3:10
    #1 0x7fd416cda1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #2 0x41c41d in _start (/tmp/global+0x41c41d)


0x000000dcee74 is located 4 bytes to the right of global variable 'a' defined in 'global.c:1:5' (0xdcece0) of size 400
SUMMARY: AddressSanitizer: global-buffer-overflow /tmp/global.c:3:10 in main
...
$ clang -o global global.c -Wall -g
$ valgrind ./global
...
(nothing found by Valgrind)

Примечание: Вы можете посмотреть этот код и его вывод в Compiler Explorer.

MSAN: чтение неинициализированной памяти

AddressSanitizer не обнаруживает чтение неинициализированной памяти. Для этого был разработан MemorySanitizer. Он требует отдельной компиляции и запуска (см. документацию по MemorySanitizer). Почему AddressSanitizer не был разработан с учетом функциональности MemorySanitizer, мне неясно (и не мне одному).

В результате запуска MemorySanitizer мы увидим:

$ cat >uninit.c <<'EOF'
int main(int argc, char **argv) {
  int a[2];
  if (a[argc != 1])
    return 1;
  else
    return 0;
}
EOF
$ clang -o uninit uninit.c -Wall -g -fsanitize=address -fsanitize=memory
clang-11: error: invalid argument '-fsanitize=address' not allowed with '-fsanitize=memory'
$ clang -o uninit uninit.c -Wall -g -fsanitize=memory
$ ./uninit
==63929==WARNING: MemorySanitizer: use-of-uninitialized-value
    #0 0x4985a9 in main /tmp/uninit.c:3:7
    #1 0x7f93e232c1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #2 0x41c39d in _start (/tmp/uninit+0x41c39d)
SUMMARY: MemorySanitizer: use-of-uninitialized-value /tmp/uninit.c:3:7 in main

Отловить эту ошибку проще с помощью Valgrind, который по умолчанию сообщает о чтении из неинициализированной памяти:

$ clang -o uninit uninit.c -Wall -g
$ valgrind ./uninit
...
==87991== Conditional jump or move depends on uninitialised value(s)
==87991==    at 0x401136: main (uninit.c:3)
...

Примечание: Вы можете посмотреть этот код и его вывод в Compiler Explorer.

ASAN: Использование стека после возврата

AddressSanitizer требует включения ASAN_OPTIONS=detect_stack_use_after_return=1 во время выполнения, поскольку эта функция накладывает дополнительные накладные расходы во время выполнения (см. документацию по AddressSanitizer Use After Return). Ниже приведен пример программы, которая запускается без ошибок сама или с помощью Valgrind, но обнажает ошибку при запуске с AddressSanitizer:

$ cat >uar.cpp <<'EOF'
int *f() {
  int i = 42;
  int *p = &i;
  return p;
}
int g(int *p) {
  return *p;
}
int main() {
  return g(f());
}
EOF
$ clang++ -o uar uar.cpp -Wall -g -fsanitize=address
$ ./uar
(nothing found by default)
$ ASAN_OPTIONS=detect_stack_use_after_return=1 ./uar
=================================================================
==164341==ERROR: AddressSanitizer: stack-use-after-return on address 0x7fb71a561020 at pc 0x0000004f78e1 bp 0x7ffc299184c0 sp 0x7ffc299184b8
READ of size 4 at 0x7fb71a561020 thread T0
    #0 0x4f78e0 in g(int*) /home/lace/src/uar.cpp:7:10
    #1 0x4f790b in main /home/lace/src/uar.cpp:10:10
    #2 0x7fb71dbde1e1 in __libc_start_main (/lib64/libc.so.6+0x281e1)
    #3 0x41c41d in _start (/home/lace/src/uar+0x41c41d)


Address 0x7fb71a561020 is located in stack of thread T0 at offset 32 in frame
    #0 0x4f771f in f() /home/lace/src/uar.cpp:1


  This frame has 1 object(s):
    [32, 36) 'i' (line 2) <== Memory access at offset 32 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-return /home/lace/src/uar.cpp:7:10 in g(int*)
...
$ clang++ -o uar uar.cpp -Wall -g
$ valgrind ./uar
...
(nothing found by Valgrind)

UBSAN: Неопределенное поведение

UndefinedBehaviorSanitizer защищает код от вычислений, запрещенных стандартом языка (см. документацию по UndefinedBehaviorSanitizer). Из соображений производительности некоторые неопределенные вычисления могут не отлавливаться во время выполнения, но никто не может ничего гарантировать о программе, если они имеют место в коде. Чаще всего такие числовые выражения просто вычисляют неожиданный результат. UndefinedBehaviorSanitizer может обнаружить и сообщить о таких операциях.

UndefinedBehaviorSanitizer можно использовать вместе с наиболее распространенным Sanitizer«ом, AddressSanitizer:

$ cat >undefined.cpp <<'EOF'
int main(int argc, char **argv) {
  return 0x7fffffff + argc;
}
EOF
$ clang++ -o undefined undefined.cpp -Wall -g -fsanitize=undefined
$ export UBSAN_OPTIONS=print_stacktrace=1
$ ./undefined
undefined.cpp:2:21: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
    #0 0x429269 in main /tmp/undefined.cpp:2:21
    #1 0x7f1212a3e1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #2 0x40345d in _start (/tmp/undefined+0x40345d)


SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior undefined.cpp:2:21 in
$ valgrind ./undefined
...
(nothing found by Valgrind)

Примечание: Вы можете посмотреть этот код и его вывод в Compiler Explorer.

Лично я предпочитаю прерывать работу программы при первом же таком проявлении, потому что иначе ошибку найти будет очень трудно. Поэтому я использую -fno-sanitize-recover=all. Я также предпочитаю немного расширить покрытие UndefinedBehaviorSanitizer, включив в него: -fsanitize=float-divide-by-zero -fsanitize=float-cast-overflow.

LSAN: Утечки памяти

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

$ cat >leak.cpp <<'EOF'
#include 
int main() {
  void *p = malloc(10);
  return p == nullptr;
}
EOF
$ clang++ -o leak leak.cpp -Wall -g -fsanitize=address
$ ./leak
=================================================================
==188539==ERROR: LeakSanitizer: detected memory leaks


Direct leak of 10 byte(s) in 1 object(s) allocated from:
    #0 0x4bfcdf in malloc (/tmp/leak+0x4bfcdf)
    #1 0x4f7728 in main /tmp/leak.cpp:3:13
    #2 0x7fd5a7a781e1 in __libc_start_main (/lib64/libc.so.6+0x281e1)


SUMMARY: AddressSanitizer: 10 byte(s) leaked in 1 allocation(s).
$ clang++ -o leak leak.cpp -Wall -g
$ valgrind --leak-check=full ./leak
...
==188524== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==188524==    at 0x4839809: malloc (vg_replace_malloc.c:307)
==188524==    by 0x401148: main (leak.cpp:3)
...

Примечание: Вы можете посмотреть этот код и его вывод в Compiler Explorer.

LSAN: Утечки памяти при использовании определенных библиотек (glib2)

Некоторые фреймворки имеют собственные аллокаторы памяти, которые не позволяют LeakSanitizer выполнять свою работу. В следующем примере используется подобный фреймворк — glib2 (не glibc). Другие библиотеки могут иметь другие опции времени выполнения или компиляции. Вот так будут выглядеть результаты работы LeakSanitizer и Valgrind:

$ cat >gc.c <<'EOF'
#include 
int main(void) {
    GHashTable *ht = g_hash_table_new(g_str_hash, g_str_equal);
    g_hash_table_insert(ht, "foo", "bar");
//    g_hash_table_destroy(ht); // утечка памяти в glib2
    g_malloc(100); // прямая утечка памяти
    return 0;
}
EOF
$ clang -o gc gc.c -Wall -g $(pkg-config --cflags --libs glib-2.0) -fsanitize=address
$ ./gc
=================================================================
==233215==ERROR: LeakSanitizer: detected memory leaks


Direct leak of 100 byte(s) in 1 object(s) allocated from:
    #0 0x4bfd2f in malloc (/tmp/gc+0x4bfd2f)
    #1 0x7f1fcf12b908 in g_malloc (/lib64/libglib-2.0.so.0+0x5b908)
    #2 0x7f1fced961e1 in __libc_start_main (/lib64/libc.so.6+0x281e1)


SUMMARY: AddressSanitizer: 100 byte(s) leaked in 1 allocation(s).
$ clang -o gc gc.c -Wall -g $(pkg-config --cflags --libs glib-2.0)
$ valgrind --leak-check=full ./gc
...
==233250== 100 bytes in 1 blocks are definitely lost in loss record 8 of 11
==233250==    at 0x4839809: malloc (vg_replace_malloc.c:307)
==233250==    by 0x48DF908: g_malloc (in /usr/lib64/libglib-2.0.so.0.6600.3)
==233250==    by 0x4011C5: main (gc.c:6)
==233250==
==233250== 256 (96 direct, 160 indirect) bytes in 1 blocks are definitely lost in loss record 9 of 11
==233250==    at 0x4839809: malloc (vg_replace_malloc.c:307)
==233250==    by 0x48DF908: g_malloc (in /usr/lib64/libglib-2.0.so.0.6600.3)
==233250==    by 0x48F71C1: g_slice_alloc (in /usr/lib64/libglib-2.0.so.0.6600.3)
==233250==    by 0x48C5A51: g_hash_table_new_full (in /usr/lib64/libglib-2.0.so.0.6600.3)
==233250==    by 0x401197: main (gc.c:3)
...

LeakSanitizer об утечке хэш-таблицы не сообщает, в то время как Valgrind о ней сообщает. Это происходит потому, что glib2 специально обнаруживает Valgrind и в присутствии Valgrind отключает свой собственный аллокатор памяти (g_slice). Однако можно сделать glib2 пригодной для отладки даже с применением LeakSanitizer:

$ clang -o gc gc.c -Wall -g $(pkg-config --cflags --libs glib-2.0) -fsanitize=address
# otherwise the backtraces would have only 2 entries:
$ export ASAN_OPTIONS=fast_unwind_on_malloc=0
# Show all glib2 memory leaks:
$ export G_SLICE=always-malloc G_DEBUG=gc-friendly
$ ./gc
=================================================================
==233921==ERROR: LeakSanitizer: detected memory leaks


Direct leak of 100 byte(s) in 1 object(s) allocated from:
    #0 0x4bfd2f in malloc (/tmp/gc+0x4bfd2f)
    #1 0x7f2a7c302908 in g_malloc ../glib/gmem.c:106:13
    #2 0x4f4b35 in main /tmp/gc.c:6:5
    #3 0x7f2a7bf6d1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #4 0x41c46d in _start (/tmp/gc+0x41c46d)


Direct leak of 96 byte(s) in 1 object(s) allocated from:
    #0 0x4bfd2f in malloc (/tmp/gc+0x4bfd2f)
    #1 0x7f2a7c302908 in g_malloc ../glib/gmem.c:106:13
    #2 0x7f2a7c31a1c1 in g_slice_alloc ../glib/gslice.c:1069:11
    #3 0x7f2a7c2e8a51 in g_hash_table_new_full ../glib/ghash.c:1072:16
    #4 0x4f4b07 in main /tmp/gc.c:3:22
    #5 0x7f2a7bf6d1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #6 0x41c46d in _start (/tmp/gc+0x41c46d)


Indirect leak of 32 byte(s) in 1 object(s) allocated from:
    #0 0x4bfd2f in malloc (/tmp/gc+0x4bfd2f)
    #1 0x7f2a7c302908 in g_malloc ../glib/gmem.c:106:13
    #2 0x7f2a7c317ce1  ../glib/gstrfuncs.c:392:17
    #3 0x7f2a7c317ce1 in g_memdup ../glib/gstrfuncs.c:385:1
    #4 0x7f2a7c2e8b65 in g_hash_table_ensure_keyval_fits ../glib/ghash.c:974:36
    #5 0x7f2a7c2e8b65 in g_hash_table_insert_node ../glib/ghash.c:1327:3
    #6 0x7f2a7c2e930f in g_hash_table_insert_internal ../glib/ghash.c:1601:10
    #7 0x7f2a7c2e930f in g_hash_table_insert ../glib/ghash.c:1630:10
    #8 0x4f4b28 in main /tmp/gc.c:4:5
    #9 0x7f2a7bf6d1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #10 0x41c46d in _start (/tmp/gc+0x41c46d)


Indirect leak of 32 byte(s) in 1 object(s) allocated from:
    #0 0x4bfed7 in calloc (/tmp/gc+0x4bfed7)
    #1 0x7f2a7c302e20 in g_malloc0 ../glib/gmem.c:136:13
    #2 0x7f2a7c2e50ef in g_hash_table_setup_storage ../glib/ghash.c:592:24
    #3 0x7f2a7c2e8a90 in g_hash_table_new_full ../glib/ghash.c:1084:3
    #4 0x4f4b07 in main /tmp/gc.c:3:22
    #5 0x7f2a7bf6d1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #6 0x41c46d in _start (/tmp/gc+0x41c46d)


Indirect leak of 32 byte(s) in 1 object(s) allocated from:
    #0 0x4c0098 in realloc (/tmp/gc+0x4c0098)
    #1 0x7f2a7c302f5f in g_realloc ../glib/gmem.c:171:16
    #2 0x7f2a7c2e50da in g_hash_table_realloc_key_or_value_array ../glib/ghash.c:380:10
    #3 0x7f2a7c2e50da in g_hash_table_setup_storage ../glib/ghash.c:590:24
    #4 0x7f2a7c2e8a90 in g_hash_table_new_full ../glib/ghash.c:1084:3
    #5 0x4f4b07 in main /tmp/gc.c:3:22
    #6 0x7f2a7bf6d1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #7 0x41c46d in _start (/tmp/gc+0x41c46d)


SUMMARY: AddressSanitizer: 292 byte(s) leaked in 5 allocation(s).

TSAN: Состояние гонки

ThreadSanitizer сообщает о состоянии гонки, когда несколько потоков обращаются к данным без защиты от состояния гонки (см. документацию по ThreadSanitizer). Ниже приведен пример:

$ cat >tiny.cpp <<'EOF'
#include 


static volatile bool flip1{false};
static volatile bool flip2{false};


int main() {
  std::thread t([&]() {
    while (!flip1);
    flip2 = true;
  });
  flip1 = true;
  while (!flip2);
  t.join();
}
EOF
$ clang++ -o tiny tiny.cpp -Wall -g -pthread -fsanitize=thread
$ ./tiny
==================
WARNING: ThreadSanitizer: data race (pid=4057433)
  Write of size 1 at 0x000000fb4b09 by thread T1:
    #0 main::$_0::operator()() const /tmp/tiny.cpp:9:11 (tiny+0x4cfc98)
    #1 void std::__invoke_impl(std::__invoke_other, main::$_0&&) /usr/lib/gcc/x86_64-redhat-linux/10/../../../../include/c++/10/bits/invoke.h:60:14 (tiny+0x4cfc30)
    #2 std::__invoke_result::type std::__invoke(main::$_0&&) /usr/lib/gcc/x86_64-redhat-linux/10/../../../../include/c++/10/bits/invoke.h:95:14 (tiny+0x4cfb40)
    #3 void std::thread::_Invoker >::_M_invoke<0ul>(std::_Index_tuple<0ul>) /usr/lib/gcc/x86_64-redhat-linux/10/../../../../include/c++/10/thread:264:13 (tiny+0x4cfae8)
    #4 std::thread::_Invoker >::operator()() /usr/lib/gcc/x86_64-redhat-linux/10/../../../../include/c++/10/thread:271:11 (tiny+0x4cfa88)
    #5 std::thread::_State_impl > >::_M_run() /usr/lib/gcc/x86_64-redhat-linux/10/../../../../include/c++/10/thread:215:13 (tiny+0x4cf97f)
    #6 execute_native_thread_routine ../../../../../libstdc++-v3/src/c++11/thread.cc:80:18 (libstdc++.so.6+0xd65f3)


  Previous read of size 1 at 0x000000fb4b09 by main thread:
    #0 main /tmp/tiny.cpp:12:11 (tiny+0x4cf51f)


  Location is global 'flip2' of size 1 at 0x000000fb4b09 (tiny+0x000000fb4b09)


  Thread T1 (tid=4057435, running) created by main thread at:
    #0 pthread_create  (tiny+0x488b7d)
    #1  /usr/src/debug/gcc-10.2.1-9.fc33.x86_64/obj-x86_64-redhat-linux/x86_64-redhat-linux/libstdc++-v3/include/x86_64-redhat-linux/bits/gthr-default.h:663:35 (libstdc++.so.6+0xd6898)
    #2 std::thread::_M_start_thread(std::unique_ptr >, void (*)()) ../../../../../libstdc++-v3/src/c++11/thread.cc:135:37 (libstdc++.so.6+0xd6898)
    #3 main /tmp/tiny.cpp:7:15 (tiny+0x4cf4f4)


SUMMARY: ThreadSanitizer: data race /tmp/tiny.cpp:9:11 in main::$_0::operator()() const
==================
ThreadSanitizer: reported 1 warnings
$ clang++ -o tiny tiny.cpp -Wall -g -pthread
$ valgrind --tool=helgrind ./tiny
...
==4057510== ----------------------------------------------------------------
==4057510==
==4057510== Possible data race during write of size 1 at 0x40406D by thread #1
==4057510== Locks held: none
==4057510==    at 0x4011DC: main (tiny.cpp:11)
==4057510==
==4057510== This conflicts with a previous read of size 1 by thread #2
==4057510== Locks held: none
==4057510==    at 0x4015F8: main::$_0::operator()() const (tiny.cpp:8)
==4057510==    by 0x4015DC: void std::__invoke_impl(std::__invoke_other, main::$_0&&) (invoke.h:60)
==4057510==    by 0x40156C: std::__invoke_result::type std::__invoke(main::$_0&&) (invoke.h:95)
==4057510==    by 0x401544: void std::thread::_Invoker >::_M_invoke<0ul>(std::_Index_tuple<0ul>) (thread:264)
==4057510==    by 0x401514: std::thread::_Invoker >::operator()() (thread:271)
==4057510==    by 0x40148D: std::thread::_State_impl > >::_M_run() (thread:215)
==4057510==    by 0x49575F3: execute_native_thread_routine (thread.cc:80)
==4057510==    by 0x4840737: mythread_wrapper (hg_intercepts.c:387)
==4057510==  Address 0x40406d is 0 bytes inside data symbol "_ZL5flip1"
==4057510==
==4057510== ----------------------------------------------------------------
==4057510==
==4057510== Possible data race during read of size 1 at 0x40406D by thread #2
==4057510== Locks held: none
==4057510==    at 0x4015F8: main::$_0::operator()() const (tiny.cpp:8)
==4057510==    by 0x4015DC: void std::__invoke_impl(std::__invoke_other, main::$_0&&) (invoke.h:60)
==4057510==    by 0x40156C: std::__invoke_result::type std::__invoke(main::$_0&&) (invoke.h:95)
==4057510==    by 0x401544: void std::thread::_Invoker >::_M_invoke<0ul>(std::_Index_tuple<0ul>) (thread:264)
==4057510==    by 0x401514: std::thread::_Invoker >::operator()() (thread:271)
==4057510==    by 0x40148D: std::thread::_State_impl > >::_M_run() (thread:215)
==4057510==    by 0x49575F3: execute_native_thread_routine (thread.cc:80)
==4057510==    by 0x4840737: mythread_wrapper (hg_intercepts.c:387)
==4057510==    by 0x4BD33F8: start_thread (pthread_create.c:463)
==4057510==    by 0x4CED902: clone (clone.S:95)
==4057510==
==4057510== This conflicts with a previous write of size 1 by thread #1
==4057510== Locks held: none
==4057510==    at 0x4011DC: main (tiny.cpp:11)
==4057510==  Address 0x40406d is 0 bytes inside data symbol "_ZL5flip1"
==4057510==
==4057510== ----------------------------------------------------------------
==4057510==
==4057510== Possible data race during write of size 1 at 0x40406E by thread #2
==4057510== Locks held: none
==4057510==    at 0x401613: main::$_0::operator()() const (tiny.cpp:9)
==4057510==    by 0x4015DC: void std::__invoke_impl(std::__invoke_other, main::$_0&&) (invoke.h:60)
==4057510==    by 0x40156C: std::__invoke_result::type std::__invoke(main::$_0&&) (invoke.h:95)
==4057510==    by 0x401544: void std::thread::_Invoker >::_M_invoke<0ul>(std::_Index_tuple<0ul>) (thread:264)
==4057510==    by 0x401514: std::thread::_Invoker >::operator()() (thread:271)
==4057510==    by 0x40148D: std::thread::_State_impl > >::_M_run() (thread:215)
==4057510==    by 0x49575F3: execute_native_thread_routine (thread.cc:80)
==4057510==    by 0x4840737: mythread_wrapper (hg_intercepts.c:387)
==4057510==    by 0x4BD33F8: start_thread (pthread_create.c:463)
==4057510==    by 0x4CED902: clone (clone.S:95)
==4057510==
==4057510== This conflicts with a previous read of size 1 by thread #1
==4057510== Locks held: none
==4057510==    at 0x4011E4: main (tiny.cpp:12)
==4057510==  Address 0x40406e is 0 bytes inside data symbol "_ZL5flip2"
==4057510==
==4057510== ----------------------------------------------------------------
==4057510==
==4057510== Possible data race during read of size 1 at 0x40406E by thread #1
==4057510== Locks held: none
==4057510==    at 0x4011E4: main (tiny.cpp:12)
==4057510==
==4057510== This conflicts with a previous write of size 1 by thread #2
==4057510== Locks held: none
==4057510==    at 0x401613: main::$_0::operator()() const (tiny.cpp:9)
==4057510==    by 0x4015DC: void std::__invoke_impl(std::__invoke_other, main::$_0&&) (invoke.h:60)
==4057510==    by 0x40156C: std::__invoke_result::type std::__invoke(main::$_0&&) (invoke.h:95)
==4057510==    by 0x401544: void std::thread::_Invoker >::_M_invoke<0ul>(std::_Index_tuple<0ul>) (thread:264)
==4057510==    by 0x401514: std::thread::_Invoker >::operator()() (thread:271)
==4057510==    by 0x40148D: std::thread::_State_impl > >::_M_run() (thread:215)
==4057510==    by 0x49575F3: execute_native_thread_routine (thread.cc:80)
==4057510==    by 0x4840737: mythread_wrapper (hg_intercepts.c:387)
==4057510==  Address 0x40406e is 0 bytes inside data symbol "_ZL5flip2"
...

Примечание: Вы можете посмотреть этот код и его вывод в Compiler Explorer

Перекомпиляция библиотек

AddressSanitizer автоматически обрабатывает все обращения к glibc. Это не относится к другим системным или пользовательским библиотекам. Чтобы AddressSanitizer работал наилучшим образом, необходимо также перекомпилировать такие библиотеки с параметром -fsanitize=address. Для Valgrind этого делать не требуется.

Следующая ошибка в библиотеке libuser.c все-равно отлавливается AddressSanitizer благодаря перехватчику glibc, даже если библиотека не скомпилирована с AddressSanitizer:

$ cat >library.c <<'EOF'
#include 
void library(char *s) {
  strcpy(s,"string");
}
EOF
$ cat >libuser.c <<'EOF'
#include 
void library(char *s);
int main(void) {
  char *s = malloc(1);
  library(s);
  free(s);
}
EOF
$ clang -o library.so library.c -Wall -g -shared -fPIC
$ clang -o libuser libuser.c -Wall -g ./library.so -fsanitize=address
$ ./libuser
=================================================================
==128657==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000011 at pc 0x000000484a6d bp 0x7fff13a4ace0 sp 0x7fff13a4a490
WRITE of size 7 at 0x602000000011 thread T0
    #0 0x484a6c in __interceptor_strcpy.part.0 (/tmp/libuser+0x484a6c)
    #1 0x7fae9f53512b in library /tmp/library.c:3:3
    #2 0x4f4abe in main /tmp/libuser.c:5:3
    #3 0x7fae9f1be1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #4 0x41c42d in _start (/tmp/libuser+0x41c42d)


0x602000000011 is located 0 bytes to the right of 1-byte region [0x602000000010,0x602000000011)
allocated by thread T0 here:
    #0 0x4bfcef in malloc (/tmp/libuser+0x4bfcef)
    #1 0x4f4ab1 in main /tmp/libuser.c:4:13
    #2 0x7fae9f1be1e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16


SUMMARY: AddressSanitizer: heap-buffer-overflow (/tmp/libuser+0x484a6c) in __interceptor_strcpy.part.0
...

В следующем случае AddressSanitizer пропускает повреждение памяти, если библиотека не была перекомпилирована с помощью AddressSanitizer:

$ cat >library.c <<'EOF'
void library(char *s) {
  const char *cs = "string";
  while (*cs)
    *s++ = *cs++;
  *s = 0;
}
EOF
$ cat >libuser.c <<'EOF'
#include 
void library(char *s);
int main(void) {
  char *s = malloc(1);
  library(s);
  free(s);
}
EOF
$ clang -o library.so library.c -Wall -g -shared -fPIC
$ clang -o libuser libuser.c -Wall -g ./library.so -fsanitize=address
$ ./libuser
(nothing found by AddressSanitizer)

Valgrind может найти ошибку без перекомпиляции:

$ clang -o library.so library.c -Wall -g -shared -fPIC; clang -o libuser libuser.c -Wall -g ./library.so; valgrind ./libuser
...
==128708== Invalid write of size 1
==128708==    at 0x4849146: library (library.c:4)
==128708==    by 0x40116E: main (libuser.c:5)
==128708==  Address 0x4a57041 is 0 bytes after a block of size 1 alloc'd
==128708==    at 0x4839809: malloc (vg_replace_malloc.c:307)
==128708==    by 0x401161: main (libuser.c:4)
...

AddressSanitizer также может найти ошибку, если мы перекомпилируем библиотеку с помощью AddressSanitizer:

$ clang -o library.so library.c -Wall -g -shared -fPIC -fsanitize=address
$ clang -o libuser libuser.c -Wall -g ./library.so -fsanitize=address
$ ./libuser
=================================================================
==128719==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000011 at pc 0x7f7e4e68b269 bp 0x7ffc40c0dc30 sp 0x7ffc40c0dc28
WRITE of size 1 at 0x602000000011 thread T0
    #0 0x7f7e4e68b268 in library /tmp/library.c:4:10
    #1 0x4f4abe in main /tmp/libuser.c:5:3
    #2 0x7f7e4e3141e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16
    #3 0x41c42d in _start (/tmp/libuser+0x41c42d)


0x602000000011 is located 0 bytes to the right of 1-byte region [0x602000000010,0x602000000011)
allocated by thread T0 here:
    #0 0x4bfcef in malloc (/tmp/libuser+0x4bfcef)
    #1 0x4f4ab1 in main /tmp/libuser.c:4:13
    #2 0x7f7e4e3141e1 in __libc_start_main /usr/src/debug/glibc-2.32-20-g5c36293f06/csu/../csu/libc-start.c:314:16


SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/library.c:4:10 in library
...

Взаимодействие Sanitizers с _FORTIFY_SOURCE

По умолчанию rpmbuild использует опцию -Wp,-D_FORTIFY_SOURCE=2, которая реализует свои собственные проверки корректности доступа к памяти. К сожалению, она отключает некоторые проверки памяти, выполняемые AddressSanitizer. Эта проблема может быть исправлена в будущем. В настоящее время, чтобы подготовиться к проверке с помошью Sanitizers, просто отключите _FORTIFY_SOURCE с помощью -Wp,-U_FORTIFY_SOURCE (это более универсальная форма простой -D_FORTIFY_SOURCE=0):

$ cat >strcpyfrom.spec <<'EOF'
Summary: strcpyfrom
Name: strcpyfrom
Version: 1
Release: 1
License: GPLv3+
%description
%build
cat >strcpyfrom.c <<'EOH'
#include 
#include 
int main(void) {
  char *s = malloc(1);
  char d[0x1000];
  strcpy(d, s);
  return 0;
}
EOH
gcc -o strcpyfrom strcpyfrom.c $RPM_OPT_FLAGS -fsanitize=address
echo no error caught:
./strcpyfrom
gcc -o strcpyfrom strcpyfrom.c $RPM_OPT_FLAGS -fsanitize=address -Wp,-U_FORTIFY_SOURCE
echo error caught:
./strcpyfrom
EOF
$ rpmbuild -bb strcpyfrom.spec 
Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.KTLr7c
+ umask 022
+ cd src/rpm/BUILD
+ cat
+ gcc -o strcpyfrom strcpyfrom.c -O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -fsanitize=address
+ echo no error caught:
no error caught:
+ ./strcpyfrom
+ gcc -o strcpyfrom strcpyfrom.c -O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -fsanitize=address -Wp,-U_FORTIFY_SOURCE
annobin: strcpyfrom.c: Warning: -D_FORTIFY_SOURCE defined as 0
+ echo error caught:
error caught:
+ ./strcpyfrom
=================================================================
==412157==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000011 at pc 0x7fe75d8b2075 bp 0x7ffccf5dd1e0 sp 0x7ffccf5dc990
READ of size 2 at 0x602000000011 thread T0
    #0 0x7fe75d8b2074  (/lib64/libasan.so.6+0x52074)
    #1 0x4011be in main strcpyfrom.c:6
    #2 0x7fe75d6bd1e1 in __libc_start_main ../csu/libc-start.c:314
    #3 0x40127d in _start (strcpyfrom+0x40127d)


0x602000000011 is located 0 bytes to the right of 1-byte region [0x602000000010,0x602000000011)
allocated by thread T0 here:
    #0 0x7fe75d90b3cf in __interceptor_malloc (/lib64/libasan.so.6+0xab3cf)
    #1 0x4011b2 in main strcpyfrom.c:4
    #2 0x40200f  (strcpyfrom+0x40200f)


SUMMARY: AddressSanitizer: heap-buffer-overflow (/lib64/libasan.so.6+0x52074) 
...
error: Bad exit status from /var/tmp/rpm-tmp.KTLr7c (%build)


RPM build errors:
    Bad exit status from /var/tmp/rpm-tmp.KTLr7c (%build)

Заключение

Если вы привыкли к Valgrind, попробуйте AddressSanitizer — для начала просто добавьте параметр компоновки и компиляции -fsanitize=address (то есть, ко всем CFLAGS, CXXFLAGS и LDFLAGS). Если вам понравится, то вернитесь к разделу «Краткая инструкция к Sanitizers» за рекомендациями по более тонкой настройке.

В завершение приглашаем всех желающих на открытый урок «Реализация динамических структур данных на Си и Python», который пройдет сегодня в 20:00. В результате у нас получится описать схемы управления динамической памятью в C и Python, методы построения динамических структур данных на этих языках, особенности и возможности применения функций динамического управления памятью для конкретных задач. А также сможем написать шаблонное приложение для собственной реализации бинарного дерева. Записаться можно на странице курса «Программист С».

Habrahabr.ru прочитано 10541 раз