Top 10 Bug Ditemukan di Proyek Java pada tahun 2019


2019 akan segera berakhir, dan tim PVS-Studio sedang melihat kembali pencapaian tahun ini. Pada awal 2019, kami meningkatkan kemampuan diagnostik analis kami dengan menambahkan dukungan Java, yang memungkinkan kami untuk memeriksa dan meninjau proyek-proyek Java juga. Kami telah menemukan banyak bug selama tahun ini, dan inilah 10 bug teratas kami yang ditemukan di proyek Java.


Tidak. 10: byte yang ditandatangani


Sumber: Analisis Kerangka Apache Dubbo RPC oleh PVS-Studio Static Code Analyzer

Ekspresi V6007 'endKey [i] <0xff' selalu benar. OptionUtil.java (32)

public static final ByteSequence prefixEndOf(ByteSequence prefix) { byte[] endKey = prefix.getBytes().clone(); for (int i = endKey.length - 1; i >= 0; i--) { if (endKey[i] < 0xff) { // <= endKey[i] = (byte) (endKey[i] + 1); return ByteSequence.from(Arrays.copyOf(endKey, i + 1)); } } return ByteSequence.from(NO_PREFIX_END); } 

Banyak programmer percaya bahwa tipe byte tidak ditandai. Ini memang terjadi dalam banyak bahasa pemrograman. Misalnya, ini berlaku untuk C #. Tetapi tidak demikian halnya di Jawa.

Dalam kondisi endKey [i] <0xff , variabel tipe byte dibandingkan dengan angka 255 (0xff) yang diwakili dalam bentuk heksadesimal. Pengembang mungkin lupa bahwa kisaran tipe byte Java adalah [-128, 127]. Kondisi ini akan selalu benar, dan untuk loop akan selalu memproses hanya elemen terakhir dari array endKey .

Tidak. 9: Dua dalam satu


Sumber: PVS-Studio untuk Jawa menyentuh jalan. Perhentian berikutnya adalah Elasticsearch

Ekspresi V6007 '(int) x <0' selalu salah. BCrypt.java (429)

V6025 Kemungkinan indeks '(int) x' di luar batas. BCrypt.java (431)

 private static byte char64(char x) { if ((int)x < 0 || (int)x > index_64.length) return -1; return index_64[(int)x]; } 

Diskon! Satu metode - dua bug! Yang pertama berkaitan dengan tipe char : karena tidak ditandatangani di Java, kondisi (int) x <0 akan selalu salah. Bug kedua adalah pengindeksan biasa di luar batas array index_64 ketika (int) x == index_64.length . Ini terjadi karena kondisi (int) x> index_64.length . Bug dapat diperbaiki dengan mengubah operator '>' menjadi '> =': (int) x> = index_64.length .

Tidak. 8: Solusi dan implikasinya


Sumber: Menganalisis Kode Platform CUBA dengan PVS-Studio

Ekspresi V6007 'previousMenuItemFlatIndex> = 0' selalu benar. CubaSideMenuWidget.java (328)

 protected MenuItemWidget findNextMenuItem(MenuItemWidget currentItem) { List<MenuTreeNode> menuTree = buildVisibleTree(this); List<MenuItemWidget> menuItemWidgets = menuTreeToList(menuTree); int menuItemFlatIndex = menuItemWidgets.indexOf(currentItem); int previousMenuItemFlatIndex = menuItemFlatIndex + 1; if (previousMenuItemFlatIndex >= 0) { // <= return menuItemWidgets.get(previousMenuItemFlatIndex); } return null; } 

Penulis metode findNextMenuItem ingin menyingkirkan nilai -1 yang dikembalikan oleh metode indexOf ketika daftar menuItemWidgets tidak mengandung currentItem . Untuk melakukan itu, programmer menambahkan 1 ke hasil indexOf (variabel menuItemFlatIndex ) dan menulis nilai yang dihasilkan ke variabel beforeMenuItemFlatIndex , yang kemudian digunakan dalam metode ini. Penambahan -1 terbukti menjadi solusi yang buruk karena menyebabkan beberapa kesalahan sekaligus:

  • Pernyataan null kembali tidak akan pernah dieksekusi karena ekspresi beforeMenuItemFlatIndex> = 0 akan selalu benar; Oleh karena itu, metode findNextMenuItem akan selalu kembali dari dalam pernyataan if ;
  • IndexOutOfBoundsException akan dimunculkan ketika daftar menuItemWidgets telah menjadi kosong karena program akan mencoba mengakses elemen pertama dari daftar kosong;
  • IndexOutOfBoundsException akan dimunculkan ketika argumen currentItem telah menjadi elemen terakhir dari daftar menuItemWidget .

Tidak. 7: Membuat file dari ketiadaan


Sumber: Huawei Cloud: Berawan di PVS-Studio Hari Ini

V6008 Potensi null dereference dari 'dataTmpFile'. CacheManager.java (91)

 @Override public void putToCache(PutRecordsRequest putRecordsRequest) { .... if (dataTmpFile == null || !dataTmpFile.exists()) { try { dataTmpFile.createNewFile(); // <= } catch (IOException e) { LOGGER.error("Failed to create cache tmp file, return.", e); return; } } .... } 

Saat menulis metode putToCache , programmer membuat kesalahan ketik dalam kondisi dataTmpFile == null || ! dataTmpFile.exists () sebelum membuat file baru dataTmpFile.createNewFile () : mereka menulis operator '==' alih-alih '! ='. Kesalahan ketik ini akan menyebabkan Anda melempar NullPointerException saat memanggil metode createNewFile . Beginilah kondisinya dengan kesalahan pengetikan:

 if (dataTmpFile != null || !dataTmpFile.exists()) 

Anda mungkin berpikir, β€œBaiklah, kita bisa santai sekarang.” Belum!

Sekarang satu bug telah diperbaiki, satu lagi muncul. NullPointerException dapat dilemparkan saat memanggil metode dataTmpFile.exists () . Untuk memperbaiki ini, kita perlu mengganti '||' operator dengan '&&'. Ini adalah versi terakhir dari kondisi, setelah semua perbaikan:

 if (dataTmpFile != null && !dataTmpFile.exists()) 

Tidak. 6: Kesalahan logika yang sangat aneh


Sumber: PVS-Studio untuk Java

V6007 [CWE-570] Ekspresi '"0". Equals (text)' selalu salah. ConvertIntegerToDecimalPredicate.java 46

 public boolean satisfiedBy(@NotNull PsiElement element) { .... @NonNls final String text = expression.getText().replaceAll("_", ""); if (text == null || text.length() < 2) { return false; } if ("0".equals(text) || "0L".equals(text) || "0l".equals(text)) {// <= return false; } return text.charAt(0) == '0'; } 

Hal yang menarik tentang metode ini adalah mengandung kesalahan logika yang jelas. Jika metode puas dengan tidak kembali setelah pernyataan if pertama, itu berarti bahwa string teks setidaknya dua karakter. Ini juga berarti bahwa pemeriksaan pertama "0". Sama dengan (teks) di pernyataan berikutnya jika tidak ada artinya. Apa yang sebenarnya dimaksud oleh programmer itu masih merupakan misteri.

Tidak. 5: Benar-benar aneh!


Sumber: PVS-Studio Mengunjungi Apache Hive

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)); // <= .... } 

Dengan argumen input wordShifts = 3 dan bitShiftsInWord = 0 , variabel roundCarryMask , yang menyimpan hasil pergeseran bitwise (1 << (bitShiftsInWord - 1)) , akan menjadi angka negatif. Pengembang mungkin tidak mengharapkan itu.

Tidak. 4: Bisakah kita melihat pengecualian?


Sumber: PVS-Studio Mengunjungi Apache Hive

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(); } } } 

Deklarasi metode getMPartitionColumnStatistics menyesatkan karena dikatakan bisa melempar pengecualian. Pada kenyataannya, pengecualian apa pun yang dihasilkan di blok percobaan , variabel yang dikomit akan selalu salah , sehingga pernyataan kembali di blok akhirnya akan mengembalikan nilai, sementara semua pengecualian yang dilemparkan akan hilang, tidak dapat diproses di luar metode. Jadi, tidak ada pengecualian yang dilemparkan dalam metode ini yang dapat meninggalkannya.

Tidak. 3: Hocus-pocus, atau mencoba untuk mendapatkan topeng baru


Sumber: PVS-Studio Mengunjungi Apache Hive

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); // <= } .... } .... } 

Bug ini juga ada hubungannya dengan perubahan bitwise, tetapi tidak hanya itu. Variabel j digunakan sebagai penghitung rentang [0 ... 63] di dalam untuk loop. Penghitung ini berpartisipasi dalam shift bitwise 1 << j . Semuanya tampak OK, tetapi di sinilah '1' integer literal dari tipe int (nilai 32-bit) ikut bermain. Karena itu, pergeseran bitwise akan mulai mengembalikan nilai yang sebelumnya dikembalikan ketika nilai variabel j telah melebihi 31. Jika perilaku ini tidak seperti yang diinginkan oleh programmer, nilai 1 harus direpresentasikan sebagai panjang : 1L << j atau (panjang) 1 << j .

Tidak. 2: Pesanan inisialisasi


Sumber: Huawei Cloud: Berawan di PVS-Studio Hari Ini

Siklus inisialisasi Kelas V6050 hadir. Inisialisasi 'INSTANCE' muncul sebelum inisialisasi 'LOG'. UntrustedSSL.java (32), UntrustedSSL.java (59), UntrustedSSL.java (33)

 public class UntrustedSSL { private static final UntrustedSSL INSTANCE = new UntrustedSSL(); private static final Logger LOG = LoggerFactory.getLogger(UntrustedSSL.class); .... private UntrustedSSL() { try { .... } catch (Throwable t) { LOG.error(t.getMessage(), t); // <= } } } 

Urutan bidang mana yang dideklarasikan dalam kelas membuat perbedaan karena mereka diinisialisasi dalam urutan yang sama mereka dideklarasikan. Melupakan fakta ini menyebabkan bug yang sulit dipahami seperti yang ada di atas.

Penganalisis menunjukkan bahwa bidang statis LOG didereferensi dalam konstruktor saat ini diinisialisasi ke nilai nol , yang mengarah ke melempar serangkaian pengecualian NullPointerException -> ExceptionInInitializerError .

Tetapi mengapa LOG bidang statis memiliki nilai nol pada saat memanggil konstruktor?

ExceptionInInitializerError adalah petunjuknya. Konstruktor sedang menginisialisasi INSTANCE bidang statis, yang dideklarasikan sebelum bidang LOG . Itu sebabnya bidang LOG belum diinisialisasi ketika konstruktor dipanggil. Agar kode ini berfungsi dengan baik, bidang LOG harus diinisialisasi sebelum panggilan.

Pertama: pemrograman berorientasi copy-paste


Sumber: Apache Hadoop Code Quality: Production VS Test

V6072 Dua fragmen kode serupa ditemukan. Mungkin, ini adalah kesalahan ketik dan variabel 'localFiles' harus digunakan daripada 'localArchives'. LocalDistributedCacheManager.java (183), LocalDistributedCacheManager.java (178), LocalDistributedCacheManager.java (176), LocalDistributedCacheManager.java (181)

 public synchronized void setup(JobConf conf, JobID jobId) throws IOException { .... // Update the configuration object with localized data. if (!localArchives.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALARCHIVES, StringUtils .arrayToString(localArchives.toArray(new String[localArchives // <= .size()]))); } if (!localFiles.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALFILES, StringUtils .arrayToString(localFiles.toArray(new String[localArchives // <= .size()]))); } .... } 

Tempat pertama dalam daftar Top 10 kami diberikan untuk menyalin-menempel, atau, lebih tepatnya, bug yang berasal dari penggunaan teknik ini secara sembrono. Pernyataan if kedua terlihat sangat mirip dengan salinan yang pertama, dengan beberapa variabel dimodifikasi:

  • localArchives diubah menjadi LocalFiles ;
  • MRJobConfig.CACHE_LOCALARCHIVES diubah menjadi MRJobConfig.CACHE_LOCALFILES .

Tetapi programmer berhasil membuat kesalahan bahkan dalam operasi sederhana ini: pernyataan if di baris yang ditunjukkan oleh penganalisa masih menggunakan variabel localArchives bukannya variabel localFiles yang dimaksudkan.

Kesimpulan


Memperbaiki bug yang ditemukan pada tahap pengembangan selanjutnya atau setelah rilis membutuhkan banyak sumber daya. Alat analisa statis PVS-Studio memungkinkan Anda untuk mendeteksi bug pada tahap pengkodean, sehingga membuatnya lebih mudah dan lebih murah. Banyak perusahaan telah membuat hidup pengembang mereka lebih mudah dengan mulai menggunakan analisa secara teratur. Jika Anda ingin benar-benar menikmati pekerjaan Anda, cobalah PVS-Studio .

Kami tidak akan berhenti pada hal itu dan berencana untuk terus meningkatkan dan meningkatkan alat kami. Nantikan diagnostik dan artikel baru dengan bug yang lebih menarik di tahun berikutnya.

Saya melihat Anda suka petualangan! Pertama, Anda mengalahkan 10 bug teratas dalam proyek C # pada tahun 2019 dan sekarang Anda berhasil dengan Java! Selamat datang di tingkat berikutnya - artikel tentang kesalahan terbaik dalam proyek C ++ pada 2019 .

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


All Articles