Вебинар: Использование статических анализаторов кода при разработке безопасного ПО - 19.12
На данный момент облачные CI-системы - очень востребованный сервис. В этой статье мы расскажем, как, с помощью уже существующих средств, доступных в PVS-Studio, можно интегрировать анализ исходного кода с облачной CI платформой, на примере сервиса Travis CI.
Для получения актуальной информации перейдите на обновляемую страницу документации "Использование в Travis CI".
Почему мы рассматриваем сторонние облака и не делаем собственное? Есть ряд причин, и главная из них, что организация SaaS — это достаточно дорогая и непростая процедура. На самом деле, непосредственно интегрировать анализ PVS-Studio со сторонней облачной платформой (будь то открытые платформы наподобие CircleCI, Travis CI, GitLab, или какое-то специализированное enterprise-решение, используемое только в одной конкретной компании) – задача достаточно простая и тривиальная. То есть можно сказать, что PVS-Studio уже доступен "в облаках". Совсем другой вопрос в организации и предоставлении инфраструктуры для такой работы в режиме 24/7. Это задача совсем другого порядка, и PVS-Studio пока что не планирует предоставлять свою собственную облачную платформу непосредственно для запуска на ней анализа.
Travis CI – сервис для сборки и тестирования программного обеспечения, использующего GitHub в качестве хранилища. Travis CI не требует изменения программного кода для использования сервиса, все настройки происходят в файле .travis.yml, расположенном в корне репозитория.
В качестве тестового проекта для проверки с помощью PVS-Studio мы возьмем LXC (Linux Containers). Это система виртуализации на уровне операционной системы для запуска нескольких экземпляров операционной системы Linux на одном узле.
Проект маленький, но для демонстрации более чем достаточен. Вывод команды cloc:
Language |
files |
blank |
comment |
code |
---|---|---|---|---|
C |
124 |
11937 |
6758 |
50836 |
C/C++ Header |
65 |
1117 |
3676 |
3774 |
Примечание: Разработчики LXC уже используют Travis CI, поэтому мы возьмем их конфигурационный файл в качестве основы и отредактируем его для наших целей.
Для начала работы с Travis CI переходим по ссылке и аутентифицируемся, используя GitHub-аккаунт.
В открывшемся окне нужно авторизовать Travis CI.
После авторизации происходит перенаправление на приветственную страницу "First time here? Lets get you started!", где кратко описано, что необходимо дальше сделать для начала работы:
Начнем выполнять эти пункты.
Для добавления в Travis CI нашего репозитория переходим в настройки профиля по ссылке и нажимаем кнопку "Activate".
После нажатия откроется окно с выбором репозиториев, к которым приложению Travis CI будет предоставлен доступ.
Примечание: для предоставления доступа к репозиторию у учетной записи должны быть права администратора на него.
Выбираем нужный репозиторий, подтверждаем выбор кнопкой "Approve & Install", и нас перенаправит обратно на страницу настройки профиля.
Сразу создадим переменные, которые будем использовать для создания файла лицензии анализатора и отсылки его отчетов. Для этого перейдем на страницу настроек - кнопка "Settings" справа от нужного репозитория.
Откроется окно настроек.
Краткое описание настроек:
В секции "Environment Variables" создадим переменные PVS_USERNAME и PVS_KEY, содержащие, соответственно, имя пользователя и лицензионный ключ для статического анализатора. Если у вас нет постоянной лицензии PVS-Studio, то вы можете запросить триальную лицензию.
Тут же создадим переменные MAIL_USER и MAIL_PASSWORD, содержащие имя пользователя и пароль от почтового ящика, который мы будем использовать для отсылки отчетов.
При запуске задачи Travis CI берет инструкции из файла .travis.yml, лежащего в корне репозитория.
Используя Travis CI, мы можем запускать статический анализ как непосредственно в виртуальной машине, так и использовать для этого предварительно настроенный контейнер. Результаты этих подходов ничем не отличаются друг от друга, но использование предварительно настроенного контейнера может пригодиться, например, если у нас уже есть контейнер с каким-то специфическим окружением, внутри которого собирается и тестируется программный продукт, и нет желания восстанавливать это окружение в Travis CI.
Создадим конфигурацию для запуска анализатора в виртуальной машине.
Для сборки и тестирования мы будем использовать виртуальную машину на базе Ubuntu Trusty, ее описание можно посмотреть по ссылке.
Первым делом указываем, что проект написан на С и перечисляем компиляторы, которые будем использовать для сборки:
language: c
compiler:
- gcc
- clang
Примечание: при указании более одного компилятора, задачи будут запускаться параллельно для каждого из них. Подробнее можно прочитать в документации.
Перед началом сборки нам необходимо добавить репозиторий анализатора, установить зависимости и дополнительные пакеты:
before_install:
- sudo add-apt-repository ppa:ubuntu-lxc/daily -y
- wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt |sudo apt-key add -
- sudo wget -O /etc/apt/sources.list.d/viva64.list
https://files.pvs-studio.com/etc/viva64.list
- sudo apt-get update -qq
- sudo apt-get install -qq coccinelle parallel
libapparmor-dev libcap-dev libseccomp-dev
python3-dev python3-setuptools docbook2x
libgnutls-dev libselinux1-dev linux-libc-dev pvs-studio
libio-socket-ssl-perl libnet-ssleay-perl sendemail
ca-certificates
Перед сборкой проекта необходимо подготовить окружение:
script:
- ./coccinelle/run-coccinelle.sh -i
- git diff --exit-code
- export CFLAGS="-Wall -Werror"
- export LDFLAGS="-pthread -lpthread"
- ./autogen.sh
- rm -Rf build
- mkdir build
- cd build
- ../configure --enable-tests --with-distro=unknown
Далее нам необходимо создать файл с лицензией и запустить анализ проекта.
Первой командой создаем файл с лицензией для анализатора. Данные для переменных $PVS_USERNAME и $PVS_KEY берутся из настроек проекта.
- pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
Следующей командой запускаем трассировку сборки проекта:
- pvs-studio-analyzer trace -- make -j4
После запускаем статический анализ.
Примечание: при использовании триальной лицензии необходимо указывать параметр ‑‑disableLicenseExpirationCheck .
- pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic
-o PVS-Studio-${CC}.log
–-disableLicenseExpirationCheck
Последней командой файл с результатами работы анализатора конвертируется в html-отчет.
- plog-converter -t html PVS-Studio-${CC}.log
-o PVS-Studio-${CC}.html
Так как TravisCI не позволяет изменять формат почтовых уведомлений, то для отсылки отчетов на последнем шаге воспользуемся пакетом sendemail:
- sendemail -t mail@domain.com
-u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT"
-m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT"
-s smtp.gmail.com:587
-xu $MAIL_USER
-xp $MAIL_PASSWORD
-o tls=yes
-f $MAIL_USER
-a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
Полный текст конфигурационного файла для запуска анализатора в виртуальной машине:
language: c
compiler:
- gcc
- clang
before_install:
- sudo add-apt-repository ppa:ubuntu-lxc/daily -y
- wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt |sudo apt-key add -
- sudo wget -O /etc/apt/sources.list.d/viva64.list
https://files.pvs-studio.com/etc/viva64.list
- sudo apt-get update -qq
- sudo apt-get install -qq coccinelle parallel
libapparmor-dev libcap-dev libseccomp-dev
python3-dev python3-setuptools docbook2x
libgnutls-dev libselinux1-dev linux-libc-dev pvs-studio
libio-socket-ssl-perl libnet-ssleay-perl sendemail
ca-certificates
script:
- ./coccinelle/run-coccinelle.sh -i
- git diff --exit-code
- export CFLAGS="-Wall -Werror"
- export LDFLAGS="-pthread -lpthread"
- ./autogen.sh
- rm -Rf build
- mkdir build
- cd build
- ../configure --enable-tests --with-distro=unknown
- pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
- pvs-studio-analyzer trace -- make -j4
- pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic
-o PVS-Studio-${CC}.log
--disableLicenseExpirationCheck
- plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html
- sendemail -t mail@domain.com
-u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT"
-m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT"
-s smtp.gmail.com:587
-xu $MAIL_USER
-xp $MAIL_PASSWORD
-o tls=yes
-f $MAIL_USER
-a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
Для запуска статического анализатора в контейнере, предварительно создадим его, используя следующий Dockerfile:
FROM docker.io/ubuntu:trusty
ENV CFLAGS="-Wall -Werror"
ENV LDFLAGS="-pthread -lpthread"
RUN apt-get update && apt-get install -y software-properties-common wget \
&& wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt |
sudo 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 install -yqq coccinelle parallel
libapparmor-dev libcap-dev libseccomp-dev
python3-dev python3-setuptools docbook2x
libgnutls-dev libselinux1-dev linux-libc-dev
pvs-studio git libtool autotools-dev automake
pkg-config clang make libio-socket-ssl-perl
libnet-ssleay-perl sendemail ca-certificates \
&& rm -rf /var/lib/apt/lists/*
В этом случае конфигурационный файл может выглядеть так:
before_install:
- docker pull docker.io/oandreev/lxc
env:
- CC=gcc
- CC=clang
script:
- docker run
--rm
--cap-add SYS_PTRACE
-v $(pwd):/pvs
-w /pvs
docker.io/oandreev/lxc
/bin/bash -c " ./coccinelle/run-coccinelle.sh -i
&& git diff --exit-code
&& ./autogen.sh
&& mkdir build && cd build
&& ../configure CC=$CC
&& pvs-studio-analyzer credentials
$PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
&& pvs-studio-analyzer trace -- make -j4
&& pvs-studio-analyzer analyze -j2
-l PVS-Studio.lic
-o PVS-Studio-$CC.log
--disableLicenseExpirationCheck
&& plog-converter -t html
-o PVS-Studio-$CC.html
PVS-Studio-$CC.log
&& sendemail -t mail@domain.com
-u 'PVS-Studio $CC report, commit:$TRAVIS_COMMIT'
-m 'PVS-Studio $CC report, commit:$TRAVIS_COMMIT'
-s smtp.gmail.com:587
-xu $MAIL_USER -xp $MAIL_PASSWORD
-o tls=yes -f $MAIL_USER
-a PVS-Studio-${CC}.log PVS-Studio-${CC}.html"
Как можно увидеть, в данном случае мы ничего не делаем внутри виртуальной машины, и абсолютно все действия по сборке и тестированию проекта происходят внутри контейнера.
Примечание: при запуске контейнера необходимо указывать параметр ‑‑cap-add SYS_PTRACE либо ‑‑security-opt seccomp:unconfined, так как для трассировки компиляции используется системный вызов ptrace.
Загружаем конфигурационный файл в корень репозитория и видим, что Travis CI получил уведомление о наличии изменений в проекте и автоматически запустил сборку.
Подробную информацию о ходе сборки и проверке анализатором можно увидеть в консоли.
После окончания тестов мы получим на почту 2 письма: одно – с результатами статического анализа для сборки проекта с использованием gcc, и второе – соответственно, clang.
В целом проект достаточно чистый, анализатор выдал всего 24 критических и 46 средних предупреждений. Для демонстрации работы рассмотрим пару интересных уведомлений:
V590 Consider inspecting the 'ret != (- 1) && ret == 1' expression. The expression is excessive or contains a misprint. attach.c 107
#define EOF -1
static struct lxc_proc_context_info *lxc_proc_get_context_info(pid_t pid)
{
....
while (getline(&line, &line_bufsz, proc_file) != -1)
{
ret = sscanf(line, "CapBnd: %llx", &info->capability_mask);
if (ret != EOF && ret == 1) // <=
{
found = true;
break;
}
}
....
}
Если ret == 1, то он точно не равен -1 (EOF). Избыточная проверка, можно убрать ret != EOF.
Таких предупреждений было выдано еще два:
V784 The size of the bit mask is less than the size of the first operand. This will cause the loss of higher bits. conf.c 1879
struct mount_opt
{
char *name;
int clear;
int flag;
};
static void parse_mntopt(char *opt, unsigned long *flags,
char **data, size_t size)
{
struct mount_opt *mo;
/* If opt is found in mount_opt, set or clear flags.
* Otherwise append it to data. */
for (mo = &mount_opt[0]; mo->name != NULL; mo++)
{
if (strncmp(opt, mo->name, strlen(mo->name)) == 0)
{
if (mo->clear)
{
*flags &= ~mo->flag; // <=
}
else
{
*flags |= mo->flag;
}
return;
}
}
....
}
Под Linux'ом long - это 64-битная целочисленная переменная, mo->flag - 32-битная целочисленная переменная. Использование mo->flag в качестве битовой маски приведет к потере 32 старших бит. Выполняется неявное приведение битовой маски к 64-битной целочисленной переменной после побитовой инверсии. Старшие биты этой маски будут нулевыми.
Продемонстрируем на примере:
unsigned long long x;
unsigned y;
....
x &= ~y;
Правильный вариант кода:
*flags &= ~(unsigned long)(mo->flag);
Анализатор выдал еще одно подобное предупреждение:
V612 An unconditional 'return' within a loop. conf.c 3477
#define lxc_list_for_each(__iterator, __list) \
for (__iterator = (__list)->next; __iterator != __list; \
__iterator = __iterator->next)
static bool verify_start_hooks(struct lxc_conf *conf)
{
char path[PATH_MAX];
struct lxc_list *it;
lxc_list_for_each (it, &conf->hooks[LXCHOOK_START]) {
int ret;
char *hookname = it->elem;
ret = snprintf(path, PATH_MAX, "%s%s",
conf->rootfs.path ? conf->rootfs.mount : "",
hookname);
if (ret < 0 || ret >= PATH_MAX)
return false;
ret = access(path, X_OK);
if (ret < 0) {
SYSERROR("Start hook \"%s\" not found in container",
hookname);
return false;
}
return true; // <=
}
return true;
}
Запускают цикл и на первой итерации его прерывают. Возможно, так и задумывалось, но тогда цикл можно опустить.
V557 Array underrun is possible. The value of 'bytes - 1' index could reach -1. network.c 2570
static int lxc_create_network_unpriv_exec(const char *lxcpath,
const char *lxcname,
struct lxc_netdev *netdev,
pid_t pid,
unsigned int hooks_version)
{
int bytes;
char buffer[PATH_MAX] = {0};
....
bytes = lxc_read_nointr(pipefd[0], &buffer, PATH_MAX);
if (bytes < 0)
{
SYSERROR("Failed to read from pipe file descriptor");
close(pipefd[0]);
}
else
{
buffer[bytes - 1] = '\0';
}
....
}
Из pipe'а читаются байты в буфер. В случае ошибки, функция lxc_read_nointr вернет отрицательное значение. Если все прошло успешно, то последним элементом записывают нуль-терминал. Однако, если будет прочитано 0 байт, то произойдет выход за границу буфера, что ведет к неопределенному поведению.
Анализатор выдал еще одно подобное предупреждение:
V576 Incorrect format. Consider checking the third actual argument of the 'sscanf' function. It's dangerous to use string specifier without width specification. Buffer overflow is possible. lxc_unshare.c 205
static bool lookup_user(const char *oparg, uid_t *uid)
{
char name[PATH_MAX];
....
if (sscanf(oparg, "%u", uid) < 1)
{
/* not a uid -- perhaps a username */
if (sscanf(oparg, "%s", name) < 1) // <=
{
free(buf);
return false;
}
....
}
....
}
Использование sscanf в данном случае может являться опасным, поскольку если длина буфера oparq окажется больше длины буфера name, произойдет выход за границу при формировании буфера name.
Как мы увидели, настроить проверку статическим анализатором кода нашего проекта в облаке – достаточно простая задача. Для этого необходимо всего лишь добавить один файл в репозиторий и потратить минимальное время на настройку CI-системы. В результате же мы получим инструмент, позволяющий выявлять проблемный код на этапе написания, и не позволяющий ошибкам попадать на следующие этапы тестирования, где их исправление займет больше времени и ресурсов.
Конечно, использование PVS-Studio совместно с облачными платформами не ограничивается только Travis CI. По аналогии с описанным в статье способом, с минимальными отличиями, анализ PVS-Studio можно интегрировать и с другими популярными облачными CI решениями, такими, как CircleCI, GitLab и т.п.
0