Особенности настройки и запуска PVS-Studio в Docker на примере кода Azure Service Fabric


    Технологии контейнеризации активно используются для сборки и тестирования программного обеспечения. С появлением PVS-Studio для Linux, пользователям стала доступна возможность добавить статический анализ к другим методам тестирования своего проекта на этой платформе, в том числе в Docker. В статье будут описаны особенности работы с анализатором PVS-Studio в Docker, которые повысят качество анализа и удобство использования. А также будут приведены ошибки, найденные в проекте Azure Service Fabric.

    Введение


    Docker — программа, позволяющая операционной системе запускать процессы в изолированном окружении на базе специально созданных образов. Технология контейнеризации стала очень распространённой для многих задач, включая разработку и тестирование программного обеспечения. Статический анализ обычно выполняется в том же окружении, что и сборка проекта, поэтому его использование в Docker очень просто реализуется в уже существующих контейнерах.

    Примеры интеграции и запуска статического анализатора PVS-Studio будут приведены для Linux версии. Но описанные возможности настройки анализатора возможны и даже рекомендуются на любой платформе. Версия анализатора под macOS, которая недавно была представлена общественности, вообще идентична в использовании PVS-Studio для Linux.

    В качестве проекта для интеграции и запуска анализатора в Docker выбран Azure Service Fabric. Service Fabric — это платформа для распределенных систем, предназначенная для развертывания и управления масштабируемыми и высоконадежными распределенными приложениями. Service Fabric работает на Windows и Linux, в любом облаке, любом дата-центре, любом регионе и даже на ноутбуке.

    Поэтапное внедрение анализатора


    Для начала посмотрим, как выполняется сборка проекта, чтобы выбрать способ интеграции анализатора. Порядок вызова скриптов и команд выглядит следующим образом:


    Ниже представлен фрагмент скрипта build.sh, где генерируется проектный файл:

    cmake ${CMakeGenerator} \
      -DCMAKE_C_COMPILER=${CC} \
      -DCMAKE_CXX_COMPILER=${CXX} \
      -DCMAKE_BUILD_TYPE=${BuildType} \
      -DBUILD_THIRD_PARTY=${BuildThirdPartyLib} \
      ${DisablePrecompileFlag} ${ScriptPath}/$DirName

    Для анализа проекта я решил воспользоваться способом из документации, описанном в разделе Быстрый старт/CMake-проект:
    diff --git a/src/build.sh b/src/build.sh
    index 290c57d..5901fd6 100755
    --- a/src/build.sh
    +++ b/src/build.sh
    @@ -179,6 +179,7 @@ BuildDir()
                   -DCMAKE_CXX_COMPILER=${CXX} \
                   -DCMAKE_BUILD_TYPE=${BuildType} \
                   -DBUILD_THIRD_PARTY=${BuildThirdPartyLib} \
    +              -DCMAKE_EXPORT_COMPILE_COMMANDS=On \
                   ${DisablePrecompileFlag} ${ScriptPath}/$DirName
             if [ $? != 0 ]; then
                 let TotalErrors+=1

    Добавление установки анализатора:
    diff --git a/src/build.sh b/src/build.sh
    index 290c57d..581cbaf 100755
    --- a/src/build.sh
    +++ b/src/build.sh
    @@ -156,6 +156,10 @@ BuildDir()
             CXX=${ProjRoot}/deps/third-party/bin/clang/bin/clang++
         fi
     
    +    dpkg -i /src/pvs-studio-6.23.25754.2246-amd64.deb
    +    apt -f install -y
    +    pvs-studio --version
    +

    Каталог src является частью проекта и монтируется в /src. Там же я разметил конфигурационный файл анализатора PVS-Studio.cfg. Тогда вызов анализатора можно выполнить следующим образом:

    diff --git a/src/build.sh b/src/build.sh
    index 290c57d..2a286dc 100755
    --- a/src/build.sh
    +++ b/src/build.sh
    @@ -193,6 +193,9 @@ BuildDir()
         
         cd ${ProjBinRoot}/build.${DirName}
     
    +    pvs-studio-analyzer analyze --cfg /src/PVS-Studio.cfg \
    +      -o ./service-fabric-pvs.log -j4
    +
         if [ "false" = ${SkipBuild} ]; then
             if (( $NumProc <= 0 )); then
                 NumProc=$(($(getconf _NPROCESSORS_ONLN)+0))

    Запуск анализатора я сделал перед сборкой проекта. Это не является правильным решением, но в скрипте очень много условий, при которых запускается сборка проекта, поэтому я немного упростил себе задачу и скомпилировал проект заранее. Разработчикам, которые лучше знают структуру своего проекта, следует интегрировать анализатор после сборки проекта.

    Теперь собрать и проанализировать проект можно следующей командой:

    sudo ./runbuild.sh -release -j4

    Первые результаты анализа расстраивают предупреждениями на многочисленные макросы, несуществующие файлы, неверные пути до файлов исходного кода т.п. В следующем разделе я расскажу о содержимом файла PVS-Studio.cfg, где я добавил несколько настроек, значительно улучшивших анализ.

    Дополнительная настройка анализатора


    Относительный путь до каталога с исходниками

    Для просмотра одного отчёта на разных компьютерах анализатор умеет генерировать отчёт с относительными путями к файлам. Восстановить их можно на другом компьютере с помощью конвертера.

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

    sourcetree-root=/

    Предупреждения на несуществующие файлы

    В контейнере разворачивается каталог /external, который отсутствует в репозитории. Скорее всего, в нём компилируются какие-то зависимости проекта и их можно просто исключить из анализа:

    exclude-path=/external

    Предупреждения на файлы компилятора, тестов и библиотек

    В Docker компилятор может размещаться в нестандартном месте и его библиотеки могут попадать в отчёт. Их тоже необходимо исключить. Для этого из проверки исключается каталог /deps и заодно каталог с тестами:

    exclude-path=/deps
    exclude-path=/src/prod/test

    Борьба с тысячами ложных срабатываний, возникающих из-за неудачных макросов

    Анализатор поддерживает настройку разных диагностик с помощью комментариев. Про них можно почитать здесь и здесь.

    Настройки можно размещать в коде проекта или вынести в отдельный файл, как это сделал я:

    rules-config=/src/service-fabric.pvsconfig

    Содержимое файла service-fabric.pvsconfig:

    #V501
    //-V:CODING_ERROR_ASSERT:501
    //-V:TEST_CONFIG_ENTRY:501
    //-V:VERIFY_IS_TRUE:501
    //-V:VERIFY_ARE_EQUAL:501
    //-V:VERIFY_IS_FALSE:501
    //-V:INTERNAL_CONFIG_ENTRY:501
    //-V:INTERNAL_CONFIG_GROUP:501
    //-V:PUBLIC_CONFIG_ENTRY:501
    //-V:PUBLIC_CONFIG_GROUP:501
    //-V:DEPRECATED_CONFIG_ENTRY:501
    //-V:TR_CONFIG_PROPERTIES:501
    //-V:DEFINE_SECURITY_CONFIG_ADMIN:501
    //-V:DEFINE_SECURITY_CONFIG_USER:501
    //-V:RE_INTERNAL_CONFIG_PROPERTIES:501
    //-V:RE_CONFIG_PROPERTIES:501
    //-V:TR_INTERNAL_CONFIG_PROPERTIES:501
    #V523
    //-V:TEST_COMMIT_ASYNC:523
    #V640
    //-V:END_COM_INTERFACE_LIST:640

    Несколько строк особой разметки удаляют из отчёта тысячи предупреждений на макросы.

    Другие настройки

    Путь к файлу лицензии и включение только диагностик общего назначения (для ускорения анализа):

    lic-file=/src/PVS-Studio.lic
    analysis-mode=4

    Весь файл PVS-Studio.cfg
    lic-file=/src/PVS-Studio.lic
    rules-config=/src/service-fabric.pvsconfig
    exclude-path=/deps
    exclude-path=/external
    exclude-path=/src/prod/test
    analysis-mode=4
    sourcetree-root=/

    Может понадобиться в других проектах


    Другой способ проверки проекта требует наличия системной утилиты strace. Скорее всего, в контейнере она будет отсутствовать и в скрипт необходимо добавить шаг установки этой утилиты из репозитория.

    В контейнер могут подложить компилятор с нестандартным названием например, кросс-компилятор. Я уже писал, что директорию компилятора необходимо исключать из анализа, но в этом случае дополнительно придётся передать анализатору и имя нового компилятора:
    pvs-studio-analyzer analyze ... --compiler COMPILER_NAME...

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

    Просмотр отчёта в Linux или Windows


    Для просмотра отчёта анализатора в Linux, можно добавить в скрипт команду генерации отчёта в нужном формате.

    Например, для просмотра в QtCreator:

    plog-converter -t tasklist -r "~/Projects/service-fabric" \
      ./service-fabric-pvs.log -o ./service-fabric-pvs.tasks

    Или в браузере:

    plog-converter -t fullhtml -r "~/Projects/service-fabric" \
      ./service-fabric-pvs.log -o ./

    Для просмотра отчёта в Windows можно просто открыть .log файл в утилите Standalone, которая входит в дистрибутив для Windows.

    Примеры ошибок из Azure Service Fabric


    Классические опечатки



    V501 CWE-571 There are identical sub-expressions to the left and to the right of the '==' operator: iter->PackageName == iter->PackageName DigestedApplicationDescription.cpp 247

    ErrorCode
    DigestedApplicationDescription::ComputeAffectedServiceTypes(....)
    {
      ....
      if (iter->PackageName == iter->PackageName &&
        originalRG != this->ResourceGovernanceDescriptions.end() &&
        targetRG != targetDescription.ResourceGovernanceDes....end())
      {
        ....
      }
      ....
    }

    Переменная iter->PackageName должна сравниваться с iter2->PackageName или codePackages.

    V501 CWE-571 There are identical sub-expressions '(dataSizeInRecordIoBuffer > 0)' to the left and to the right of the '&&' operator. OverlayStream.cpp 4966

    VOID
    OverlayStream::AsyncMultiRecordReadContextOverlay::FSMContinue(
        __in NTSTATUS Status
        )
    {
      ULONG dataSizeInRecordMetadata = 0;
      ULONG dataSizeInRecordIoBuffer = 0;
      ....
      if ((dataSizeInRecordIoBuffer > 0) &&
          (dataSizeInRecordIoBuffer > 0))
      {
        ....
      }
      ....
    }

    Из-за Copy-Paste не проверяется размер буфера dataSizeInRecordMetadata.

    V534 CWE-691 It is likely that a wrong variable is being compared inside the 'for' operator. Consider reviewing 'ix0'. RvdLoggerVerifyTests.cpp 2395

    NTSTATUS
    ReportLogStateDifferences(....)
    {
      ....
      for (ULONG ix0=0; ix0 < RecoveredState._NumberOfStreams; ix0++)
      {
        KWString    streamId(....);
        ULONG       ix1;
    
        for (ix1 = 0; ix0 < LogState._NumberOfStreams; ix1++)
        {
          ...
        }
        ....
      }
      ....
    }

    Вероятно, в условии вложенного цикла должна проверяться переменная ix1, а не ix0.

    V570 The 'statusDetails_' variable is assigned to itself. ComposeDeploymentStatusQueryResult.cpp 49

    ComposeDeploymentStatusQueryResult &
    ComposeDeploymentStatusQueryResult::operator = (
      ComposeDeploymentStatusQueryResult && other)        // <=
    {
      if (this != & other)
      {
        deploymentName_ = move(other.deploymentName_);
        applicationName_ = move(other.applicationName_);
        dockerComposeDeploymentStatus_ = move(other....);
        statusDetails_ = move(statusDetails_);            // <=
      }
    
      return *this;
    }

    Скорее всего, значение поля statusDetails_ хотели взять из other.statusDetails_, но допустили опечатку.

    V606 Ownerless token 'false'. CryptoUtility.Linux.h 81

    template <typename TK, typename TV>
    static bool MapCompare(const std::map<TK, TV>& lhs,
                           const std::map<TK, TV>& rhs)
    {
      if (lhs.size() != rhs.size()) { false; }
    
      return std::equal(lhs.begin(), lhs.end(), rhs.begin());
    }

    Пропущенное ключевое слово return привело к тому, что код стал не оптимален. Из-за опечатки быстрая проверка на размер коллекций не работает так, как задумывал автор.

    V607 CWE-482 Ownerless expression. EnvironmentOverrideDescription.cpp 60

    bool EnvironmentOverridesDescription::operator == (....) const
    {
      bool equals = true;
      for (auto i = 0; i < EnvironmentVariables.size(); i++)
      {
        equals = EnvironmentVariables[i] ==
                 other.EnvironmentVariables[i];
        if (!equals) { return equals; }
      }
      this->CodePackageRef == other.CodePackageRef; // <=
      if (!equals) { return equals; }
      return equals;
    }

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

    equals = this->CodePackageRef == other.CodePackageRef;
    if (!equals) { return equals; }

    Неверное использование функций


    V521 CWE-480 Such expressions using the ',' operator are dangerous. Make sure the expression is correct. ReplicatedStore.SecondaryPump.cpp 1231

    ErrorCode
    ReplicatedStore::SecondaryPump::ApplyOperationsWithRetry(....)
    {
     ....
     if (errorMessage.empty())
     {
      errorMessage = L"error details missing: LSN={0}", operationLsn;
    
      Assert::TestAssert("{0}", errorMessage);
     }
     ....
    }

    Анализатор обнаружил странный код для формирования сообщения в переменной errorMessage. Судя по соседним фрагментам кода, здесь необходимо написать так:

    WriteInfo(errorMessage, L"error ....: LSN={0}", operationLsn);

    V547 CWE-570 Expression 'nwrite < 0' is always false. Unsigned type value is never < 0. File.cpp 1941

    static void* ScpWorkerThreadStart(void* param)
    {
      ....
      do
      {
        size_t nwrite = fwrite(ptr, 1, remaining, destfile);
        if (nwrite < 0)
        {
          pRequest->error_.Overwrite(ErrorCode::FromErrno(errno));
          break;
        }
        else
        {
          remaining -= nwrite;
          ptr += nwrite;
          pRequest->szCopied_ += nwrite;
        }
      } while (remaining != 0);
      ....
    }

    Неправильная проверка возвращаемого значения функции fwrite(). Документацию по этой функции можно найти на cppreference.com и cplusplus.com.

    V547 CWE-571 Expression 'len >= 0' is always true. Unsigned type value is always >= 0. Types.cpp 121

    size_t BIO_ctrl_pending(BIO *b);
    
    template <typename TBuf>
    TBuf BioMemToTBuf(BIO* bio)
    {
      char* data = NULL;
      auto len = BIO_ctrl_pending(bio);
      Invariant(len >= 0);
      ....
    }

    Неверная проверка возвращаемого значения функции из библиотеки OpenSSL. Это вполне может быть серьёзной ошибкой или даже уязвимостью.

    Про указатели и память


    V603 CWE-665 The object was created but it is not being used. If you wish to call constructor, 'this->JsonBufferManager2::JsonBufferManager2(....)' should be used. JsonReader.h 48

    class JsonBufferManager2
    {
        template<typename T>
        friend struct JsonBufferManagerTraits;
    public:
        JsonBufferManager2()
        {
            JsonBufferManager2(nullptr, 0);
        }
        ....
    }

    Вероятно, из одного конструктора хотели вызвать другой. Но на самом деле создаётся временный объект класса JsonBufferManager2 и тут же уничтожается. Подробнее этот тип ошибки описан в статье "Не зная брода, не лезь в воду: часть первая". В этой же статье рассказано, как можно вызвать один конструктор из другого.

    V568 It's odd that 'sizeof()' operator evaluates the size of a pointer to a class, but not the size of the 'thisPtr' class object. TimerQueue.cpp 443

    void TimerQueue::SigHandler(int sig, siginfo_t *si, void*)
    {
      TimerQueue* thisPtr = (TimerQueue*)si->si_value.sival_ptr;
    
      auto written = write(thisPtr->pipeFd_[1],
                           &thisPtr, sizeof(thisPtr));
    
      Invariant(written == sizeof(thisPtr));           // <=
    }

    В функцию write() передан правильный sizeof(), а вот результат функции чтения, скорее всего, должен сравниваться с размером записанного объекта:

    Invariant(written == sizeof(*thisPtr));

    V595 CWE-476 The 'globalDomain' pointer was utilized before it was verified against nullptr. Check lines: 196, 197. PlacementReplica.cpp 196

    void PlacementReplica::ForEachWeightedDefragMetric(....) const
    {
      ....
      size_t metricIndexInGlobalDomain =
        totalMetricIndexInGloba.... - globalDomain->MetricStartIndex;
      if (globalDomain != nullptr &&
        globalDomain->Metrics[metricIndexInGlobalDomain].Weight > 0)
      {
        if (!processor(totalMetricIndexInGlobalDomain))
        {
          break;
        }
      }
    }

    Классическая ошибка при работе с указателем globalDomain: сначала разыменование, потом проверка.

    V611 CWE-762 The memory was allocated using 'new T[]' operator but was released using the 'delete' operator. Consider inspecting this code. It's probably better to use 'delete [] groups;'. PAL.cpp 4733

    NET_API_STATUS NetUserGetLocalGroups(....)
    {
      string unameA = utf16to8(UserName).substr(0, ACCT_NAME_MAX);
      int ngroups = 50;
      gid_t *groups = new gid_t[ngroups];
      gid_t gid;
      ....
      delete groups;
      return NERR_Success;
    }

    Нашлось много мест, где неправильным способом освобождается память, выделенная под массив. Нужно использовать delete[].

    Запуск анализатора в контейнерах с Windows


    В этом случае запуск анализатора не сильно отличается от автоматизации анализа, например, в Jenkins на реальном компьютере. Мы сами используем Docker для тестирования PVS-Studio для Windows. Достаточно выполнить установку анализатора:

    START /w PVS-Studio_setup.exe /VERYSILENT /SUPPRESSMSGBOXES \
      /NORESTART /COMPONENTS=Core,Standalone

    и запустить анализ своего проекта:

    "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" ...

    Заключение


    Акцент статьи был сделан на интересной технологии контейнеризации, которая не является препятствием для интеграции статического анализа в свой проект. Поэтому найденные предупреждения PVS-Studio были сокращены в статье, но полностью доступны для скачивания в формате для браузера: service-fabric-pvs-studio-html.7z.

    Предлагаю всем желающим скачать и попробовать PVS-Studio на своём проекте. Анализатор работает на Windows, Linux и macOS!


    Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Svyatoslav Razmyslov. Features of PVS-Studio setting and running in Docker on the example of Azure Service Fabric code.

    Прочитали статью и есть вопрос?
    Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio, версия 2015. Пожалуйста, ознакомьтесь со списком.
    • +24
    • 2,3k
    • 9
    PVS-Studio
    799,33
    Static Code Analysis for C, C++, C# and Java
    Поделиться публикацией

    Комментарии 9

      +3
      Простите, что? «sudo plog-converter» ?!!!
        –3
        Есть проекты, требующие sudo с самого начала — с этапа сборки. Следовательно, на этапе анализа и обработки отчёта тоже могут потребоваться аналогичные права для запуска в том же окружении. Для того раздела статьи, скорее всего, это избыточная информация, поправлю. В остальном ничего необычного, требования зависят от конкретного проекта.
          +4
          суду на сборку?! серьёзно?! гбм…
            –2
            Серьёзно. Я даже скажу, что это не самое странное, что можно встретить в open source, позанимавшись статическим анализом пару лет.
              +2
              Всем срочно курить fakeroot до просветления :)))
                –1

                У нас есть продукт который на fedore core 6 и под sudo билдится) Happy upgrading)

        0

        Пара вопросов. Не задумывались ли вы о том, чтобы 'коллектор' над strace в исходниках отдовать, и аналайзер и плог официальным контейнером сделать?

          0
          чтобы 'коллектор' над strace в исходниках отдовать
          А зачем?
          официальным контейнером сделать
          Не получится сделать контейнер для анализа, потому что частью анализа является сборка проекта, а это бесконечность различных окружений. Поэтому интеграция анализатора в уже настроенный контейнер является единственным рабочим решением.
            0
            Ха — теперь понятно почему время сборки проекта существенно выростает.

            Честно говоря надеялся, что параметры из трейса аналайзер просто выгребает (по типу ключи и пр)

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

        Самое читаемое