Funzionalità dell'impostazione e dell'esecuzione di PVS-Studio in Docker nell'esempio del codice di Azure Service Fabric

Funzionalità dell'impostazione e dell'esecuzione di PVS-Studio in Docker nell'esempio del codice di Azure Service Fabric

Una tecnologia di containerizzazione viene utilizzata attivamente per costruire e testare il software. Con l'emergere di PVS-Studio per Linux, è diventata disponibile per gli utenti la possibilità di aggiungere l'analisi statica ad altri metodi per testare un progetto su questa piattaforma, incluso Docker. L'articolo descriverà le caratteristiche del lavoro con l'analizzatore PVS-Studio in Docker, che migliorerà la qualità dell'analisi e l'usabilità. L'articolo fornirà anche gli errori rilevati nel progetto Azure Service Fabric.

Introduzione

Docker è un programma che consente al sistema operativo di eseguire processi in un ambiente isolato sulla base di immagini appositamente create. La tecnologia della containerizzazione è diventata molto comune per molte attività, incluso lo sviluppo e il test di software. L'analisi statica viene in genere eseguita nello stesso ambiente della build del progetto, quindi il suo utilizzo è facilmente implementabile nei container già esistenti in Docker.

Gli esempi di integrazione e di esecuzione dell'analizzatore statico PVS-Studio verranno forniti per la versione Linux. Inoltre, le possibilità descritte di personalizzazione dell'analizzatore sono consigliate anche su altre piattaforme. La versione dell'analizzatore in macOS, che è stata recentemente presentata al pubblico, è generalmente identica nell'utilizzo di PVS-Studio per Linux.

Il progetto Azure Service Fabric è stato scelto come progetto per l'integrazione e il lancio dell'analizzatore in Docker. Service Fabric è una piattaforma di sistemi distribuiti per il confezionamento, la distribuzione e la gestione di applicazioni e contenitori stateless e stately distribuiti su larga scala. Service Fabric può essere eseguito su Windows e Linux, su qualsiasi cloud, qualsiasi data center, in tutte le aree geografiche o sul tuo laptop.

Implementazione graduale dell'analizzatore

Per iniziare, diamo un'occhiata al modo in cui viene eseguita la build per scegliere la modalità di integrazione dell'analizzatore. L'ordine degli script e delle chiamate dei comandi è simile al seguente:

Il seguente frammento dello script build.sh dove viene generato il file di progetto:

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

Per analizzare il progetto, ho deciso di utilizzare il metodo della documentazione descritto nella sezione Quick run/CMake-project:

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

L'aggiunta dell'installazione dell'analizzatore:

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
+

La directory src fa parte del progetto ed è montato in /src . Ho posizionato il file di configurazione dell'analizzatore PVS-Studio.cfg nello stesso posto. Quindi la chiamata dell'analizzatore può essere eseguita come segue:

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))

Ho eseguito l'analizzatore prima della creazione del progetto. Questa non è la decisione giusta, ma nello script ci sono molte condizioni in cui viene eseguita una build, quindi ho leggermente semplificato l'attività e compilato il progetto in anticipo. Gli sviluppatori che conoscono meglio la struttura del loro progetto dovrebbero integrare l'analizzatore dopo costruire il progetto.

Ora è possibile costruire e analizzare un progetto con il seguente comando:

sudo ./runbuild.sh -release -j4

I primi risultati dell'analisi non ci soddisfano a causa degli avvisi su numerose macro, file inesistenti, percorsi errati dei file di codice sorgente, ecc. Nella prossima sezione parlerò del contenuto del file PVS -Studio.cfg , dove ho aggiunto alcune impostazioni, che hanno notevolmente migliorato l'analisi.

Configurazione aggiuntiva dell'analizzatore

Il percorso relativo alla directory con i file di origine

Per visualizzare un singolo report su computer diversi, l'analizzatore può generare un report con percorsi relativi ai file. Puoi ripristinarli su un altro computer utilizzando il convertitore.

È necessario eseguire una configurazione dell'analizzatore simile per estrarre un report con i percorsi dei file corretti da un contenitore.

La directory principale è montata in root, quindi il parametro dell'analizzatore sarebbe il seguente:

sourcetree-root=/

La directory principale è selezionata qui perché in un container e in un host è la directory del progetto.

Avvisi per file inesistenti

Nel contenitore un /esterno catalogo si espande, che non esiste nel repository. Molto probabilmente, alcune dipendenze del progetto sono compilate al suo interno e possono essere semplicemente escluse dall'analisi:

exclude-path=/external

Avvisi per file del compilatore, test e librerie

In Docker, un compilatore può essere posizionato in una posizione non standard e le sue librerie potrebbero rientrare in un report. Devono essere rimossi anche loro. Per questo, la directory /deps e la directory con le prove sono escluse dal controllo:

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

Lotta contro migliaia di falsi positivi che emergono a causa di macro non riuscite

L'analizzatore supporta una configurazione di diverse diagnostiche tramite commenti. Puoi leggerli qui e qui.

Puoi inserire le impostazioni nel codice del progetto o creare un file separato, come ho fatto io:

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

Il contenuto del file 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

Alcune righe di markup speciale rimuovono dal rapporto migliaia di avvisi per le macro.

Altre impostazioni

Il percorso del file di licenza e l'abilitazione della sola diagnostica di uso generale (per velocizzare l'analisi):

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

L'intero file 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=/

Potrebbe diventare necessario in altri progetti

Un altro modo per testare un progetto richiede la presenza di un'utilità di sistema strace. Molto probabilmente, non verrà presentato nel contenitore e dovrai aggiungere il passaggio di installazione di questa utilità da un repository.

Il contenitore può includere un compilatore non standard, ad esempio un compilatore incrociato. Ho già scritto che è necessario escludere dall'analisi la directory del compilatore, ma in questo caso dovrai passare all'analizzatore anche il nome del nuovo compilatore:

pvs-studio-analyzer analyze ... --compiler COMPILER_NAME...

Puoi duplicare un flag per specificare più compilatori.

Visualizzazione di un report in Linux o Windows

Per visualizzare il rapporto dell'analizzatore in Linux, puoi aggiungere un comando in uno script per generare il rapporto nel formato necessario.

Ad esempio, per la visualizzazione in QtCreator:

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

Oppure nel browser:

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

Per visualizzare il rapporto in Windows, è sufficiente aprire il .log file nell'utilità Standalone, inclusa nel pacchetto di distribuzione per Windows.

Esempi di errori da Azure Service Fabric

Errori di battitura classici

V501 CWE-571 Esistono sottoespressioni identiche a sinistra ea destra dell'operatore '==':iter->NomePacchetto ==iter->NomePacchetto DigestedApplicationDescription.cpp 247

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

La variabile iter->PackageName dovrebbe essere confrontato con iter2->PackageName o codePackages .

V501 CWE-571 Sono presenti sottoespressioni identiche '(dataSizeInRecordIoBuffer> 0)' a sinistra ea destra dell'operatore '&&'. OverlayStream.cpp 4966

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

A causa del copia-incolla della dimensione del buffer dataSizeInRecordMetadata non è selezionato.

V534 CWE-691 È probabile che una variabile errata venga confrontata all'interno dell'operatore 'for'. Prendi in considerazione la revisione di '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++)
    {
      ...
    }
    ....
  }
  ....
}

Probabilmente, in una condizione di ciclo annidato la variabile ix1 deve essere selezionato invece di ix0 uno.

V570 La variabile 'statusDetails_' è assegnata a se stessa. 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;
}

Molto probabilmente, un programmatore ha voluto prendere il valore del campo statusDetails_ daother.statusDetails_ , ma ho commesso un errore di battitura.

V606 Token senza proprietario 'falso'. 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());
}

Una parola chiave mancante ritorno ha prodotto codice non ottimale. A causa di un errore di battitura, un rapido controllo delle dimensioni delle raccolte non funziona come previsto dall'autore.

V607 CWE-482 Espressione senza proprietario. 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;
}

Un errore di battitura è simile all'esempio precedente, ma porta a un errore più grave. Il risultato di uno dei confronti non viene mai salvato. Il codice corretto dovrebbe essere questo:

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

Uso errato delle funzioni

V521 CWE-480 Tali espressioni che utilizzano l'operatore ',' sono pericolose. Assicurati che l'espressione sia corretta. ReplicatedStore.SecondaryPump.cpp 1231

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

  Assert::TestAssert("{0}", errorMessage);
 }
 ....
}

L'analizzatore ha rilevato uno strano codice per la generazione di un messaggio nella variabile errorMessage . A giudicare dai frammenti vicini, qui si deve scrivere:

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

V547 CWE-570 L'espressione 'nwrite <0' è sempre falsa. Il valore del tipo senza segno non è mai <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);
  ....
}

Controllo errato del valore di ritorno della funzione fwrite() . La documentazione per questa funzione è reperibile su cppreference.com e cplusplus.com.

V547 CWE-571 L'espressione 'len>=0' è sempre vera. Il valore del tipo senza segno è sempre>=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);
  ....
}

Controllo errato del valore di ritorno di una funzione dalla libreria OpenSSL. Potrebbe trattarsi di un grave errore o addirittura di una vulnerabilità.

Informazioni sui puntatori e sulla memoria

V603 CWE-665 L'oggetto è stato creato ma non viene utilizzato. Se si desidera chiamare il costruttore, è necessario utilizzare 'this-> JsonBufferManager2::JsonBufferManager2(....)'. JsonReader.h 48

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

Probabilmente un programmatore voleva chiamare un costruttore da un altro. In realtà, un oggetto temporaneo di una classe JsonBufferManager2 viene creato e immediatamente distrutto. Questo tipo di errore è descritto in dettaglio nell'articolo "Non guadare in acque sconosciute. Parte prima". Questo articolo spiega anche come chiamare un costruttore da un altro.

V568 È strano che l'operatore 'sizeof()' valuti la dimensione di un puntatore a una classe, ma non la dimensione dell'oggetto classe 'thisPtr'. 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));           // <=
}

Il diritto sizeof() viene passato alla funzione write(), ma il risultato della funzione di lettura, molto probabilmente, deve essere confrontato con la dimensione dell'oggetto scritto:

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

V595 CWE-476 Il puntatore 'globalDomain' è stato utilizzato prima che fosse verificato rispetto a nullptr. Righe di controllo: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;
    }
  }
}

Un classico errore con il puntatore globalDomain :prima una dereferenza, poi un assegno.

V611 CWE-762 La memoria è stata allocata utilizzando l'operatore 'new T[]' ma è stata rilasciata utilizzando l'operatore 'cancella'. Prendi in considerazione la possibilità di ispezionare questo codice. Probabilmente è meglio usare '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;
}

Sono stati trovati molti punti in cui la memoria, allocata per un array, è stata rilasciata in modo errato. elimina[] doveva essere usato.

Esecuzione dell'analizzatore nei container con Windows

In questo caso, l'esecuzione dell'analizzatore non è molto diversa dall'automazione dell'analisi, ad esempio, in Jenkins su un computer reale. Noi stessi utilizziamo Docker per testare PVS-Studio per Windows. Puoi semplicemente eseguire l'installazione dell'analizzatore:

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

ed esegui l'analisi del tuo progetto:

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

Conclusione

Il focus dell'articolo è stato posto sull'entusiasmante tecnologia della containerizzazione, che non è un ostacolo all'integrazione dell'analisi statica nel tuo progetto. Pertanto, gli avvisi di PVS-Studio trovati sono stati ridotti nell'articolo, ma completamente disponibili per il download nel formato per il browser:service-fabric-pvs-studio-html .7z.

Suggerisco a chi è interessato di provare PVS-Studio sui vostri progetti. L'analizzatore funziona su Windows, Linux e macOS!