Nos últimos dez anos, o movimento de código aberto foi um dos fatores principais no desenvolvimento da indústria de TI e uma parte importante dela. O papel e o local do código aberto não são apenas aprimorados pelo crescimento de indicadores quantitativos, mas também há uma mudança em seu posicionamento qualitativo no mercado de TI como um todo. Sem ficar parado, a brava equipe do PVS-Studio contribui ativamente para consolidar a posição dos projetos de código aberto, encontrando bugs ocultos em grandes espessuras de bases de código e oferecendo licenças gratuitas para esses projetos. Este artigo não é exceção! Hoje vamos falar sobre o Apache Hive! Relatório recebido - há algo para ver!
Sobre o PVS-Studio
O analisador de código estático do
PVS-Studio existe há mais de 10 anos no mercado de TI e é uma solução de software multifuncional e de fácil implementação. No momento, o analisador suporta linguagens C, C ++, C #, Java e funciona nas plataformas Windows, Linux e macOS.
O PVS-Studio é uma solução B2B paga e é usada por um grande número de equipes em várias empresas. Se você quiser ver do que o analisador é capaz, faça o download do kit de distribuição e solicite uma chave de avaliação
aqui .
Se você é um nerd de código aberto ou, por exemplo, é um estudante, pode usar uma das
opções de licenciamento gratuitas
do PVS-Studio.
Sobre o Apache Hive
O volume de dados nos últimos anos está crescendo em alta velocidade. Os bancos de dados padrão não podem mais manter a operabilidade com uma taxa de crescimento tão grande quanto a quantidade de informações que serviram como o surgimento do termo Big Data e tudo relacionado a ele (processamento, armazenamento e todas as ações subsequentes com esses volumes de dados).
Atualmente, o
Apache Hadoop é considerado uma das tecnologias fundamentais do Big Data. Os principais objetivos desta tecnologia são o armazenamento, processamento e gerenciamento de grandes volumes de dados. Os principais componentes da estrutura são o Hadoop Common,
HDFS ,
Hadoop MapReduce ,
Hadoop YARN . Com o tempo, um ecossistema inteiro de projetos e tecnologias relacionados se formou em torno do Hadoop, muitos dos quais se desenvolveram inicialmente como parte do projeto e, posteriormente, se tornaram independentes. Um desses projetos é o
Apache Hive .
O Apache Hive é um armazém de dados distribuído. Ele gerencia os dados armazenados no HDFS e fornece uma linguagem de consulta baseada em SQL (HiveQL) para trabalhar com esses dados. Para um conhecimento detalhado deste projeto, você pode estudar as informações
aqui .
Sobre a análise
A sequência de etapas para a análise é bastante simples e não requer muito tempo:
- Obtenha o Apache Hive com o GitHub ;
- Eu usei as instruções para iniciar o analisador Java e iniciei a análise;
- Recebi um relatório do analisador, analisei e destaquei casos interessantes.
Resultados da análise: 1456 avisos do nível de confiança Alto e Médio (602 e 854, respectivamente) foram emitidos para mais de 6500 arquivos.
Nem todos os avisos são erros. Essa é uma situação normal e, antes do uso regular do analisador, é necessária sua configuração. Então podemos esperar uma porcentagem bastante baixa de falsos positivos (
exemplo ).
Entre os avisos, 407 avisos (177 Alto e 230 Médio) por arquivos de teste não foram considerados. A regra de diagnóstico
V6022 não
foi considerada (é difícil separar situações incorretas das corretas em um código desconhecido), que tinha até 482 avisos.
O V6021 com 179 avisos também não foi considerado.
No final, mesmo assim, permaneceu um número suficiente de avisos. E como não configurei o analisador, entre eles, novamente, há falsos positivos. Não faz sentido descrever um grande número de avisos em um artigo :). Considere o que chamou minha atenção e pareceu interessante.
Condições predeterminadas
A regra de diagnóstico
V6007 é a detentora de registros entre todos os avisos restantes do analisador. Pouco mais de 200 avisos !!! Alguns, como, inofensivos, alguns são suspeitos, enquanto outros são erros completamente reais! Vamos dar uma olhada em alguns deles.
A expressão
V6007 'key.startsWith ("hplsql.")' Sempre é verdadeira. Exec.java (675)
void initOptions() { .... if (key == null || value == null || !key.startsWith("hplsql.")) {
Uma construção if-else-if bastante longa! O analisador jura no final
if (key.startsWith ("hplsql.")) , Indicando sua verdade se o programa atingir esse fragmento de código. De fato, se você observar o início da construção if-else-if, a verificação já foi concluída. E caso nossa linha não tenha começado com a substring
"hplsql". , a execução do código passou imediatamente para a próxima iteração.
A expressão
V6007 'columnNameProperty.length () == 0' é sempre falsa. OrcRecordUpdater.java (238)
private static TypeDescription getTypeDescriptionFromTableProperties(....) { .... if (tableProperties != null) { final String columnNameProperty = ....; final String columnTypeProperty = ....; if ( !Strings.isNullOrEmpty(columnNameProperty) && !Strings.isNullOrEmpty(columnTypeProperty)) { List<String> columnNames = columnNameProperty.length() == 0 ? new ArrayList<String>() : ....; List<TypeInfo> columnTypes = columnTypeProperty.length() == 0 ? new ArrayList<TypeInfo>() : ....; .... } } } .... }
Comparar comprimentos de string de
columnNameProperty com zero sempre retornará
false . Isso ocorre porque nossa comparação está sob teste
! Strings.isNullOrEmpty (columnNameProperty) . Se o estado do programa atingir nossa condição em questão, é garantido que a linha
columnNameProperty seja diferente de zero e vazia.
Isso também se aplica à linha
columnTypeProperty . Linha de aviso abaixo:
- A expressão V6007 'columnTypeProperty.length () == 0' é sempre falsa. OrcRecordUpdater.java (239)
A expressão
V6007 'colOrScalar1.equals ("Column")' é sempre falsa. GenVectorCode.java (3469)
private void generateDateTimeArithmeticIntervalYearMonth(String[] tdesc) throws Exception { .... String colOrScalar1 = tdesc[4]; .... String colOrScalar2 = tdesc[6]; .... if (colOrScalar1.equals("Col") && colOrScalar1.equals("Column"))
}
Aqui está uma cópia-pasta trivial. Descobriu-se que a linha
colOrScalar1 deve ser igual a valores diferentes ao mesmo tempo, e isso é impossível. Aparentemente, a variável
colOrScalar1 deve ser verificada à esquerda e
colOrScalar2 à direita.
Mais avisos semelhantes nas linhas abaixo:
- A expressão V6007 'colOrScalar1.equals ("Scalar") "é sempre falsa. GenVectorCode.java (3475)
- A expressão V6007 'colOrScalar1.equals ("Column")' é sempre falsa. GenVectorCode.java (3486)
Como resultado, nenhuma ação na construção if-else-if será executada.
Alguns outros avisos para o
V6007 :
- V6007 A expressão 'caracteres == nulo' é sempre falsa. RandomTypeUtil.java (43)
- A expressão V6007 'writeIdHwm> 0' é sempre falsa. TxnHandler.java (1603)
- A expressão V6007 'fields.equals ("*")' sempre é verdadeira. Server.java (983)
- A expressão V6007 'currentGroups! = Null' sempre é verdadeira. GenericUDFCurrentGroups.java (90)
- A expressão V6007 'this.wh == null' é sempre falsa. Novo retorna referência não nula. StorageBasedAuthorizationProvider.java (93), StorageBasedAuthorizationProvider.java (92)
- e assim por diante ...
NPE
V6008 Dereferência nula potencial de 'dagLock'. QueryTracker.java (557), QueryTracker.java (553)
private void handleFragmentCompleteExternalQuery(QueryInfo queryInfo) { if (queryInfo.isExternalQuery()) { ReadWriteLock dagLock = getDagLock(queryInfo.getQueryIdentifier()); if (dagLock == null) { LOG.warn("Ignoring fragment completion for unknown query: {}", queryInfo.getQueryIdentifier()); } boolean locked = dagLock.writeLock().tryLock(); ..... } }
Pegou o objeto zero, prometeu e ... continuou trabalhando. Isso leva ao fato de que, após a verificação do objeto, ocorre a desreferenciação do objeto zero. Tristeza!
Provavelmente, no caso de uma referência nula, você deve sair imediatamente da função ou lançar alguma exceção especial.
V6008 Dereferência nula de 'buffer' na função 'unlockSingleBuffer'. MetadataCache.java (410), MetadataCache.java (465)
private boolean lockBuffer(LlapBufferOrBuffers buffers, ....) { LlapAllocatorBuffer buffer = buffers.getSingleLlapBuffer(); if (buffer != null) {
E novamente um potencial NPE. Se o programa atingir o método
unlockSingleBuffer , o objeto de
buffer será zero. Vamos dizer que aconteceu! Vejamos o método
unlockSingleBuffer e imediatamente na primeira linha, vemos que nosso objeto é desreferenciado. Aqui estamos nós!
Não seguiu a mudança
V6034 A alteração no valor de 'bitShiftsInWord - 1' pode ser inconsistente com o tamanho do tipo: 'bitShiftsInWord - 1' = [-1 ... 30]. UnsignedInt128.java (1791)
private void shiftRightDestructive(int wordShifts, int bitShiftsInWord, boolean roundUp) { if (wordShifts == 0 && bitShiftsInWord == 0) { return; } assert (wordShifts >= 0); assert (bitShiftsInWord >= 0); assert (bitShiftsInWord < 32); if (wordShifts >= 4) { zeroClear(); return; } final int shiftRestore = 32 - bitShiftsInWord;
Possível deslocamento por -1. Se, por exemplo,
wordShifts == 3 e
bitShiftsInWord == 0 chegarem à entrada do método em questão, 1 << -1 ocorrerá na linha especificada. Isso está planejado?
V6034 A mudança no valor de 'j' pode ser inconsistente com o tamanho do tipo: 'j' = [0 ... 63]. IoTrace.java (272)
public void logSargResult(int stripeIx, boolean[] rgsToRead) { .... for (int i = 0, valOffset = 0; i < elements; ++i, valOffset += 64) { long val = 0; for (int j = 0; j < 64; ++j) { int ix = valOffset + j; if (rgsToRead.length == ix) break; if (!rgsToRead[ix]) continue; val = val | (1 << j);
Na linha especificada, a variável
j pode assumir um valor no intervalo [0 ... 63]. Por esse motivo, o cálculo do valor de
val no loop pode não ocorrer como o desenvolvedor pretendia. Na expressão
(1 << j), a unidade é do tipo
int e, deslocando-a de 32 ou mais, vamos além dos limites do permitido. Para remediar a situação, você deve escrever
((longo) 1 << j) .
Entusiasmado com o log
V6046 Formato incorreto. É esperado um número diferente de itens de formato. Argumentos não utilizados: 1, 2. StatsSources.java (89)
private static ImmutableList<PersistedRuntimeStats> extractStatsFromPlanMapper (....) { .... if (stat.size() > 1 || sig.size() > 1) { StringBuffer sb = new StringBuffer(); sb.append(String.format( "expected(stat-sig) 1-1, got {}-{} ;",
Ao formatar uma string por meio de
String.format (), o desenvolvedor confundiu a sintaxe. Conclusão: os parâmetros transmitidos não foram inseridos na sequência resultante. Eu posso assumir que na tarefa anterior o desenvolvedor trabalhou no log, de onde ele emprestou a sintaxe.
Roubou a exceção
V6051 O uso da instrução 'return' no bloco 'final' pode levar à perda de exceções não tratadas. ObjectStore.java (9080)
private List<MPartitionColumnStatistics> getMPartitionColumnStatistics(....) throws NoSuchObjectException, MetaException { boolean committed = false; try { .... committed = commitTransaction(); return result; } catch (Exception ex) { LOG.error("Error retrieving statistics via jdo", ex); if (ex instanceof MetaException) { throw (MetaException) ex; } throw new MetaException(ex.getMessage()); } finally { if (!committed) { rollbackTransaction(); return Lists.newArrayList(); } } }
Retornar algo do bloco final é uma prática muito ruim e, com este exemplo, veremos isso.
No bloco
try , a solicitação é formada e o armazenamento é acessado. A variável
confirmada assume como padrão
false e altera seu estado somente após todas as ações concluídas com êxito no bloco
try . Isso significa que, se ocorrer uma exceção, nossa variável será sempre
falsa .
O bloco de
captura capturou uma exceção, ligeiramente corrigido e jogou mais. E quando chega a hora do bloco
final, a execução entra em uma condição a partir da qual retornamos a lista vazia. Quanto custa esse retorno? Mas vale a pena o fato de que todas as exceções detectadas nunca serão lançadas e processadas de maneira apropriada. Todas as exceções indicadas na assinatura do método nunca serão descartadas e simplesmente desconcertantes.
Um aviso semelhante:
- V6051 O uso da instrução 'return' no bloco 'final' pode levar à perda de exceções não tratadas. ObjectStore.java (808)
... outro
V6009 A função 'compareTo' recebe um argumento estranho. Um objeto 'o2.getWorkerIdentity ()' é usado como argumento para seu próprio método. LlapFixedRegistryImpl.java (244)
@Override public List<LlapServiceInstance> getAllInstancesOrdered(....) { .... Collections.sort(list, new Comparator<LlapServiceInstance>() { @Override public int compare(LlapServiceInstance o1, LlapServiceInstance o2) { return o2.getWorkerIdentity().compareTo(o2.getWorkerIdentity());
Copie e cole, descuido, pressa e muitas outras razões para cometer esse erro estúpido. Ao verificar projetos de código aberto, erros desse tipo são bastante comuns. Existe até um
artigo inteiro sobre isso.
V6020 Divida por zero. O intervalo dos valores do denominador 'divisor' inclui zero. SqlMathUtil.java (265)
public static long divideUnsignedLong(long dividend, long divisor) { if (divisor < 0L) { return (compareUnsignedLong(dividend, divisor)) < 0 ? 0L : 1L; } if (dividend >= 0) {
Tudo é bem simples aqui. Um número de verificações não alertou contra a divisão por 0.
Mais avisos:
- V6020 Mod por zero. O intervalo dos valores do denominador 'divisor' inclui zero. SqlMathUtil.java (309)
- V6020 Divida por zero. O intervalo dos valores do denominador 'divisor' inclui zero. SqlMathUtil.java (276)
- V6020 Divida por zero. O intervalo dos valores do denominador 'divisor' inclui zero. SqlMathUtil.java (312)
V6030 O método localizado à direita do '|' O operador será chamado independentemente do valor do operando esquerdo. Talvez seja melhor usar '||'. OperatorUtils.java (573)
public static Operator<? extends OperatorDesc> findSourceRS(....) { .... List<Operator<? extends OperatorDesc>> parents = ....; if (parents == null | parents.isEmpty()) {
Em vez do operador lógico || escreveu um operador bit a bit |. Isso significa que o lado direito será executado independentemente do resultado do lado esquerdo. Esse erro de digitação, no caso de
pais == null , levará imediatamente a um NPE na próxima subexpressão lógica.
V6042 A expressão é verificada quanto à compatibilidade com o tipo 'A', mas é convertida para o tipo 'B'. VectorColumnAssignFactory.java (347)
public static VectorColumnAssign buildObjectAssign(VectorizedRowBatch outputBatch, int outColIndex, PrimitiveCategory category) throws HiveException { VectorColumnAssign outVCA = null; ColumnVector destCol = outputBatch.cols[outColIndex]; if (destCol == null) { .... } else if (destCol instanceof LongColumnVector) { switch(category) { .... case LONG: outVCA = new VectorLongColumnAssign() { .... } .init(.... , (LongColumnVector) destCol); break; case TIMESTAMP: outVCA = new VectorTimestampColumnAssign() { .... }.init(...., (TimestampColumnVector) destCol);
As classes em
questão são LongColumnVector estende ColumnVector e
TimestampColumnVector estende ColumnVector . A verificação de nosso objeto
destCol quanto
à propriedade
LongColumnVector nos diz claramente que o objeto dessa classe estará dentro da instrução condicional. Apesar disso, estamos lançando para
TimestampColumnVector ! Como você pode ver, essas classes são diferentes, sem contar os pais comuns. Como resultado -
ClassCastException .
Pode-se dizer o mesmo sobre a conversão de tipos para
IntervalDayTimeColumnVector :
- V6042 A expressão é verificada quanto à compatibilidade com o tipo 'A', mas é convertida para o tipo 'B'. VectorColumnAssignFactory.java (390)
V6060 A referência 'var' foi utilizada antes de ser verificada com relação a nulo. Var.java (402), Var.java (395)
@Override public boolean equals(Object obj) { if (getClass() != obj.getClass()) {
Uma comparação estranha de um objeto
var com
nulo após a desreferenciação ter ocorrido. Nesse contexto,
var e
obj são o mesmo objeto (
var = (Var) obj ). A verificação de
nulo implica que um objeto nulo possa vir. E no caso de
igual a (nulo), obtemos imediatamente o NPE de primeira linha, em vez do
falso esperado. Infelizmente, há um cheque, mas não existe.
Momentos suspeitos semelhantes ao usar o objeto antes da verificação:
- V6060 A referência 'value' foi utilizada antes de ser verificada em relação a null. ParquetRecordReaderWrapper.java (168), ParquetRecordReaderWrapper.java (166)
- V6060 A referência 'defaultConstraintCols' foi utilizada antes de ser verificada com relação a nulo. HiveMetaStore.java (2539), HiveMetaStore.java (2530)
- V6060 A referência 'projIndxLst' foi utilizada antes de ser verificada como nula. RelOptHiveTable.java (683), RelOptHiveTable.java (682)
- V6060 A referência 'oldp' foi utilizada antes de ser verificada com relação a nulo. ObjectStore.java (4343), ObjectStore.java (4339)
- e assim por diante ...
Conclusão
Qualquer um que estivesse um pouco interessado em Big Data, quase não percebia o significado do Apache Hive. O projeto é popular e bastante amplo, e em sua composição possui mais de 6500 arquivos de código-fonte (* .java). O código foi escrito por muitos desenvolvedores por muitos anos e, como resultado, o analisador estático tem algo a encontrar. Isso confirma mais uma vez que a análise estática é extremamente importante e útil no desenvolvimento de projetos de médio e grande porte!
Nota Essas verificações únicas demonstram os recursos de um analisador de código estático, mas são uma maneira completamente errada de usá-lo. Essa idéia é apresentada em mais detalhes
aqui e
aqui . Use a análise regularmente!
Ao verificar a Hive, um número suficiente de defeitos e momentos suspeitos foram detectados. Se este artigo chamar a atenção da equipe de desenvolvimento do Apache Hive, teremos o prazer de contribuir para essa tarefa difícil.
É impossível imaginar o Apache Hive sem o Apache Hadoop, por isso é provável que o unicórnio do PVS-Studio também procure lá. Mas isso é tudo por hoje, mas por enquanto
baixe o analisador e verifique seus próprios projetos.

Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Maxim Stefanov.
O PVS-Studio visita o Apache Hive .