PVS-Studio Mengunjungi Apache Hive

Gambar 1

Selama sepuluh tahun terakhir, gerakan open-source telah menjadi salah satu pendorong utama pengembangan industri TI, dan komponen krusialnya. Peran proyek-proyek sumber terbuka menjadi semakin menonjol tidak hanya dalam hal kuantitas tetapi juga dalam hal kualitas, yang mengubah konsep bagaimana mereka diposisikan di pasar TI pada umumnya. Tim PVS-Studio kami yang berani tidak duduk diam dan mengambil peran aktif dalam memperkuat keberadaan perangkat lunak sumber terbuka dengan menemukan bug tersembunyi di kedalaman basis kode yang sangat besar dan menawarkan opsi lisensi gratis kepada penulis proyek semacam itu. Artikel ini hanyalah bagian lain dari aktivitas itu! Hari ini kita akan berbicara tentang Apache Hive. Saya sudah mendapat laporannya - dan ada beberapa hal yang layak untuk dilihat.

Tentang PVS-Studio


Penganalisa kode statis PVS-Studio , yang telah ada selama lebih dari 10 tahun sekarang, adalah solusi perangkat lunak multi-fungsional dan mudah diintegrasikan. Saat ini, ia mendukung C, C ++, C #, dan Java dan berjalan pada Windows, Linux, dan macOS.

PVS-Studio adalah solusi B2B berbayar yang digunakan oleh banyak tim di sejumlah perusahaan. Jika Anda ingin mencoba penganalisa, kunjungi halaman ini untuk mengunduh distribusi dan meminta kunci percobaan.

Jika Anda seorang geek sumber terbuka atau, misalnya, seorang siswa, Anda dapat memanfaatkan salah satu opsi lisensi gratis kami.

Tentang Apache Hive


Jumlah data telah tumbuh pada tingkat yang sangat besar selama beberapa tahun terakhir. Basis data standar tidak dapat lagi menghadapi pertumbuhan yang cepat ini, yang merupakan asal istilah Big Data berasal bersama dengan gagasan terkait lainnya (seperti pemrosesan, penyimpanan, dan operasi lainnya pada data besar).

Apache Hadoop saat ini dianggap sebagai salah satu teknologi Big Data perintis. Tugas utamanya adalah menyimpan, memproses, dan mengelola sejumlah besar data. Komponen utama yang terdiri dari kerangka kerja adalah Hadoop Common, HDFS , Hadoop MapReduce , dan Hadoop YARN . Seiring waktu, ekosistem besar proyek dan teknologi terkait telah berkembang di sekitar Hadoop, banyak di antaranya awalnya dimulai sebagai bagian dari proyek dan kemudian beranjak untuk menjadi mandiri. Apache Hive adalah salah satunya.

Apache Hive adalah gudang data terdistribusi. Ini mengelola data yang disimpan dalam HDFS dan menyediakan bahasa query berdasarkan SQL (HiveQL) untuk menangani data itu. Detail lebih lanjut tentang proyek ini dapat ditemukan di sini .

Menjalankan analisis


Tidak butuh banyak usaha atau waktu untuk memulai analisis. Inilah algoritma saya:

  • Unduh Apache Hive dari GitHub ;
  • Baca panduan tentang memulai analisis Java dan meluncurkan analisis;
  • Dapatkan laporan penganalisa, mempelajarinya, dan menulis kasus yang paling menarik.

Hasil analisis adalah sebagai berikut: 1456 peringatan tingkat Tinggi dan Sedang (masing-masing 602 dan 854) pada 6500+ file.

Tidak semua peringatan merujuk pada bug asli. Itu cukup normal; Anda harus mengubah pengaturan penganalisa sebelum mulai menggunakannya secara teratur. Setelah itu, Anda biasanya mengharapkan tingkat positif palsu yang cukup rendah ( contoh ).

Saya mengabaikan 407 peringatan (177 Tinggi dan 230 Tingkat Menengah) yang dipicu oleh file uji. Saya juga mengabaikan diagnostik V6022 (karena Anda tidak dapat dengan andal membedakan fragmen yang salah dan benar ketika Anda tidak terbiasa dengan kode), yang dipicu sebanyak 482 kali. Saya juga tidak memeriksa 179 peringatan yang dihasilkan oleh diagnostik V6021 .

Pada akhirnya, saya masih memiliki cukup peringatan untuk diikuti, dan karena saya tidak mengubah pengaturan, masih ada beberapa persentase positif palsu di antara mereka. Tidak ada gunanya memasukkan terlalu banyak peringatan dalam artikel seperti ini :). Jadi kita hanya akan berbicara tentang apa yang menarik perhatian saya dan terlihat cukup penasaran.

Kondisi yang ditentukan sebelumnya


Di antara diagnostik yang diperiksa untuk analisis ini, V6007 memegang rekor untuk jumlah peringatan yang dikeluarkan. Sedikit lebih dari 200 pesan !!! Beberapa terlihat tidak berbahaya, yang lain mencurigakan, dan beberapa yang lain adalah bug asli! Mari kita lihat beberapa di antaranya.

V6007 Expression 'key.startsWith ("hplsql.")' Selalu benar. Exec.java (675)

void initOptions() { .... if (key == null || value == null || !key.startsWith("hplsql.")) { // <= continue; } else if (key.compareToIgnoreCase(Conf.CONN_DEFAULT) == 0) { .... } else if (key.startsWith("hplsql.conn.init.")) { .... } else if (key.startsWith(Conf.CONN_CONVERT)) { .... } else if (key.startsWith("hplsql.conn.")) { .... } else if (key.startsWith("hplsql.")) { // <= .... } } 

Itu cukup panjang jika-jika-jika membangun! Penganalisis tidak menyukai kondisi di if terakhir (key.startsWith ("hplsql.")) Karena jika eksekusi mencapainya, itu berarti itu benar. Memang, jika Anda melihat baris pertama dari seluruh konstruksi if-else-if ini, Anda akan melihat bahwa itu sudah berisi cek yang berlawanan, jadi jika string tidak dimulai dengan "hplsql." , eksekusi akan segera dilewati ke iterasi berikutnya.

Ekspresi V6007 ' columnNameProperty.length () == 0' selalu salah. 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>() : ....; .... } } } .... } 

Perbandingan panjang string columnNameProperty dengan nol akan selalu menghasilkan false . Ini terjadi karena perbandingan ini mengikuti cek! Strings.isNullOrEmpty (columnNameProperty) . Jadi jika eksekusi mencapai kondisi kita, itu berarti bahwa string columnNameProperty pasti bukan nol atau kosong.

Hal yang sama berlaku untuk string columnTypeProperty satu baris nanti:

  • Ekspresi V6007 'columnTypeProperty.length () == 0' selalu salah. OrcRecordUpdater.java (239)

Ekspresi V6007 'colOrScalar1.equals ("Column")' selalu salah. 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")) // <= { .... } else if (colOrScalar1.equals("Col") && colOrScalar1.equals("Scalar")) { .... } else if (colOrScalar1.equals("Scalar") && colOrScalar1.equals("Column")) { .... } } 

Copy-paste tua yang bagus. Dari sudut pandang logika saat ini, string colOrScalar1 mungkin memiliki dua nilai yang berbeda sekaligus, yang tidak mungkin. Jelas, cek harus memiliki variabel colOrScalar1 di sebelah kiri dan colOrScalar2 di sebelah kanan.

Peringatan serupa beberapa baris di bawah ini:

  • Ekspresi V6007 'colOrScalar1.equals ("Scalar")' selalu salah. GenVectorCode.java (3475)
  • Ekspresi V6007 'colOrScalar1.equals ("Column")' selalu salah. GenVectorCode.java (3486)

Akibatnya, konstruksi if-else-if ini tidak akan pernah melakukan apa pun.

Beberapa peringatan V6007 lagi:

  • Ekspresi V6007 'karakter == null' selalu salah. RandomTypeUtil.java (43)
  • Ekspresi V6007 'writeIdHwm> 0' selalu salah. TxnHandler.java (1603)
  • Ekspresi V6007 'fields.equals ("*")' selalu benar. Server.java (983)
  • Ekspresi V6007 'currentGroups! = Null' selalu benar. GenericUDFCurrentGroups.java (90)
  • Ekspresi V6007 'this.wh == null' selalu salah. Pengembalian baru bukan-null referensi. StorageBasedAuthorizationProvider.java (93), StorageBasedAuthorizationProvider.java (92)
  • dan seterusnya ...

NPE


V6008 Potensi null dereference dari '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(); ..... } } 

Objek nol ditangkap, dicatat, dan ... program tetap berjalan. Akibatnya, pemeriksaan diikuti oleh dereference pointer nol. Aduh!

Pengembang harus benar-benar menginginkan program untuk keluar dari fungsi atau melemparkan beberapa pengecualian khusus dalam kasus mendapatkan referensi nol.

V6008 Null dereference 'buffer' dalam fungsi 'unlockSingleBuffer'. MetadataCache.java (410), MetadataCache.java (465)

 private boolean lockBuffer(LlapBufferOrBuffers buffers, ....) { LlapAllocatorBuffer buffer = buffers.getSingleLlapBuffer(); if (buffer != null) { // <= return lockOneBuffer(buffer, doNotifyPolicy); } LlapAllocatorBuffer[] bufferArray = buffers.getMultipleLlapBuffers(); for (int i = 0; i < bufferArray.length; ++i) { if (lockOneBuffer(bufferArray[i], doNotifyPolicy)) continue; for (int j = 0; j < i; ++j) { unlockSingleBuffer(buffer, true); // <= } .... } .... } .... private void unlockSingleBuffer(LlapAllocatorBuffer buffer, ....) { boolean isLastDecref = (buffer.decRef() == 0); // <= if (isLastDecref) { .... } } 

NPE potensial lainnya. Jika eksekusi mencapai metode unlockSingleBuffer , itu berarti objek buffer adalah nol. Misalkan itulah yang terjadi! Jika Anda melihat metode unlockSingleBuffer , Anda akan melihat bagaimana objek kami ditereferensi langsung di baris pertama. Gotcha!

Pergeseran menjadi liar


V6034 Shift dengan nilai 'bitShiftsInWord - 1' bisa tidak konsisten dengan ukuran tipe: '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; // check this because "123 << 32" will be 123. final boolean noRestore = bitShiftsInWord == 0; final int roundCarryNoRestoreMask = 1 << 31; final int roundCarryMask = (1 << (bitShiftsInWord - 1)); // <= .... } 

Ini adalah potensi pergeseran sebesar -1. Jika metode ini dipanggil dengan, katakanlah, wordShifts == 3 dan bitShiftsInWord == 0 , baris yang dilaporkan akan berakhir dengan 1 << -1. Apakah itu perilaku yang direncanakan?

V6034 Shift dengan nilai 'j' bisa tidak konsisten dengan ukuran tipe: '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); // <= } .... } .... } 

Pada baris yang dilaporkan, variabel j dapat memiliki nilai dalam rentang [0 ... 63]. Karena itu, perhitungan nilai val dalam loop dapat berjalan secara tak terduga. Dalam ekspresi (1 << j) , nilai 1 adalah tipe int , jadi menggesernya dengan 32 bit dan lebih banyak membawa kita melampaui batas kisaran tipe. Ini dapat diperbaiki dengan menulis ((panjang) 1 << j) .

Dibawa oleh logging


V6046 Format salah. Jumlah item format yang berbeda diharapkan. Argumen tidak digunakan: 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 {}-{} ;", // <= stat.size(), sig.size() )); .... } .... if (e.getAll(OperatorStats.IncorrectRuntimeStatsMarker.class).size() > 0) { LOG.debug( "Ignoring {}, marked with OperatorStats.IncorrectRuntimeStatsMarker", sig.get(0) ); continue; } .... } 

Saat menulis kode untuk memformat string menggunakan String.format () , pengembang menggunakan sintaks yang salah. Akibatnya, parameter yang diteruskan tidak pernah sampai ke string yang dihasilkan. Dugaan saya adalah bahwa pengembang telah mengerjakan penebangan sebelum menulis ini, di mana mereka meminjam sintaks.

Pengecualian yang dicuri


V6051 Penggunaan pernyataan 'kembali' di blok 'akhirnya' dapat menyebabkan hilangnya pengecualian yang tidak ditangani. ObjectStore.java (9080)

 private List<MPartitionColumnStatistics> getMPartitionColumnStatistics(....) throws NoSuchObjectException, MetaException { boolean committed = false; try { .... /*some actions*/ 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(); } } } 

Mengembalikan apa pun dari blok terakhir adalah praktik yang sangat buruk, dan contoh ini dengan jelas menunjukkan alasannya.

Di blok percobaan , program membentuk permintaan dan mengakses penyimpanan. Variabel yang dikomit memiliki nilai false secara default dan mengubah statusnya hanya setelah semua tindakan sebelumnya di blok percobaan telah berhasil dieksekusi. Ini berarti bahwa jika pengecualian dikemukakan, variabel itu akan selalu salah . Blok penangkap akan menangkap pengecualian, sesuaikan sedikit, dan buang. Jadi ketika giliran blok akhirnya , eksekusi akan memasuki kondisi dari mana daftar kosong akan dikembalikan. Berapa pengembalian ini kepada kita? Yah, itu biaya kita mencegah pengecualian tertangkap dari dibuang ke luar di mana itu bisa ditangani dengan benar. Tidak ada pengecualian yang ditentukan dalam tanda tangan metode yang akan dilemparkan; mereka hanya menyesatkan.

Pesan diagnostik serupa:

  • V6051 Penggunaan pernyataan 'kembali' di blok 'akhirnya' dapat menyebabkan hilangnya pengecualian yang tidak ditangani. ObjectStore.java (808)

Lain-lain


Fungsi V6009 'compareTo' menerima argumen aneh. Objek 'o2.getWorkerIdentity ()' digunakan sebagai argumen untuk metode sendiri. 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()); // <= } }); .... } 

Mungkin ada sejumlah penyebab yang menyebabkan kesalahan konyol: copy-paste, kecerobohan, terburu-buru, dan sebagainya. Kita sering melihat kesalahan seperti itu dalam proyek open-source dan bahkan memiliki seluruh artikel tentang itu.

V6020 Bagilah dengan nol. Kisaran nilai penyebut 'pembagi' termasuk nol. SqlMathUtil.java (265)

 public static long divideUnsignedLong(long dividend, long divisor) { if (divisor < 0L) { /*some comments*/ return (compareUnsignedLong(dividend, divisor)) < 0 ? 0L : 1L; } if (dividend >= 0) { // Both inputs non-negative return dividend / divisor; // <= } else { .... } } 

Yang ini cukup sepele. Serangkaian cek tidak berdaya untuk mencegah pembagian dengan nol.

Beberapa peringatan lagi:

  • V6020 Mod dengan nol. Kisaran nilai penyebut 'pembagi' termasuk nol. SqlMathUtil.java (309)
  • V6020 Bagilah dengan nol. Kisaran nilai penyebut 'pembagi' termasuk nol. SqlMathUtil.java (276)
  • V6020 Bagilah dengan nol. Kisaran nilai penyebut 'pembagi' termasuk nol. SqlMathUtil.java (312)

V6030 Metode yang terletak di sebelah kanan '|' operator akan dipanggil terlepas dari nilai operan kiri. Mungkin, lebih baik menggunakan '||'. OperatorUtils.java (573)

 public static Operator<? extends OperatorDesc> findSourceRS(....) { .... List<Operator<? extends OperatorDesc>> parents = ....; if (parents == null | parents.isEmpty()) { // reached end eg TS operator return null; } .... } 

Programmer menulis operator bitwise | bukannya logis ||. Itu berarti bagian kanan akan dieksekusi tidak peduli hasil dari yang kiri. Jika orang tua == null , kesalahan ketik ini akan berakhir dengan NPE tepat di subekspresi logis berikutnya.

V6042 Ekspresi diperiksa untuk kompatibilitas dengan tipe 'A' tetapi dilemparkan ke tipe '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); // <= break; case DATE: outVCA = new VectorLongColumnAssign() { .... } .init(...., (LongColumnVector) destCol); break; case INTERVAL_YEAR_MONTH: outVCA = new VectorLongColumnAssign() { .... }.init(...., (LongColumnVector) destCol); break; case INTERVAL_DAY_TIME: outVCA = new VectorIntervalDayTimeColumnAssign() { .... }.init(...., (IntervalDayTimeColumnVector) destCol);// <= break; default: throw new HiveException(....); } } else if (destCol instanceof DoubleColumnVector) { .... } .... else { throw new HiveException(....); } return outVCA; } 

Kami tertarik pada kelas yang LongColumnVector memperluas ColumnVector dan TimestampColumnVector memperluas ColumnVector . Pemeriksaan bahwa objek destCol adalah turunan dari LongColumnVector secara eksplisit menunjukkan bahwa itu adalah objek kelas ini yang akan ditangani dalam tubuh pernyataan bersyarat. Meskipun demikian, bagaimanapun, itu masih dilemparkan ke TimestampColumnVector ! Seperti yang Anda lihat, ini adalah kelas yang berbeda kecuali bahwa mereka berasal dari orangtua yang sama. Sebagai hasilnya, kami mendapatkan ClassCastException .

Hal yang sama berlaku untuk casting ke IntervalDayTimeColumnVector :

  • V6042 Ekspresi diperiksa untuk kompatibilitas dengan tipe 'A' tetapi dilemparkan ke tipe 'B'. VectorColumnAssignFactory.java (390)

V6060 Referensi 'var' digunakan sebelum diverifikasi terhadap nol. Var.java (402), Var.java (395)

 @Override public boolean equals(Object obj) { if (getClass() != obj.getClass()) { // <= return false; } Var var = (Var)obj; if (this == var) { return true; } else if (var == null || // <= var.value == null || this.value == null) { return false; } .... } 

Di sini Anda melihat pemeriksaan aneh objek var untuk null setelah dereference telah terjadi. Dalam konteks ini, var dan obj adalah objek yang sama ( var = (Var) obj ). Kehadiran cek nol menyiratkan bahwa objek yang dikirimkan mungkin nol. Jadi, memanggil equals (null) akan menghasilkan NPE, bukannya false yang diharapkan, tepat di baris pertama. Ya, ceknya ada di sana, tapi, sayangnya, ada di tempat yang salah.

Beberapa kasus serupa lainnya, di mana objek digunakan sebelum pemeriksaan:

  • V6060 Referensi 'nilai' digunakan sebelum diverifikasi terhadap nol. ParquetRecordReaderWrapper.java (168), ParquetRecordReaderWrapper.java (166)
  • V6060 Referensi 'defaultConstraintCols' digunakan sebelum diverifikasi terhadap nol. HiveMetaStore.java (2539), HiveMetaStore.java (2530)
  • V6060 Referensi 'projIndxLst' digunakan sebelum diverifikasi terhadap nol. RelOptHiveTable.java (683), RelOptHiveTable.java (682)
  • V6060 Referensi 'oldp' digunakan sebelum diverifikasi terhadap nol. ObjectStore.java (4343), ObjectStore.java (4339)
  • dan seterusnya ...

Kesimpulan


Jika Anda pernah tertarik pada Big Data jika hanya sedikit, maka Anda tidak akan menyadari betapa pentingnya Apache Hive. Ini adalah proyek yang populer, dan cukup besar, terdiri dari lebih dari 6500 file sumber (* .java). Banyak pengembang telah menulisnya selama bertahun-tahun, yang berarti ada banyak hal yang dapat ditemukan oleh penganalisa statis di sana. Ini membuktikan sekali lagi bahwa analisis statis sangat penting dan berguna ketika mengembangkan proyek-proyek menengah dan besar!

Catatan Pemeriksaan satu kali seperti yang saya lakukan di sini baik untuk menunjukkan kemampuan penganalisa tetapi skenario yang benar-benar tidak tepat untuk menggunakannya. Gagasan ini diuraikan di sini dan di sini . Analisis statis akan digunakan secara teratur!

Pemeriksaan Hive ini mengungkapkan beberapa cacat dan fragmen yang mencurigakan. Jika penulis Apache Hive menemukan artikel ini, kami akan dengan senang hati membantu kerja keras meningkatkan proyek.

Anda tidak dapat membayangkan Apache Hive tanpa Apache Hadoop, jadi Unicorn dari PVS-Studio juga dapat berkunjung ke sana. Tapi itu saja untuk hari ini. Sementara itu, saya mengundang Anda untuk mengunduh penganalisa dan memeriksa proyek Anda sendiri.

Source: https://habr.com/ru/post/id463753/


All Articles