Пару недель назад (а если быть точнее, то 2 июля 2021 года) исполнилось двадцать лет легендарному протоколу BitTorrent. Созданный Брэмом Коуэном (Bram Cohen) протокол с момента своего появления стремительно развивался и быстро стал одним из самых популярных способов для обмена файлами. Почему бы в честь этого не проверить парочку долгоживущих тематических проектов с помощью анализатора PVS-Studio для Linux?


0846_BitTorrent_ru/image1.png


Введение


Сегодня на рассмотрении у нас будет два проекта: libtorrent (он же "Rasterbar libtorrent" или "rb-libtorrent") и Transmission.


Libtorrent – свободная кроссплатформенная библиотека для работы с протоколом BitTorrent, написанная на языке С++. На официальном сайте в списке достоинств упоминается эффективное использование ресурсов центрального процессора и памяти, а также простота использования. Судя по английской wiki, на этой библиотеке базируется около половины доступных сейчас BitTorrent клиентов.


Transmission – кроссплатформенный BitTorrent клиент с открытым исходным кодом. Так же как и у libtorrent, основными преимуществами Transmission являются легкость в эксплуатации и эффективное использование ресурсов. Кроме того, программа может похвастаться полным отсутствием рекламы, аналитики, платных версий, наличием нативного GUI (графического пользовательского интерфейса) для различным платформ, а также headless версий (без GUI) для установки на серверах, роутерах и т. п.


Как проверялось


Для анализа использовалась Linux версия статического анализатора PVS-Studio, запущенная в контейнере с Ubuntu 20.04 через WSL2. Для установки в консоли необходимо выполнить следующие команды (для остальных систем также есть инструкции в документации):


wget -q -O - https://files.viva64.com/etc/pubkey.txt | \
  sudo apt-key add -

sudo wget -O /etc/apt/sources.list.d/viva64.list \
  https://files.viva64.com/etc/viva64.list

sudo apt-get update
sudo apt-get install pvs-studio

Далее перед проверкой необходимо ввести данные своей лицензии. Делается это с помощью следующей команды:


pvs-studio-analyzer credentials NAME KEY

(где NAME и KEY – имя и ключ лицензии соответственно).


Таким образом лицензия будет сохранена в каталоге ~/.config/PVS-Studio/, и её не придется указывать при каждом запуске анализатора.


К слову, о лицензиях… Мы активно поддерживаем разработчиков проектов с открытым исходным кодом и поэтому не только сообщаем о найденных багах в репозитории, но и даём им возможность использовать PVS-Studio бесплатно. Всех остальных приглашаем скачать и попробовать анализатор PVS-Studio в деле, воспользовавшись временной лицензией :)


Для запуска анализа воспользуемся самым простым способом – попросим сборочную систему сгенерировать файл compile_commands.json (в котором перечисляются все параметры и команды, необходимые для сборки проекта). А уже его передадим для анализа в PVS-Studio. С этой целью во время сборки добавим аргумент -DCMAKE_EXPORT_COMPILE_COMMANDS=On к вызову cmake. Например:


cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On ..

Ну и наконец запустим анализ. Для этого в папке, содержащей сгенерированный файл compile_commands.json, выполним следующую команду:


pvs-studio-analyzer analyze -o transmission.log -j 8

где с помощью ключа -o указывается файл для сохранения результатов работы анализатора, а флаг -j позволяет распараллелить анализ требуемого количества потоков.


Если описанный выше способ взаимодействия с PVS-Studio вам не подходит, то у нас в документации есть примеры по взаимодействию с различными сборочными системами и компиляторами.


Ещё один примечательный момент – использование формата SARIF для просмотра отчёта анализатора. Это особенно актуально для разработчиков, предпочитающих редактор Visual Studio Code, ведь для него существует расширение Sarif Viewer, которое позволяет просматривать отчёт и прямо из него переходить по затронутым в коде местам. На скриншоте ниже вы как раз можете увидеть, как визуально выглядела проверка проекта Transmission.


0846_BitTorrent_ru/image3.png


Для создания отчета в формате SARIF при работе с PVS-Studio в Linux необходимо после работы анализатора выполнить следующую команду:


plog-converter -t sarif -o ./transmission.sarif ./transmission.log -d V1042

где с помощью -t sarif мы как раз указываем, что результат нужно сохранить в формате SARIF. Флаг -o указывает название файла-отчёта. А флагом -d мы подавляем нерелевантные в данном случае диагностики.


Более подробно прочитать про открытый стандарт обмена результатами статического анализа (SARIF) можно на сайте OASIS Open. А с примером взаимодействия с GitHub можно ознакомиться в нашей статье "Как в GitHub смотреть красивые отчеты об ошибках с помощью SARIF".


Результаты проверки


Хотелось бы похвалить разработчиков – код достаточно чистый и заслуживающих упоминания предупреждений нашлось совсем немного. Конечно же, хотелось для своей первой статьи найти какие-нибудь интересные ошибки, покопаться в деталях, но… увы. Проекты небольшие по объему, и видно, что ими занимаются опытные разработчики. Кроме того, в changelog'ах были найдены упоминания использования сторонних статических анализаторов (Coverity, Cppcheck). Но даже несмотря на всё это PVS-Studio удалось найти пару любопытных ошибок.


Transmission


Начнём, пожалуй, с проекта Transmission, как наиболее популярного и часто используемого. Только хочу заранее предупредить, что для удобства чтения код будет сокращаться и минимально рефакториться.


Фрагмент N1: использование memset для очистки памяти


static void freeMetaUI(gpointer p)
{
  MakeMetaUI* ui = p;

  tr_metaInfoBuilderFree(ui->builder);
  g_free(ui->target);
  memset(ui, ~0, sizeof(MakeMetaUI));
  g_free(ui);
}

Предупреждение V597 The compiler could delete the 'memset' function call, which is used to flush 'ui' object. The memset_s() function should be used to erase the private data. makemeta-ui.c:53


Уже немало раз было сказано да и немало статей написано про грабли, на которые разработчики периодически наступают при использовании функции memset для очистки памяти. Если вкратце, то компилятор имеет полное право удалить вызовы memset, если посчитает их бессмысленными (такое часто происходит, когда буфер очищается в конце операции и более не используется). Удостовериться в том, что компиляторы действительно могут убрать ненужный вызов, можно с помощью проверки аналогичного кода сервисом Compiler Explorer.


0846_BitTorrent_ru/image5.png


Как видно из рисунка выше, компилятор Clang 12.0.1 уже при использовании флага компиляции -O2 действительно будет вырезать вызов memset. Многие скажут: "Ну, удалил и удалил, чего бубнить-то", — а проблема в том, что незатёртыми могут оказаться приватные данные пользователя. Согласен, не думаю, что проблема приватности данных будет актуальна для торрента клиента. Но если разработчик написал так здесь, то что помешает ему сделать так же в более ответственном месте? Чтобы избежать этого, нужно использовать специально предназначенные функции (например, memset_s или RtlSecureZeroMemory). Более подробно про эту проблему уже писали мои коллеги раз, два и три.


Фрагмент N2: ошибки в библиотеках – тоже ошибки


void jsonsl_jpr_match_state_init(jsonsl_t jsn,
                                 jsonsl_jpr_t *jprs,
                                 size_t njprs)
{
  size_t ii, *firstjmp;
  ...
  jsn->jprs = (jsonsl_jpr_t *)malloc(sizeof(jsonsl_jpr_t) * njprs);
  jsn->jpr_count = njprs;
  jsn->jpr_root = (size_t*)calloc(1, sizeof(size_t) * njprs * jsn->levels_max);
  memcpy(jsn->jprs, jprs, sizeof(jsonsl_jpr_t) * njprs);

  /* Set the initial jump table values */
  firstjmp = jsn->jpr_root;
  for (ii = 0; ii < njprs; ii++) {
    firstjmp[ii] = ii+1;
  }
}

Предупреждение V575 The potential null pointer is passed into 'memcpy' function. Inspect the first argument. Check lines: 1142, 1139. jsonsl.c:1142


Предупреждение V522 There might be dereferencing of a potential null pointer 'firstjmp'. Check lines: 1147, 1141. jsonsl.c:1147


В этом фрагменте скрылись сразу две проблемы, и обе связаны с отсутствием проверки указателя, полученного из функции malloc/calloc. Да, возможно, эта ошибка вообще никогда не проявит себя, но именно этот код желательно поправить. Почему? Все просто – используя сторонние библиотеки, разработчик безоговорочно доверяет им часть работы и вычислений. И, я думаю, мало кому будет приятно, если программа вдруг повредит важные данные, тем более по вине сторонней библиотеки. Более подробно эта проблема и пути её решения были описаны в одной из наших прошлых статей "Почему важно проверять, что вернула функция malloc" и комментариях к ней.


Анализатор также выявил похожие подозрительные фрагменты кода:


  • V522 There might be dereferencing of a potential null pointer 'jsn'. Check lines: 117, 113. jsonsl.c:117
  • V522 There might be dereferencing of a potential null pointer 'i'. DetailsDialog.cc:133
  • V522 There might be dereferencing of a potential null pointer. TorrentFilter.cc:320

libtorrent


На этом, пожалуй, закончим с Transmission и посмотрим, что же интересного нашлось в проекте libtorrent.


Фрагмент N1: недостаточная проверка индексов массивов


template <typename Handler>
void handshake2(error_code const& e, Handler h)
{
  ...
  std::size_t const read_pos = m_buffer.size();
  ...
  if (m_buffer[read_pos - 1] == '\n' && read_pos > 2) // <=
  {
    if (m_buffer[read_pos - 2] == '\n')
    {
      found_end = true;
    }
    else if (read_pos > 4
      && m_buffer[read_pos - 2] == '\r'
      && m_buffer[read_pos - 3] == '\n'
      && m_buffer[read_pos - 4] == '\r')
    {
      found_end = true;
    }
  }
  ...
}

Предупреждение V781 The value of the 'read_pos' index is checked after it was used. Perhaps there is a mistake in program logic. http_stream.hpp:166.


Классическая ошибка, в которой разработчик сначала пытается получить элемент массива m_buffer по индексу read_pos — 1, а уже потом read_pos проверяется на корректность (read_pos > 2). Сложно сказать, что в данном случае произойдёт на практике: возможно, будет прочитана другая переменная, а может, вообще возникнет Access Violation. Всё-таки неопределенное поведение (Undefined Behavior) не просто так назвали неопределенным :) Правильным решением здесь будет поменять эти действия местами:


if (read_pos > 2 && m_buffer[read_pos - 1] == '\n')

Фрагмент N2, N3: перезаписывание значений


void dht_tracker::dht_status(session_status& s)
{
  s.dht_torrents += int(m_storage.num_torrents());    // <=

  s.dht_nodes = 0;
  s.dht_node_cache = 0;
  s.dht_global_nodes = 0;
  s.dht_torrents = 0;                                 // <=
  s.active_requests.clear();
  s.dht_total_allocations = 0;

  for (auto& n : m_nodes)
    n.second.dht.status(s);
}

Предупреждение V519 The 's.dht_torrents' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 205, 210. dht_tracker.cpp 210.


В вышеприведённом фрагменте два раза изменяется переменная s.dht_torrents: в первый раз ей назначается значение, а через несколько строк оно обнуляется без использования между присвоениями. Т. е. мы имеем дело с так называемой тупиковой записью (Dead Store). Сложно сказать, как на самом деле должен выглядеть код, т. к. тип session_status содержит большое количество полей. Возможно, одно из присвоений здесь лишнее или случайно обнуляется не та переменная.


Похожая проблема есть и в следующем участке кода, но там она усугубляется тем, что перезапись переменных сложнее заметить из-за большого количества кода и комментариев между ними. А между тем, перезаписываемых переменных здесь сразу три, причём одна из них получает то же самое значение, что и до условия. Отловить такие проблемы вручную обычно очень сложно, а вот статический анализ справляется с этим на раз:


void torrent::bytes_done(torrent_status& st, status_flags_t const flags) const
{
  ...
  st.total_done = 0;
  st.total_wanted_done = 0;
  st.total_wanted = m_size_on_disk;
  ...
  if (m_seed_mode || is_seed())
  {
    st.total_done = m_torrent_file->total_size() - m_padding_bytes;
    st.total_wanted_done = m_size_on_disk;
    st.total_wanted = m_size_on_disk;
    ...
    return;
  }
  else if (!has_picker())
  {
    st.total_done = 0;
    st.total_wanted_done = 0;
    st.total_wanted = m_size_on_disk;
    return;
  }
  ...
}

Предупреждения PVS-Studio:


  • V1048 The 'st.total_wanted' variable was assigned the same value. torrent.cpp 3784
  • V1048 The 'st.total_done' variable was assigned the same value. torrent.cpp 3792
  • V1048 The 'st.total_wanted_done' variable was assigned the same value. torrent.cpp 3793
  • V1048 The 'st.total_wanted' variable was assigned the same value. torrent.cpp 3794

Фрагмент N4: неудачное явное приведение типа


void torrent::get_download_queue(std::vector<partial_piece_info>* queue) const
{
  ...
  const int blocks_per_piece = m_picker->blocks_in_piece(piece_index_t(0));
  ...
  int counter = 0;
  for (auto i = q.begin(); i != q.end(); ++i, ++counter)
  {
    partial_piece_info pi;
    ...
    pi.blocks = &blk[std::size_t(counter * blocks_per_piece)];
  }
}

Предупреждение V1028 Possible overflow. Consider casting operands of the 'counter * blocks_per_piece' operator to the 'size_t' type, not the result. torrent.cpp 7092


В данном случае явное приведение типа к size_t используется для корректного доступа к элементам массива. Проблема здесь в том, что оба операнда являются знаковыми целыми числами и при их перемножении может возникнуть переполнение. Очень часто такой код можно встретить, когда разработчики пытаются по-быстрому просто заглушить предупреждение компилятора, но при этом не задумываются, что только плодят ошибки. В данном случае, чтобы исправить проблему, достаточно привести хотя бы один операнд к типу size_t. Примерно так:


pi.blocks = &blk[std::size_t(counter) * blocks_per_piece];

Похожие проблемы также были найдены в следующих фрагментах:


  • V1028 Possible overflow. Consider casting operands of the 'new_size_words + 1' operator to the 'size_t' type, not the result. bitfield.cpp 179
  • V1028 Possible overflow. Consider casting operands of the 'm_capacity + amount_to_grow' operator to the 'size_t' type, not the result. heterogeneous_queue.hpp 207

Фрагмент N5: лишние условия


В проекте libtorrent так же, как и в Transmission, нашлось немало предупреждений, связанных с лишними условиями. Ложными их назвать нельзя, но перечислять их нет смысла, потому что интересного в этом мало. Чтобы было понятно, что именно я имею в виду, приведу такой фрагмент кода:


char const* operation_name(operation_t const op)
  {
    ...
    static char const* const names[] = {
      ...
    };

    int const idx = static_cast<int>(op);
    if (idx < 0 || idx >= int(sizeof(names) / sizeof(names[0])))
      return "unknown operation";
    return names[idx];
}

Предупреждение V560 A part of conditional expression is always false: idx < 0. alert.cpp 1885.


В нём анализатор справедливо предупреждает, что условие idx < 0 не имеет смысла, т. к. переменная index получает значение из перечисления, в котором лишь беззнаковые целые числа:


enum class operation_t : std::uint8_t

Стоит ли обращать внимание на подобные предупреждения? Я думаю, что на этот случай у каждого разработчика найдётся своё мнение. Кто-то скажет, что исправлять их нет смысла, потому что на реальные ошибки они не указывают, а кто-то, напротив, скажет, что не нужно засорять код. Я же считаю, что подобного рода диагностики – отличная возможность найти хорошие места для будущего рефакторинга.


Заключение


Как видите, интересных ошибок нашлось не так уж и много, что может еще раз сказать о высоком качестве и чистоте кода проверяемых проектов. Думаю, не последнюю роль здесь играет то, что проекты существуют уже довольно давно, активно развиваются open source сообществом и, судя по истории коммитов, ранее проверялись статическими анализаторами.


Ну и в конце хотелось бы еще раз напомнить, что команда PVS-Studio любит и активно поддерживает проекты с открытым исходным кодом. Именно поэтому мы не только сообщаем о найденных багах разработчикам, но и даём им возможность использовать PVS-Studio бесплатно. Также будет не лишним напомнить про бесплатные лицензии для студентов и преподавателей. В случае коммерческих проектов предлагаем скачать и попробовать анализатор PVS-Studio, запросив ознакомительную лицензию на нашем веб-сайте :)


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Mikhail Gelvikh. Checking BitTorrent in honor of the 20th anniversary. Time == quality.

Комментарии (4)


  1. 13werwolf13
    30.07.2021 13:42

    хм, пойти чтоли eiskaltdcpp проверить, тематика та-же..


  1. PkXwmpgN
    30.07.2021 18:27

    Если вкратце, то компилятор имеет полное право удалить вызовы memset, если посчитает их бессмысленными (такое часто происходит, когда буфер очищается в конце операции и более не используется). Удостовериться в том, что компиляторы действительно могут убрать ненужный вызов, можно с помощью проверки аналогичного кода сервисом Compiler Explorer.

    Приведенный фрагмент кода не аналогичен. Transmission использует Glib и g_free соответственно. Ни один компилятор не посчитает бессмысленым вызов memset и не уберет его перед вызовом g_free.

    code
    #include <stdlib.h>
    #include <string.h>
    #include <glib.h>
    
    typedef void* gpointer;
    typedef struct
    {
        char* target;
        gpointer thereWereOtherElementsButTheyWereTruncatedForAnExample;
        gpointer builder;
    }
    MakeMetaUI;
    
    void doSomething(gpointer);
    void freeMetaUI(gpointer p)
    {
        MakeMetaUI* ui = static_cast<MakeMetaUI*>(p);
    
        doSomething(ui->builder);
        g_free(ui->target);
        memset(ui, ~0, sizeof(MakeMetaUI));
        g_free(ui);
        
    }
    g++ `pkg-config --cflags --libs glib-2.0` test.cpp -O2 -masm=intel -S
    output
    	.file	"test.cpp"
    	.intel_syntax noprefix
    	.text
    	.p2align 4
    	.globl	_Z10freeMetaUIPv
    	.type	_Z10freeMetaUIPv, @function
    _Z10freeMetaUIPv:
    .LFB334:
    	.cfi_startproc
    	endbr64
    	push	rbp
    	.cfi_def_cfa_offset 16
    	.cfi_offset 6, -16
    	mov	rbp, rdi
    	mov	rdi, QWORD PTR 16[rdi]
    	call	_Z11doSomethingPv@PLT
    	mov	rdi, QWORD PTR 0[rbp]
    	call	g_free@PLT
    	mov	QWORD PTR 16[rbp], -1
    	pcmpeqd	xmm0, xmm0
    	mov	rdi, rbp
    	movups	XMMWORD PTR 0[rbp], xmm0
    	pop	rbp
    	.cfi_def_cfa_offset 8
    	jmp	g_free@PLT
    	.cfi_endproc
    .LFE334:
    	.size	_Z10freeMetaUIPv, .-_Z10freeMetaUIPv
    	.ident	"GCC: (Ubuntu 10.3.0-1ubuntu1~20.04) 10.3.0"
    	.section	.note.GNU-stack,"",@progbits
    	.section	.note.gnu.property,"a"
    	.align 8
    	.long	 1f - 0f
    	.long	 4f - 1f
    	.long	 5
    0:
    	.string	 "GNU"
    1:
    	.align 8
    	.long	 0xc0000002
    	.long	 3f - 2f
    2:
    	.long	 0x3
    3:
    	.align 8
    4:
    
    clang++ `pkg-config --cflags --libs glib-2.0` test.cpp -O2 -masm=intel -S
    output
    	.text
    	.intel_syntax noprefix
    	.file	"test.cpp"
    	.globl	_Z10freeMetaUIPv                # -- Begin function _Z10freeMetaUIPv
    	.p2align	4, 0x90
    	.type	_Z10freeMetaUIPv,@function
    _Z10freeMetaUIPv:                       # @_Z10freeMetaUIPv
    	.cfi_startproc
    # %bb.0:
    	push	rbx
    	.cfi_def_cfa_offset 16
    	.cfi_offset rbx, -16
    	mov	rbx, rdi
    	mov	rdi, qword ptr [rdi + 16]
    	call	_Z11doSomethingPv
    	mov	rdi, qword ptr [rbx]
    	call	g_free
    	pcmpeqd	xmm0, xmm0
    	movdqu	xmmword ptr [rbx], xmm0
    	mov	qword ptr [rbx + 16], -1
    	mov	rdi, rbx
    	pop	rbx
    	.cfi_def_cfa_offset 8
    	jmp	g_free                          # TAILCALL
    .Lfunc_end0:
    	.size	_Z10freeMetaUIPv, .Lfunc_end0-_Z10freeMetaUIPv
    	.cfi_endproc
                                            # -- End function
    	.ident	"Ubuntu clang version 11.1.0-++20210428103817+1fdec59bffc1-1~exp1~20210428204431.166"
    	.section	".note.GNU-stack","",@progbits
    	.addrsig
    


  1. fiego
    30.07.2021 22:01
    -1

    Я, возможно, не гуру теории программирования, но пояснения к "Фрагмент N4: неудачное явное приведение типа" меня как-то не удовлетворили. Я так понимаю, автор собирал библиотеки на 64-битной системе, где int имеет размер 32 бита, а size_t размер 64 бита. В этом случае объяснение и решение верное, но разве эра 32-битных ситем закончилась и их нигде и никак не осталось? Может быть это уже можно считать верным для десктопа, но для встраиваемых систем это точно вряд ли. Как я понимаю, на 32-битных ситсемах размер size_t будет те же 32 бита, а тогда ни о каком решении проблемы переполнения речи нет.


  1. gban
    31.07.2021 07:41

    В Transmission под win есть небольшая ошибка - если указать каталог для загрузок на диске, отличном от установленного клиента, невозможно открыть загруженный торрент через меню правым кликом по торренту в окне клиента или хоткеем Ctrl-E. А в остальном нормально....