PVS-Studio идёт в облака – запуск анализа на Travis CI

    На данный момент облачные CI-системы — очень востребованный сервис. В этой статье мы расскажем, как, с помощью уже существующих средств, доступных в PVS-Studio, можно интегрировать анализ исходного кода с облачной CI платформой, на примере сервиса Travis CI.

    Picture 1


    Почему мы рассматриваем сторонние облака и не делаем собственное? Есть ряд причин, и главная из них, что организация 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-аккаунт.

    Picture 17

    В открывшемся окне нужно авторизовать Travis CI.

    Picture 16

    После авторизации происходит перенаправление на приветственную страницу«First time here? Lets get you started!», где кратко описано, что необходимо дальше сделать для начала работы:

    • активировать репозитории;
    • добавить файл .travis.yml в репозиторий;
    • запустить первую сборку.

    Picture 18

    Начнем выполнять эти пункты.

    Для добавления в Travis CI нашего репозитория переходим в настройки профиля по ссылке и нажимаем кнопку «Activate».

    Picture 19

    После нажатия откроется окно с выбором репозиториев, к которым приложению Travis CI будет предоставлен доступ.
    Примечание: для предоставления доступа к репозиторию у учетной записи должны быть права администратора на него.

    Picture 38

    Выбираем нужный репозиторий, подтверждаем выбор кнопкой «Approve & Install», и нас перенаправит обратно на страницу настройки профиля.

    Сразу создадим переменные, которые будем использовать для создания файла лицензии анализатора и отсылки его отчетов. Для этого перейдем на страницу настроек — кнопка «Settings» справа от нужного репозитория.

    Picture 39

    Откроется окно настроек.

    Picture 41

    Краткое описание настроек:

    • Секция «General» – настройка триггеров автозапуска задачи;
    • Секция «Auto Cancelation» – позволяет настроить автоотмену сборки;
    • Секция «Environment Variables» – позволяет определить переменные окружения, содержащие как открытую, так и конфиденциальную информацию, такие как учетные данные, ssh-ключи;
    • Секция «Cron Jobs» – настройка расписания запуска задачи.

    В секции «Environment Variables» создадим переменные PVS_USERNAME и PVS_KEY, содержащие, соответственно, имя пользователя и лицензионный ключ для статического анализатора. Если у вас нет постоянной лицензии PVS-Studio, то вы можете запросить триальную лицензию.

    Picture 5

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

    Picture 4

    При запуске задачи 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.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 -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.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 -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.viva64.com/etc/pubkey.txt | 
            sudo apt-key add - \
        && wget -O /etc/apt/sources.list.d/viva64.list
           https://files.viva64.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 получил уведомление о наличии изменений в проекте и автоматически запустил сборку.

    Подробную информацию о ходе сборки и проверке анализатором можно увидеть в консоли.

    Picture 2

    После окончания тестов мы получим на почту 2 письма: одно – с результатами статического анализа для сборки проекта с использованием gcc, и второе – соответственно, clang.

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


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

    Избыточные условия в if


    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.

    Таких предупреждений было выдано еще два:

    • V590 Consider inspecting the 'ret != (- 1) && ret == 1' expression. The expression is excessive or contains a misprint. attach.c 579
    • V590 Consider inspecting the 'ret != (- 1) && ret == 1' expression. The expression is excessive or contains a misprint. attach.c 583

    Потеря старших битов


    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;

    Picture 3


    Правильный вариант кода:

    *flags &= ~(unsigned long)(mo->flag);

    Анализатор выдал еще одно подобное предупреждение:

    • 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 1933

    Подозрительный цикл


    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 байт, то произойдет выход за границу буфера, что ведет к неопределенному поведению.

    Анализатор выдал еще одно подобное предупреждение:

    • V557 Array underrun is possible. The value of 'bytes — 1' index could reach -1. network.c 2725

    Переполнение буфера


    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 и т.п.

    Полезные ссылки


    • Дополнительную информацию про запуск PVS-Studio в Linux и MacOS можно получить здесь.
    • Про создание, настройку и использование контейнеров с установленным статическим анализатором PVS-Studio можно почитать здесь.
    • Документация TravisCI.



    Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Oleg Andreev. PVS-Studio in the Clouds -Running the Analysis on Travis CI
    PVS-Studio
    229.84
    Static Code Analysis for C, C++, C# and Java
    Share post

    Comments 13

      +3

      За образ с докером — жирный плюс.
      Молодцы, что обратили внимание на интеграцию с CI процессами, в частности, на облачных платформах! Уверен, что это поспособствует популярности вашего продукта.

        +2
         - wget -q -O - http://files.viva64.com/etc/pubkey.txt | sudo apt-key add -

        Здравствуй MITM… Вы бы хоть https какой-то прикрутили, если уж такое советуете...

          –4

          https не спасает от mitm

            0

            Ну, я соглашусь и не соглашусь.
            В случае докера — скорее всего возьмёте образ от вендора. В нем проблемы явно не будет, т.к. либо его собрали нормально, либо должны пересобрать.
            Если собирать самому — да, лучше, если ключи apt зашиты как переменные внутри докерфайла, это гибче и надёжнее. Та же история с контентом, который выкачивают из инета (надо sha суммы класть в образ).
            Не стоит забывать, что докерфайл — это всего лишь чертеж, а не само "изделие", полученное по нему

              0

              Не понимаю, как ваш комментарий касается mitm.
              Тем не менее не стоит доверять даже образам от вендора, есть масса способов залить «исправленный» образ. Про чертеж и хешсумму полностью согласен.

              –1

              Неужели минусующие защищаются от mitm, заменяя http на https в скриптах?

                +2

                Конкретно в этом случае спасает.
                Поясняю: сейчас банальная подмена DNS для files.viva64.com или перехват траффика позволяет засунуть в apt keyring жертвы сгенерированый злоумышлеником ключ. Ну и дальше можно делать что хочешь, например предложить обновить любой пакет в системе, просто подписав левый репозиторий этим ключем.


                Если бы тут был https, то эта атака загнулась бы еще на скачивании ключа. Понятно что владельцы files.viva64.com могут подсунуть левый пакет, но это заметно лучше чем когда это делает вообще кто попало.

                +6
                Поправил ссылки на https.
                  0

                  также apt-key adv fetch-keys 'http://files.viva64.com/etc/pubkey.txt'

                0
                Отличные новости, спасибо!

                Теперь ждём веб-сервис для работы с результатами анализа. Ну это, по крайней мере, выглядит как логичное развитие продукта, раз уж вы «пошли в облака» :)
                  +3
                  Зачем его ждать, если уже давно у нас есть решения для этого:

                  • Всегда достуна интеграция результатов анализа PVS-Studio в SonarQube (url)
                  • Либо с помощью утилиты PlogConverter можно сделать .html-отчет, включающий исходники (url)
                    +1
                    Про SonarQube не знал, спасибо.
                  +2
                  Спасибо за статью!
                  Использую PVS-Studio в своём домашнем проекте уже полгода с интеграцией с travis-ci, после новогодней раздачи ключей для открытых проектов.
                  У меня скрипт запуска получился очень похожий, но только заканчивается на
                  test "$(cat ./pvs-error-list.txt | wc -l)" -le 1

                  для того, чтобы не пропустить ни одной новой ошибки.

                  Only users with full accounts can post comments. Log in, please.