Эта статья является продолжением цикла публикаций об использовании PVS-Studio в облачных системах. На этот раз мы рассмотрим работу анализатора совместно с GitLab CI — продуктом от GitLab Inc. Интеграция статического анализатора в CI систему позволяет выявить баги сразу после этапа сборки проекта и является очень эффективным способом сократить затраты на обнаружение ошибок.
Список других наших статей по интеграции в облачные CI системы:
- PVS-Studio идёт в облака: Azure DevOps
- PVS-Studio идёт в облака: Travis CI
- PVS-Studio идёт в облака: CircleCI
Информация об используемом ПО
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.viva64.com/etc/pubkey.txt | apt-key add -
- wget -O /etc/apt/sources.list.d/viva64.list
https://files.viva64.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
-u "PVS-Studio report, commit:GITLAB_COMMIT"
-m "PVS-Studio report, commit:GITLAB_COMMIT"
-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.viva64.com/etc/pubkey.txt | apt-key add -
- wget -O /etc/apt/sources.list.d/viva64.list
https://files.viva64.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
-u "PVS-Studio report, commit:GITLAB_COMMIT"
-m "PVS-Studio report, commit:GITLAB_COMMIT"
-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 системы. Проверка кода же позволит выявлять проблемы сразу после сборки, что поможет устранять их тогда, когда сложность и стоимость правок ещё малы.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Vladislav Stolyarov. PVS-Studio in the Clouds: GitLab CI/CD.
Автор: Vladislav Stolyarov