>
>
>
PVS-Studio идёт в облака: GitLab CI/CD

Владислав Столяров
Статей: 20

PVS-Studio идёт в облака: GitLab CI/CD

Эта статья является продолжением цикла публикаций об использовании PVS-Studio в облачных системах. На этот раз мы рассмотрим работу анализатора совместно с GitLab CI - продуктом от GitLab Inc. Интеграция статического анализатора в CI систему позволяет выявить баги сразу после этапа сборки проекта и является очень эффективным способом сократить затраты на обнаружение ошибок.

Для получения актуальной информации перейдите на обновляемую страницу документации " Использование в GitLab CI/CD".

Список других наших статей по интеграции в облачные CI системы:

Информация об используемом ПО

GitLab - это онлайн-сервис, предназначенный для управления репозиториями. Его можно использовать прямо в браузере на официальном сайте, зарегистрировав аккаунт, или установить и развернуть на собственном сервере.

PVS-Studio - инструмент статического анализа кода, предназначенный для выявления ошибок и потенциальных уязвимостей в программах, написанных на языках С, C++, C# и Java. Работает в 64-битных системах на Windows, Linux и macOS и может анализировать код, предназначенный для 32-битных, 64-битных и встраиваемых ARM платформ. Если вы впервые будете пробовать статический анализ кода для проверки своих проектов, то рекомендуем познакомиться со статьёй о том, как быстро посмотреть самые интересные предупреждения PVS-Studio и оценить возможности этого инструмента.

Для демонстрации работы статического анализатора в облаке будет использоваться проект OBS. Open Broadcaster Software - свободный и открытый набор программ для записи видео и потокового вещания. OBS предоставляет возможность перехвата с устройств и источников в реальном времени, композицию сцен, декодировку, запись и вещание. Передача данных осуществляется в основном через протокол Real Time Messaging Protocol, и данные могут быть переданы в любой источник, поддерживающий RTMP— в программе имеются готовые предустановки для прямой трансляции на самые популярные стриминговые платформы.

Настройка

Для начала работы с GitLab перейдём на сайт и нажмём кнопку Register:

Можно зарегистрироваться, привязав аккаунты таких сервисов, как: GitHub, Twitter, Google, BitBucket, Saleforce или просто заполнив открывшуюся форму. После авторизации GitLab встречает нас предложением создать проект:

Список платформ, с которых можно совершить импорт:

Для наглядности создадим пустой проект:

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

При запуске задачи GitLab CI берет инструкции из файла .gitlab-ci.yml. Его можно добавить либо кликнув на кнопку Set up CI/CD, либо просто создав в локальном репозитории и загрузив на сайт. Воспользуемся первым вариантом:

Составим минимальную обёртку для скрипта:

image: debian
job:
  script:

Скачиваем анализатор и утилиту sendemail, которая понадобится нам в дальнейшем:

- apt-get update && apt-get -y install wget gnupg 
- wget -O - https://files.pvs-studio.com/etc/pubkey.txt | apt-key add - 
- wget -O /etc/apt/sources.list.d/viva64.list
  https://files.pvs-studio.com/etc/viva64.list
- apt-get update && apt-get -y install pvs-studio
  sendemail

Далее, установим зависимости и утилиты для сборки OBS:

- apt-get -y install build-essential cmake  
  make pkg-config libx11-dev libgl1-mesa-dev 
  libpulse-dev libxcomposite-dev 
  libxinerama-dev libv4l-dev libudev-dev libfreetype6-dev 
  libfontconfig-dev qtbase5-dev 
  libqt5x11extras5-dev libx264-dev libxcb-xinerama0-dev 
  libxcb-shm0-dev libjack-jackd2-dev libcurl4-openssl-dev 
  libavcodec-dev libqt5svg5 libavfilter-dev 
  libavdevice-dev libsdl2-dev ffmpeg
  qt5-default qtscript5-dev libssl-dev 
  qttools5-dev qttools5-dev-tools qtmultimedia5-dev 
  libqt5svg5-dev libqt5webkit5-dev  libasound2 
  libxmu-dev libxi-dev freeglut3-dev libasound2-dev 
  libjack-jackd2-dev libxrandr-dev libqt5xmlpatterns5-dev 
  libqt5xmlpatterns5 coccinelle parallel
  libapparmor-dev libcap-dev libseccomp-dev
  python3-dev python3-setuptools docbook2x
  libgnutls28-dev libselinux1-dev linux-libc-dev
  libtool autotools-dev 
  libio-socket-ssl-perl 
  libnet-ssleay-perl ca-certificates

Теперь нам нужно создать файл с лицензией анализатора.По умолчанию будет создан файл PVS-Studio.lic в директории ../.config/PVS-Studio. В этом случае файл лицензии можно не указывать в параметрах запуска анализатора, он будет подхвачен автоматически:

- pvs-studio-analyzer credentials $PVS_NAME $PVS_KEY

Здесь PVS_NAME и PVS_KEY – названия переменных, значения которых мы указываем в настройках. Они будут хранить логин и лицензионный ключ PVS-Studio. Чтобы установить их значения, перейдём: Settings > CI/CD > Variables.

Соберём проект, используя cmake:

- cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On /builds/Stolyarrrov/obscheck/
- make -j4

Потом запустим анализатор:

- pvs-studio-analyzer analyze -o PVS-Studio.log

PVS-Studio.log будет хранить результаты анализа. Получившийся файл с отчётом не предназначен для чтения и, чтобы привести его в доступный человеческому глазу вид, нам нужна утилита plog-converter. Данная программа конвертирует лог анализатора в различные форматы. Для удобства чтения совершим преобразование в html формат:

- plog-converter -t html PVS-Studio.log -o PVS-Studio.html

Отчёт можно выгрузить при помощи артефактов, но мы воспользуемся альтернативным способом и отправим файл с результатами работы анализатора на почту при помощи утилиты sendemail:

- sendemail -t $MAIL_TO
  -m "PVS-Studio report, commit:$CI_COMMIT_SHORT_SHA"
  -s $GMAIL_PORT
  -o tls=auto
  -f $MAIL_FROM 
  -xu $MAIL_FROM 
  -xp $MAIL_FROM_PASS 
  -a PVS-Studio.log PVS-Studio.html

Полный .gitlab-ci.yml:

image: debian
job:
  script:
    - apt-get update && apt-get -y install wget gnupg 
    - wget -O - https://files.pvs-studio.com/etc/pubkey.txt | apt-key add - 
    - wget -O /etc/apt/sources.list.d/viva64.list 
      https://files.pvs-studio.com/etc/viva64.list
    - apt-get update && apt-get -y install pvs-studio
      sendemail
    - apt-get -y install build-essential cmake  
      pkg-config libx11-dev libgl1-mesa-dev 
      libpulse-dev libxcomposite-dev 
      libxinerama-dev libv4l-dev libudev-dev libfreetype6-dev 
      libfontconfig-dev qtbase5-dev 
      libqt5x11extras5-dev libx264-dev libxcb-xinerama0-dev 
      libxcb-shm0-dev libjack-jackd2-dev libcurl4-openssl-dev 
      libavcodec-dev libqt5svg5 libavfilter-dev 
      libavdevice-dev libsdl2-dev ffmpeg
      qt5-default qtscript5-dev libssl-dev 
      qttools5-dev qttools5-dev-tools qtmultimedia5-dev 
      libqt5svg5-dev libqt5webkit5-dev  libasound2 
      libxmu-dev libxi-dev freeglut3-dev libasound2-dev 
      libjack-jackd2-dev libxrandr-dev libqt5xmlpatterns5-dev 
      libqt5xmlpatterns5 coccinelle parallel
      libapparmor-dev libcap-dev libseccomp-dev
      python3-dev python3-setuptools docbook2x
      libgnutls28-dev libselinux1-dev linux-libc-dev
      libtool autotools-dev 
      make libio-socket-ssl-perl 
      libnet-ssleay-perl ca-certificates
    - pvs-studio-analyzer credentials $PVS_NAME $PVS_KEY
    - cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On /builds/Stolyarrrov/obscheck/
    - make -j4
    - pvs-studio-analyzer analyze -o PVS-Studio.log 
    - plog-converter -t html PVS-Studio.log -o PVS-Studio.html
    - sendemail -t $MAIL_TO
      -m "PVS-Studio report, commit:$CI_COMMIT_SHORT_SHA"
      -s $GMAIL_PORT
      -o tls=auto
      -f $MAIL_FROM 
      -xu $MAIL_FROM 
      -xp $MAIL_FROM_PASS 
      -a PVS-Studio.log PVS-Studio.html

Нажмём на кнопку commit changes. Если мы всё сделали правильно, нам высветится надпись: This GitLab CI configuration is valid. Чтобы отследить прогресс выполнения, перейдём во вкладку CI/CD > Pipelines.

Нажмём на running. Мы увидим окно терминала виртуальной машины, на которой выполняется наш файл конфигурации. Спустя некоторое время получаем сообщение: job succeeded.

Значит, время перейти на почту и открыть html файл с предупреждениями.

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

Давайте теперь для демонстрации сути статического анализа кода посмотрим на некоторые предупреждения из отчёта, которые указывают на ошибки в проекте Open Broadcaster Software. Целью статьи является описание принципов взаимодействия PVS-Studio и GitLab CI/CD, поэтому выписаны только некоторые интересные фрагменты кода с ошибками. Мы готовы предоставить авторам проекта временную лицензию, и при желании они могут провести более тщательный анализ проекта. Или они могут воспользоваться одним из вариантов бесплатного лицензирования PVS-Studio.

Также каждый желающий может самостоятельно получить триальный ключ для изучения возможностей PVS-Studio и проверки своих проектов.

Итак, давите посмотрим на некоторые примеры найденных ошибок в Open Broadcaster Software.

Предупреждение N1

V547 Expression 'back_size' is allways true. circlebuf.h (138)

struct circlebuf 
{
  ....
  size_t capacity;
};
static inline void circlebuf_place(struct circlebuf *cb, 
      size_t position,....,const void *data, size_t size)
{
  ....
  size_t data_end_pos;
  data_end_pos = position + size;
  if (data_end_pos > cb->capacity) 
  {
    size_t back_size = data_end_pos - cb->capacity;
    if (back_size)
    {
      memcpy((uint8_t *)cb->data + position, data, loop_size);
    }
  ....
}

Обратите внимание на строку: if (data_end_pos > cb->capacity), выполнение условия будет обозначать, что переменная back_size, определённая в строчке ниже, всегда будет больше нуля, так как происходит вычитание заведомо меньшего из заведомо большего, а значит, условие, находящееся ещё на строчку ниже, всегда будет true. Избыточное условие не так безобидно, когда под ним находится код, модифицирующий данные.

Предупреждения N2, N3

V629 Consider inspecting the '1 << indent' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. profiler.c (610)

static void profile_print_entry(uint64_t active, unsigned indent, ....)
{
  ....
  active &= (1 << indent) - 1;
  ....
}

Подозрительным здесь выглядит смешение операций над 32-битными и 64-битными типами. Сначала вычисляют маску, используя 32-битные типы (выражение (1 << indent) - 1), а затем она неявно расширяется до 64-битного типа в выражении active &= ... . Скорее всего при вычислении маски также предполагалось использование 64-битных типов.

Корректный вариант кода:

active &= ((uint64_t)(1) << indent) - 1;

Или:

active &= (1ull << indent) - 1;

Также, copy-paste данного блока кода есть ниже, о чём анализатор тоже выдал предупреждение: V629 Consider inspecting the '1 << indent' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. profiler.c (719)

Предупреждение N4

V761 Four identical blocks of text were found. 'obs-audio-controls.c' (353)

static float get_true_peak(....)
{
  ....
  peak = _mm_max_ps(peak, abs_ps(intrp_samples));
  SHIFT_RIGHT_2PS(new_work, work);
  VECTOR_MATRIX_CROSS_PS(intrp_samples, work, m3, m1, p1, p3);

  peak = _mm_max_ps(peak, abs_ps(intrp_samples));
  SHIFT_RIGHT_2PS(new_work, work);
  VECTOR_MATRIX_CROSS_PS(intrp_samples, work, m3, m1, p1, p3);

  peak = _mm_max_ps(peak, abs_ps(intrp_samples));
  SHIFT_RIGHT_2PS(new_work, work);
  VECTOR_MATRIX_CROSS_PS(intrp_samples, work, m3, m1, p1, p3);

  peak = _mm_max_ps(peak, abs_ps(intrp_samples));
  SHIFT_RIGHT_2PS(new_work, work);
  VECTOR_MATRIX_CROSS_PS(intrp_samples, work, m3, m1, p1, p3);
  ....
}

Четыре одинаковых блока. Почти всегда такой код свидетельствует о copy-paste ошибке. Скорее всего данные функции должны были быть вызваны с разными аргументами. Даже если нет, такой код выглядит странно. Хорошим решением было бы написать блок один раз и обернуть его в цикл:

for(size_t i = 0; i < 3; i++)
{
  peak = _mm_max_ps(peak, abs_ps(intrp_samples));
  SHIFT_RIGHT_2PS(new_work, work);
  VECTOR_MATRIX_CROSS_PS(intrp_samples, work, m3, m1, p1, p3);
}

Предупреждение N5

V560 A part of conditional expression is always false: '!modifiers'. obs-hotkey.c (662)

typedef struct obs_key_combination obs_key_combination_t;
struct obs_key_combination 
{
  uint32_t modifiers;
  obs_key_t key;
};
static inline void load_binding(....)
{
  obs_key_combination_t combo = {0};
  uint32_t *modifiers = &combo.modifiers;
  load_modifier(modifiers, data, "shift", INTERACT_SHIFT_KEY);
  load_modifier(modifiers, data, "control", INTERACT_CONTROL_KEY);
  load_modifier(modifiers, data, "alt", INTERACT_ALT_KEY);
  load_modifier(modifiers, data, "command", INTERACT_COMMAND_KEY);
  if (!modifiers && (combo.key == OBS_KEY_NONE || 
                     combo.key >= OBS_KEY_LAST_VALUE))
  {
    ....
  }
  ....
}

Определение функции load_modifier:

static inline void load_modifier(uint32_t *modifiers, 
                                 obs_data_t *data, 
                                 const char *name, 
                                 uint32_t flag)
{
  if (obs_data_get_bool(data, name))
    *modifiers |= flag;
}

Как мы видим, modifiers - указатель, который инициализируется адресом поля modifiers структуры combo. Так как его значение не изменяется до места проверки, он так и останется ненулевым. Более того - между местом инициализации и проверки, указатель используется при вызовах функции load_modifier, где разыменовывается. Соответственно, проверка !modifiers не имеет смысла, так как в результате работы оператора && мы всегда будем получать false при вычислении логического выражения. Думаю, что программист хотел проверить целочисленное значение, лежащие по адресу, которое хранится в указателе modifiers, но забыл разыменовать этот указатель.

Т.е. мне кажется, что проверка должна быть такой:

if (!*modifiers && ....)

Или такой:

if (!combo.modifiers && ....)

Предупреждение N6

V575 The potential null pointer is passed into 'strncpy' function. Inspect the first argument. Check lines: 2904, 2903. rtmp.c (2904)

static int PublisherAuth(....)
{
  ....
  ptr = malloc(r->Link.app.av_len + pubToken.av_len);
  strncpy(ptr, r->Link.app.av_val, r->Link.app.av_len);
  ....
}

Зачастую, данный код является небезопасным, поскольку не учитывает, что malloc может вернуть нулевой указатель. Если malloc вернёт NULL, в данном случае возникнет неопределённое поведение, так как первый аргумент функции strncpy будет иметь значение NULL.

Подробнее про то, почему важно проверять возвращаемое значение функции malloc, можно почитать в соответствующей статье.

Предупреждения N7, N8, N9

Попробуем угадать в каких из кейсов могут происходить неправильные вычисления:

class OBSProjector : public OBSQTDisplay 
{
  ....
  float sourceX, sourceY, ....;
  ....
}
....
void OBSProjector::OBSRenderMultiview(....)
{
  OBSProjector *window = (OBSProjector *)data;
  ....
  auto calcBaseSource = [&](size_t i) 
  {
    switch (multiviewLayout) 
    {
    case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
      window->sourceX = (i % 6) * window->scenesCX;
      window->sourceY =
      window->pvwprgCY + (i / 6) * window->scenesCY;
      break;
    case MultiviewLayout::VERTICAL_LEFT_8_SCENES:
      window->sourceX = window->pvwprgCX;
      window->sourceY = (i / 2) * window->scenesCY;
      if (i % 2 != 0)
      {
        window->sourceX += window->scenesCX;
      }
      break;
    case MultiviewLayout::VERTICAL_RIGHT_8_SCENES:
      window->sourceX = 0;
      window->sourceY = (i / 2) * window->scenesCY;
      if (i % 2 != 0)
      {
        window->sourceX = window->scenesCX;
      }
      break;
    case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES:
      if (i < 4) 
      {
        window->sourceX = (float(i) * window->scenesCX);
        window->sourceY = 0;
      } else 
      {
        window->sourceX =
       (float(i - 4) * window->scenesCX);
       window->sourceY = window->scenesCY;
      }
      break;
    default:// MultiviewLayout::HORIZONTAL_TOP_8_SCENES:
      if (i < 4) 
      {
        window->sourceX = (float(i) * window->scenesCX);
        window->sourceY = window->pvwprgCY;
      } else
      {
        window->sourceX =
        (float(i - 4) * window->scenesCX);
        window->sourceY =
        window->pvwprgCY + window->scenesCY;
      }
    }
  }
  ....
}

Предупреждения анализатора:

  • V636 The 'i / 6' expression was implicitly cast from 'size_t' type to 'float' type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. window-projector.cpp (330)
  • V636 The 'i / 2' expression was implicitly cast from 'size_t' type to 'float' type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. window-projector.cpp (334)
  • V636 The 'i / 2' expression was implicitly cast from 'size_t' type to 'float' type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. window-projector.cpp (340)

Правильный ответ: в теx, в которых i не приводится к типу float. В выражениях, на которые нам показывает анализатор, происходит целочисленное деление. Данный код может работать не совсем так, как рассчитывал программист.

Заключение

Как мы видим, интегрировать статический анализатор PVS-Studio в свой проект на GitLab довольно просто. Для этого достаточно написать всего один файл конфигурации и поместить его в свой облачный репозиторий. Благодаря тому, что у GitLab есть собственная интегрированная виртуальная машина, нам даже не нужно тратить много времени для настройки CI системы. Проверка кода же позволит выявлять проблемы сразу после сборки, что поможет устранять их тогда, когда сложность и стоимость правок ещё малы.