A “aventura” com o cliente de email Mozilla Thunderbird começou com uma atualização automática para a versão 68.0. Os recursos mais notáveis desta versão foram: mais texto é adicionado às notificações pop-up e um tema sombrio por padrão. Houve um erro que eu queria tentar detectar usando a análise estática. Foi uma ocasião para verificar novamente o código fonte do projeto usando o PVS-Studio. Aconteceu que, no momento da análise, o erro já havia sido corrigido. Mas, como chamamos a atenção para este projeto, podemos escrever sobre outros defeitos encontrados nele.
1. Introdução
O tema sombrio da nova versão do Thunderbird parece bastante bonito. Eu amo temas sombrios. Já mudou para eles em mensageiros instantâneos, Windows, macOS. Em breve, o iPhone será atualizado para o iOS 13, onde um tema sombrio apareceu. Para isso, tive que mudar meu iPhone 5S para um modelo mais novo. Na prática, o tema sombrio exige mais esforço dos desenvolvedores para escolher as cores da interface. Nem todo mundo lida com isso pela primeira vez. 
Então, minhas tags padrão no Thunderbird começaram a se parecer com:
Uso seis tags (5 padrão + 1 personalizado) para marcar e-mails. Tornou-se impossível assistir metade deles após a atualização, e decidi mudar a cor para mais brilhante nas configurações. Mas aqui encontrei um bug:
Você não pode mudar a cor da etiqueta !!! Mais precisamente, é possível, mas o editor não permitirá que ele seja salvo, referindo-se a um nome existente (WTF ???).
Outra manifestação do bug será a inação do botão OK, se você tentar alterar o nome, pois não poderá salvar esse nome. Você também não pode renomear.
Finalmente, você pode perceber que o tema sombrio não tocou nas configurações, o que também não é muito bonito.
Após uma longa luta com o sistema de compilação no Windows, ainda era possível montar o Thunderbird a partir da fonte. A versão mais recente do cliente de email acabou sendo muito melhor que a versão mais recente. Nele, um tema sombrio chegou às configurações, e esse bug com o editor de tags também desapareceu. Mas, para que o trabalho de montagem do projeto não fosse desperdiçado, o analisador de código estático 
PVS-Studio foi lançado.
Nota O código-fonte do Thunderbird se sobrepõe de alguma forma à base de código do Firefox. Portanto, a análise incluiu erros de diferentes componentes, que valem uma olhada de perto nos desenvolvedores de diferentes equipes.
Nota 2 Enquanto o artigo estava sendo escrito, a atualização do Thunderbird 68.1 foi lançada com uma correção para esse bug:
comm
comm-central é um repositório Mercurial do código de extensão Thunderbird, SeaMonkey e Lightning.
V501 Existem 
subexpressões idênticas '(! Strcmp (cabeçalho, "Responder para"))' à esquerda e à direita do '||' operador. nsEmitterUtils.cpp 28
extern "C" bool EmitThisHeaderForPrefSetting(int32_t dispType, const char *header) { .... if (nsMimeHeaderDisplayTypes::NormalHeaders == dispType) { if ((!strcmp(header, HEADER_DATE)) || (!strcmp(header, HEADER_TO)) || (!strcmp(header, HEADER_SUBJECT)) || (!strcmp(header, HEADER_SENDER)) || (!strcmp(header, HEADER_RESENT_TO)) || (!strcmp(header, HEADER_RESENT_SENDER)) || (!strcmp(header, HEADER_RESENT_FROM)) || (!strcmp(header, HEADER_RESENT_CC)) || (!strcmp(header, HEADER_REPLY_TO)) || (!strcmp(header, HEADER_REFERENCES)) || (!strcmp(header, HEADER_NEWSGROUPS)) || (!strcmp(header, HEADER_MESSAGE_ID)) || (!strcmp(header, HEADER_FROM)) || (!strcmp(header, HEADER_FOLLOWUP_TO)) || (!strcmp(header, HEADER_CC)) || (!strcmp(header, HEADER_ORGANIZATION)) || (!strcmp(header, HEADER_REPLY_TO)) || (!strcmp(header, HEADER_BCC))) return true; else return false; .... } 
A linha do 
cabeçalho foi comparada com a constante 
HEADER_REPLY_TO duas vezes. Talvez em seu lugar devesse ter havido outra constante.
V501 Existem 
subexpressões idênticas 'obj-> options-> headers! = MimeHeadersCitation' à esquerda e à direita do operador '&&'. mimemsig.cpp 536
 static int MimeMultipartSigned_emit_child(MimeObject *obj) { .... if (obj->options && obj->options->headers != MimeHeadersCitation && obj->options->write_html_p && obj->options->output_fn && obj->options->headers != MimeHeadersCitation && sig->crypto_closure) { .... } .... } 
Outra comparação estranha de uma variável com um nome semelhante são os 
cabeçalhos . Como sempre, existem duas explicações possíveis: um cheque extra ou um erro de digitação.
V517 O uso do 
padrão 'if (A) {...} else if (A) {...}' foi detectado. Há uma probabilidade de presença de erro lógico. Verifique as linhas: 1306, 1308. MapiApi.cpp 1306
 void CMapiApi::ReportLongProp(const char *pTag, LPSPropValue pVal) { if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_LONG)) { nsCString num; nsCString num2; num.AppendInt((int32_t)pVal->Value.l); num2.AppendInt((int32_t)pVal->Value.l, 16); MAPI_TRACE3("%s %s, 0x%s\n", pTag, num, num2); } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_NULL)) { MAPI_TRACE1("%s {NULL}\n", pTag); } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_ERROR)) {  
A cascata de expressões condicionais foi claramente acelerada pressionando Ctrl + C e Ctrl + V. Como resultado, uma das ramificações nunca é executada.
V517 O uso do 
padrão 'if (A) {...} else if (A) {...}' foi detectado. Há uma probabilidade de presença de erro lógico. Verifique as linhas: 777, 816. nsRDFContentSink.cpp 777
 nsresult RDFContentSinkImpl::GetIdAboutAttribute(const char16_t** aAttributes, nsIRDFResource** aResource, bool* aIsAnonymous) { .... if (localName == nsGkAtoms::about) { .... } else if (localName == nsGkAtoms::ID) { .... } else if (localName == nsGkAtoms::nodeID) { nodeID.Assign(aAttributes[1]); } else if (localName == nsGkAtoms::about) {  
A primeira e a última condição são as mesmas. O código mostra que o código está em processo de gravação. É seguro dizer que, com uma alta probabilidade, o erro se manifestará após a finalização do código. Um programador pode alterar o código comentado, mas ele nunca terá controle. Tenha cuidado e cuidado com este código.
V522 A desreferenciação do ponteiro nulo 'linha' pode ocorrer. morkRowCellCursor.cpp 175
 NS_IMETHODIMP morkRowCellCursor::MakeCell(  
A desreferenciação do ponteiro nulo da 
linha é possível na seguinte linha:
 morkCell* cell = row->CellAt(ev, pos); 
Provavelmente, a inicialização do ponteiro foi ignorada antes desta linha, por exemplo, usando o método 
GetRow , etc.
V543 É estranho que o valor '-1' seja designado à variável 'm_lastError' do tipo HRESULT. MapiApi.cpp 1050
 class CMapiApi { .... private: static HRESULT m_lastError; .... }; CMsgStore *CMapiApi::FindMessageStore(ULONG cbEid, LPENTRYID lpEid) { if (!m_lpSession) { MAPI_TRACE0("FindMessageStore called before session is open\n"); m_lastError = -1; return NULL; } .... } 
O tipo 
HRESULT é um tipo de dados complexo. Diferentes bits de uma variável desse tipo representam diferentes campos de descrição de erro. O código de erro deve ser definido usando constantes especiais dos arquivos de cabeçalho do sistema.
Mais alguns desses lugares:
- V543 É estranho que o valor '-1' seja designado à variável 'm_lastError' do tipo HRESULT. MapiApi.cpp 817
- V543 É estranho que o valor '-1' seja designado à variável 'm_lastError' do tipo HRESULT. MapiApi.cpp 1749
V579 A função memset recebe o ponteiro e seu tamanho como argumentos. É possivelmente um erro. Inspecione o terceiro argumento. icalmime.c 195
 icalcomponent* icalmime_parse(....) { struct sspm_part *parts; int i, last_level=0; icalcomponent *root=0, *parent=0, *comp=0, *last = 0; if ( (parts = (struct sspm_part *) malloc(NUM_PARTS*sizeof(struct sspm_part)))==0) { icalerror_set_errno(ICAL_NEWFAILED_ERROR); return 0; } memset(parts,0,sizeof(parts)); sspm_parse_mime(parts, NUM_PARTS,  icalmime_local_action_map,  get_string, data,  0 ); .... } 
A variável 
parts é um ponteiro para uma matriz de estruturas. Para redefinir os valores das estruturas, eles usaram a função 
memset , mas transferiram o tamanho do ponteiro para ele como o tamanho de um pedaço de memória.
Outros locais suspeitos:
- V579 A função memset recebe o ponteiro e seu tamanho como argumentos. É possivelmente um erro. Inspecione o terceiro argumento. icalmime.c 385
- V579 A função memset recebe o ponteiro e seu tamanho como argumentos. É possivelmente um erro. Inspecione o terceiro argumento. icalparameter.c 114
- V579 A função snprintf recebe o ponteiro e seu tamanho como argumentos. É possivelmente um erro. Inspecione o segundo argumento. icaltimezone.c 1908
- V579 A função snprintf recebe o ponteiro e seu tamanho como argumentos. É possivelmente um erro. Inspecione o segundo argumento. icaltimezone.c 1910
- V579 A função strncmp recebe o ponteiro e seu tamanho como argumentos. É possivelmente um erro. Inspecione o terceiro argumento. sspm.c 707
- V579 A função strncmp recebe o ponteiro e seu tamanho como argumentos. É possivelmente um erro. Inspecione o terceiro argumento. sspm.c 813
V595 O ponteiro 'aValues' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 553, 555. nsLDAPMessage.cpp 553
 NS_IMETHODIMP nsLDAPMessage::GetBinaryValues(const char *aAttr, uint32_t *aCount, nsILDAPBERValue ***aValues) { .... *aValues = static_cast<nsILDAPBERValue **>( moz_xmalloc(numVals * sizeof(nsILDAPBERValue))); if (!aValues) { ldap_value_free_len(values); return NS_ERROR_OUT_OF_MEMORY; } .... } 
O diagnóstico 
V595 geralmente encontra erros típicos de 
cancelamento de referência de ponteiro nulo. Mas foi encontrado um caso muito interessante, digno de atenção especial.
Tecnicamente, o analisador está certo de que o ponteiro 
aValues é primeiro desreferenciado e depois verificado, mas o erro é diferente. Como é um ponteiro duplo, o código correto deve ficar assim:
 *aValues = static_cast<nsILDAPBERValue **>( moz_xmalloc(numVals * sizeof(nsILDAPBERValue))); if (!*aValues) { ldap_value_free_len(values); return NS_ERROR_OUT_OF_MEMORY; } 
Outro lugar muito semelhante:
- V595 O ponteiro '_retval' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 357, 358. nsLDAPSyncQuery.cpp 357
V1044 As condições de interrupção do loop não dependem do número de iterações. mimemoz2.cpp 1795
 void ResetChannelCharset(MimeObject *obj) { .... if (cSet) { char *ptr2 = cSet; while ((*cSet) && (*cSet != ' ') && (*cSet != ';') && (*cSet != '\r') && (*cSet != '\n') && (*cSet != '"')) ptr2++; if (*cSet) { PR_FREEIF(obj->options->default_charset); obj->options->default_charset = strdup(cSet); obj->options->override_charset = true; } PR_FREEIF(cSet); } .... } 
Este erro foi encontrado usando um novo diagnóstico, que estará disponível na próxima versão do analisador. Todas as variáveis usadas na condição para interromper o 
loop while não são modificadas, porque as variáveis 
ptr2 e 
cSet são misturadas no corpo da função.
netwerk
O netwerk contém interfaces e código C para acesso de baixo nível à rede (usando soquetes e caches de arquivos e memória), bem como acesso de nível superior (usando vários protocolos como http, ftp, gopher, castanet). Esse código também é conhecido pelos nomes "netlib" e "Necko".
V501 Existem 
subexpressões idênticas 'connectStarted' à esquerda e à direita do operador '&&'. nsSocketTransport2.cpp 1693
 nsresult nsSocketTransport::InitiateSocket() { .... if (gSocketTransportService->IsTelemetryEnabledAndNotSleepPhase() && connectStarted && connectCalled) {  
No começo, pensei que duplicar a variável 
connectStarted é apenas um código supérfluo até examinar toda a função suficientemente longa e encontrar um fragmento semelhante. Provavelmente, em vez de uma única variável 
connectStarted aqui, também deve ser uma variável 
connectCalled .
V611 A memória foi alocada usando o operador 'new T []', mas foi liberada usando o operador 'delete'. Considere inspecionar este código. Provavelmente é melhor usar 'delete [] mData;'. Verifique as linhas: 233, 222. DataChannel.cpp 233
 BufferedOutgoingMsg::BufferedOutgoingMsg(OutgoingMsg& msg) { size_t length = msg.GetLeft(); auto* tmp = new uint8_t[length];  
O ponteiro 
mData aponta para uma matriz, não para um único objeto. Eles cometeram um erro no destruidor de classes, esquecendo de adicionar colchetes ao operador de 
exclusão .
V1044 As condições de interrupção do loop não dependem do número de iterações. ParseFTPList.cpp 691
 int ParseFTPList(....) { .... pos = toklen[2]; while (pos > (sizeof(result->fe_size) - 1)) pos = (sizeof(result->fe_size) - 1); memcpy(result->fe_size, tokens[2], pos); result->fe_size[pos] = '\0'; .... } 
O valor de 
pos é substituído no loop pela mesma quantidade. Parece que o novo diagnóstico encontrou outro erro.
gfx
O gfx contém interfaces C e código para desenho e imagem independentes da plataforma. Pode ser usado para desenhar retângulos, linhas, imagens, etc. Essencialmente, é um conjunto de interfaces para um contexto de dispositivo independente de plataforma (desenho). Ele não suporta widgets ou rotinas de desenho específicas; apenas fornece as operações primitivas para o desenho.
V501 Existem 
subexpressões idênticas à esquerda e à direita do '||' operador: mVRSystem || mVRCompositor || mVRSystem OpenVRSession.cpp 876
 void OpenVRSession::Shutdown() { StopHapticTimer(); StopHapticThread(); if (mVRSystem || mVRCompositor || mVRSystem) { ::vr::VR_Shutdown(); mVRCompositor = nullptr; mVRChaperone = nullptr; mVRSystem = nullptr; } } 
Na condição, a variável 
mVRSystem está presente duas vezes. Obviamente, um deles deve ser substituído por 
mVRChaperone .
dom
dom contém interfaces e código C para implementar e rastrear objetos DOM (Document Object Model) em Javascript. Ele forma a subestrutura C que cria, destrói e manipula objetos internos e definidos pelo usuário de acordo com o script Javascript.
V570 A variável 'clonedDoc-> mPreloadReferrerInfo' é atribuída a si mesma. Document.cpp 12049
 already_AddRefed<Document> Document::CreateStaticClone( nsIDocShell* aCloneContainer) { .... clonedDoc->mReferrerInfo = static_cast<dom::ReferrerInfo*>(mReferrerInfo.get())->Clone(); clonedDoc->mPreloadReferrerInfo = clonedDoc->mPreloadReferrerInfo; .... } 
O analisador detectou a atribuição de uma variável a si mesma.
xpcom
O xpcom contém as interfaces C de baixo nível, código C, código C, um pouco de código de montagem e ferramentas de linha de comando para implementar o mecanismo básico dos componentes XPCOM (que significa "Modelo de objeto de componente de plataforma cruzada"). XPCOM é o mecanismo que permite ao Mozilla exportar interfaces e disponibilizá-las automaticamente para scripts JavaScript, Microsoft COM e código Mozilla C regular.
V611 A memória foi alocada usando a função 'malloc / realloc', mas foi liberada usando o operador 'delete'. Considere inspecionar as lógicas de operação por trás da variável 'key'. Verifique as linhas: 143, 140. nsINIParser.h 143
 struct INIValue { INIValue(const char* aKey, const char* aValue) : key(strdup(aKey)), value(strdup(aValue)) {} ~INIValue() { delete key; delete value; } void SetValue(const char* aValue) { delete value; value = strdup(aValue); } const char* key; const char* value; mozilla::UniquePtr<INIValue> next; }; 
Depois de chamar a função 
strdup , 
é necessário liberar memória usando a função 
free , não o operador 
delete .
V716 Conversão de tipo suspeita na inicialização: 'HRESULT var = BOOL'. SpecialSystemDirectory.cpp 73
 BOOL SHGetSpecialFolderPathW( HWND hwnd, LPWSTR pszPath, int csidl, BOOL fCreate ); static nsresult GetWindowsFolder(int aFolder, nsIFile** aFile) { WCHAR path_orig[MAX_PATH + 3]; WCHAR* path = path_orig + 1; HRESULT result = SHGetSpecialFolderPathW(nullptr, path, aFolder, true); if (!SUCCEEDED(result)) { return NS_ERROR_FAILURE; } .... } 
A função 
WinGI SHGetSpecialFolderPathW retorna um valor do tipo 
BOOL , não 
HRESULT . A verificação do resultado da função deve ser reescrita para a correta.
nsprpub
O nsprpub contém código C para a biblioteca de tempo de execução "C" de plataforma cruzada. A biblioteca de tempo de execução “C” contém funções C não visuais básicas para alocar e desalocar memória, obter hora e data, ler e gravar arquivos, manipular threads e manipular e comparar strings em todas as plataformas
V647 O valor do tipo 'int' é atribuído ao ponteiro do tipo 'curto'. Considere inspecionar a atribuição: 'out_flags = 0x2'. prsocket.c 1220
 #define PR_POLL_WRITE 0x2 static PRInt16 PR_CALLBACK SocketPoll( PRFileDesc *fd, PRInt16 in_flags, PRInt16 *out_flags) { *out_flags = 0; #if defined(_WIN64) if (in_flags & PR_POLL_WRITE) { if (fd->secret->alreadyConnected) { out_flags = PR_POLL_WRITE; return PR_POLL_WRITE; } } #endif return in_flags; }  
O analisador detectou a atribuição de uma constante numérica ao ponteiro 
out_flags . Provavelmente, eles simplesmente esqueceram de desreferê-lo:
 if (fd->secret->alreadyConnected) { *out_flags = PR_POLL_WRITE; return PR_POLL_WRITE; } 
Conclusão
Este não é o fim. Ser novas revisões de código. O código Thunderbird e Firefox inclui duas grandes bibliotecas: Network Security Services (NSS) e WebRTC (Web Real Time Communications). Houve alguns erros muito interessantes. Nesta revisão, mostrarei uma de cada vez.
NssV597 O compilador pode excluir a chamada de função 'memset', usada para liberar o buffer 'newdeskey'. A função RtlSecureZeroMemory () deve ser usada para apagar os dados particulares. pkcs11c.c 1033
 static CK_RV sftk_CryptInit(....) { .... unsigned char newdeskey[24]; .... context->cipherInfo = DES_CreateContext( useNewKey ? newdeskey : (unsigned char *)att->attrib.pValue, (unsigned char *)pMechanism->pParameter, t, isEncrypt); if (useNewKey) memset(newdeskey, 0, sizeof newdeskey); sftk_FreeAttribute(att); .... } 
O NSS é uma biblioteca para o desenvolvimento de aplicativos seguros para clientes e servidores, e aqui a DES Key não é limpa. O compilador removerá a chamada 
memset do código, conforme a matriz 
newdeskey não 
é mais usada no código além desse local.
WebRTCV519 A 
variável 'state [state_length - x_length + i]' recebe valores duas vezes sucessivos. Talvez isso seja um erro. Verifique as linhas: 83, 84. filter_ar.c 84
 size_t WebRtcSpl_FilterAR(....) { .... for (i = 0; i < state_length - x_length; i++) { state[i] = state[i + x_length]; state_low[i] = state_low[i + x_length]; } for (i = 0; i < x_length; i++) { state[state_length - x_length + i] = filtered[i]; state[state_length - x_length + i] = filtered_low[i];  
No segundo ciclo, os dados são gravados na matriz errada, porque o autor copiou o código e esqueceu de alterar o nome da matriz de 
state para 
state_low .
Provavelmente existem bugs mais interessantes nesses projetos que vale a pena mencionar. E faremos isso em um futuro próximo. Enquanto isso, tente o 
PVS-Studio em seu projeto.

Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Svyatoslav Razmyslov. 
Tema sombrio do Thunderbird como um motivo para executar um analisador de código .