Address Sanitizer, или Что делать, если не работает valgrind

    Случилась непростая ситуация. Есть код, написанный на С, который активно используется через cgo в проекте, написанном на Go. В какой-то момент программа начала падать с ошибками от malloc: то segfault, то memory corruption.

    Логичная мысль: нужен valgrind с его memcheck, чтобы проверить, кто лезет поперёк батьки в пекло в невалидную память. Однако, попытка скормить валгринду бинарник, полученный от go build, приведёт только к разочарованию — даже на простом Hello World валгринд разразится сотнями ошибок и отправит разработчика на известные координаты (спойлер: "Go fix your program!").

    Это происходит из-за того, что go runtime довольно специфичен и значительно отличается от такового в С. (Подробности можно спокойно найти по запросу «golang valgrind»).

    Так как же нам разобраться, что происходит?

    Пристальный гуглёж показал мне, что у gcc (и у clang, кстати) есть очень удобный инструмент — Address Sanitizer. Удобен он тем, что умеет делать штуки, подвластные valgrind (как минимум, умеет ловить использование освобождённой памяти, переполнения и утечки), и при этом автоматически встраивается в бинарник без необходимости использовать внешние утилиты. Но главное — его можно спокойно использовать в CGo для отладки С-кода без помех для Go runtime (собственно, сами разработчики Go рекомендуют использовать этот инструмент).

    Как его использовать?

    1. Проверить, что gcc у нас версии не ниже 4.8.
    2. Скомпилировать нашу программу с флагом -fsanitize=address (в документации к gcc описано ещё много санитайзеров, посмотреть можно тут). Этот флаг надо добавить как при сборке, так и при линковке (и в CFLAGS, и в LDFLAGS).
    3. Запустить программу и радоваться разноцветному выводу санитайзера.

    Для проверки я написал простейшую программу, которая выделяет места на 10 интов, но заполняет 11 (test.c):

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char *argv[]) {
            int *m = (int *) malloc(10 * sizeof (int));
    
            for (int i = 0; i < 11; i++) {
                    m[i] = i;
            }
    
            return 0;
    }
    

    Компилируем это с -fsanitize=address, после запуска видим вот это:

    webconn@webconn-laptop:~/Projects/Testing/C/Sanitizer$ gcc -o ./main ./test.c -fsanitize=address
    webconn@webconn-laptop:~/Projects/Testing/C/Sanitizer$ ./main 
    =================================================================
    ==18098==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60400000dff8 at pc 0x557945c74922 bp 0x7fff94c67d40 sp 0x7fff94c67d38
    WRITE of size 4 at 0x60400000dff8 thread T0
        #0 0x557945c74921 in main (/home/webconn/Projects/Testing/C/Sanitizer/main+0x921)
        #1 0x7fea6440b2b0 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x202b0)
        #2 0x557945c747a9 in _start (/home/webconn/Projects/Testing/C/Sanitizer/main+0x7a9)
    
    0x60400000dff8 is located 0 bytes to the right of 40-byte region [0x60400000dfd0,0x60400000dff8)
    allocated by thread T0 here:
        #0 0x7fea6484ad28 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.3+0xc1d28)
        #1 0x557945c748c8 in main (/home/webconn/Projects/Testing/C/Sanitizer/main+0x8c8)
        #2 0x7fea6440b2b0 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x202b0)
    
    SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/webconn/Projects/Testing/C/Sanitizer/main+0x921) in main
    Shadow bytes around the buggy address:
      0x0c087fff9ba0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9bb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9bc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9bd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9be0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    =>0x0c087fff9bf0: fa fa fa fa fa fa fa fa fa fa 00 00 00 00 00[fa]
      0x0c087fff9c00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9c10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9c20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9c30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9c40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    Shadow byte legend (one shadow byte represents 8 application bytes):
      Addressable:           00
      Partially addressable: 01 02 03 04 05 06 07 
      Heap left redzone:       fa
      Heap right redzone:      fb
      Freed heap region:       fd
      Stack left redzone:      f1
      Stack mid redzone:       f2
      Stack right redzone:     f3
      Stack partial redzone:   f4
      Stack after return:      f5
      Stack use after scope:   f8
      Global redzone:          f9
      Global init order:       f6
      Poisoned by user:        f7
      Container overflow:      fc
      Array cookie:            ac
      Intra object redzone:    bb
      ASan internal:           fe
      Left alloca redzone:     ca
      Right alloca redzone:    cb
    ==18098==ABORTING
    

    На деле, это всё ещё довольно удобно подсвечивается цветами:

    Вывод в цвете (PNG, 78.5 KB)
    image

    Для того, чтобы это заработало с cgo, я добавил соответствующие спецкомментарии при подключении кода на C (на месте многоточий могли бы быть Ваши полезные флаги и директивы):

    /*
    #cgo linux LDFLAGS: ... -fsanitize=address
    #cgo linux CFLAGS: ... -fsanitize=address
    ...
    */
    import "C"
    

    Что и дало желаемый результат — теперь модуль, написанный на С, выдаёт отладочную информацию при проблемах с памятью, но не мешает спокойно жить Go runtime.

    Собственно, сами разработчики Go рекомендуют использовать Sanitizer в подобных ситуациях.

    Надеюсь, информация окажется кому-то полезной.

    UPD: Конечно же, если добавить флаг -g, вывод будет ещё чуть более читаемым.
    Вывод после компиляции с флагом -g
    webconn@webconn-laptop:~/Projects/Testing/C/Sanitizer$ ./main 
    =================================================================
    ==12266==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60400000dff8 at pc 0x55d4f8d03922 bp 0x7ffc10a57370 sp 0x7ffc10a57368
    WRITE of size 4 at 0x60400000dff8 thread T0
        #0 0x55d4f8d03921 in main /home/webconn/Projects/Testing/C/Sanitizer/main.c:14
        #1 0x7fa688d282b0 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x202b0)
        #2 0x55d4f8d037a9 in _start (/home/webconn/Projects/Testing/C/Sanitizer/main+0x7a9)
    
    0x60400000dff8 is located 0 bytes to the right of 40-byte region [0x60400000dfd0,0x60400000dff8)
    allocated by thread T0 here:
        #0 0x7fa689167d28 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.3+0xc1d28)
        #1 0x55d4f8d038c8 in main /home/webconn/Projects/Testing/C/Sanitizer/main.c:11
        #2 0x7fa688d282b0 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x202b0)
    
    SUMMARY: AddressSanitizer: heap-buffer-overflow /home/webconn/Projects/Testing/C/Sanitizer/main.c:14 in main
    Shadow bytes around the buggy address:
      0x0c087fff9ba0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9bb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9bc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9bd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9be0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    =>0x0c087fff9bf0: fa fa fa fa fa fa fa fa fa fa 00 00 00 00 00[fa]
      0x0c087fff9c00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9c10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9c20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9c30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
      0x0c087fff9c40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    Shadow byte legend (one shadow byte represents 8 application bytes):
      Addressable:           00
      Partially addressable: 01 02 03 04 05 06 07 
      Heap left redzone:       fa
      Heap right redzone:      fb
      Freed heap region:       fd
      Stack left redzone:      f1
      Stack mid redzone:       f2
      Stack right redzone:     f3
      Stack partial redzone:   f4
      Stack after return:      f5
      Stack use after scope:   f8
      Global redzone:          f9
      Global init order:       f6
      Poisoned by user:        f7
      Container overflow:      fc
      Array cookie:            ac
      Intra object redzone:    bb
      ASan internal:           fe
      Left alloca redzone:     ca
      Right alloca redzone:    cb
    ==12266==ABORTING
    

    Комментарии 7

      +1

      Выглядит прикольно, вот только без опции -g при компиляции не так-то просто будет по выхлопу понять, какое именно место в коде привело к ошибке.


      Но всё равно спасибо за статью! Мотивирует подумать, где и как применить санитайзер у себя на работе

        +1
        Спасибо за важное замечание! Добавил в статью пример вывода с флагом -g.

        На самом деле, я успел про него забыть ненадолго, ибо в проекте много маленьких функций на C, и мне достаточно было только взглянуть на стек.
        0
        А нельзя msan включить для всего проекта целиком, например какими-нибудь переменными окружения?
          0
          С переменными CFLAGS и LDFLAGS не работает? (Если проект собирается через make, где эти переменные объявлены и вставлены в нужные правила)
            0
            Гошные проекты ведь через go build собираются обычно
              0
              Опять контекст не переключился, извиняюсь.

              В cgo вроде как есть переменные окружения CGO_CFLAGS, CGO_LDFLAGS и всё в этом духе (https://golang.org/cmd/cgo/). Тогда сборка должна выглядеть примерно так:

              CGO_CFLAGS=-fsanitize=address CGO_LDFLAGS=-fsanitize=address go build

              Хотя у меня это сходу не заработало, проверю, когда доберусь до машины с go.
                0
                Привет из 2018! Добрались до машины?

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое