Solusi untuk Tantangan Penemuan Bug yang Ditawarkan oleh Tim PVS-Studio di Konferensi pada 2018-2019

Gambar 2


Hai! Meskipun musim konferensi 2019 belum berakhir, kami ingin berbicara tentang tantangan menemukan bug yang kami tawarkan kepada pengunjung di stan kami selama konferensi sebelumnya. Dimulai dengan musim gugur 2019, kami telah membawa serangkaian tantangan baru, jadi sekarang kami dapat mengungkapkan solusi untuk tugas-tugas sebelumnya pada 2018 dan paruh pertama 2019 - setelah semua, banyak dari mereka berasal dari artikel yang diposting sebelumnya, dan kami memiliki tautan atau kode QR dengan informasi tentang masing-masing artikel yang dicetak pada selebaran tantangan kami.

Jika Anda menghadiri acara di mana kami berpartisipasi dengan stan, Anda mungkin melihat atau bahkan mencoba untuk menyelesaikan beberapa tantangan kami. Ini adalah potongan kode dari proyek sumber terbuka nyata yang ditulis dalam C, C ++, C #, atau Java. Setiap cuplikan berisi bug, dan para tamu ditantang untuk mencoba menemukannya. Solusi yang berhasil (atau hanya partisipasi dalam diskusi bug) dihargai dengan hadiah: status desktop terikat spiral, gantungan kunci, dan sejenisnya:

Gambar 4

Mau juga? Kemudian selamat datang di stan kami di acara mendatang.

Ngomong-ngomong, dalam artikel " Waktu Konferensi! Ringkas 2018 " dan " Konferensi. Sub-total untuk paruh pertama 2019 ", kami berbagi pengalaman berpartisipasi dalam acara yang diadakan awal tahun ini dan pada 2018.

Oke, mari kita mainkan game "Temukan bug" kami. Pertama-tama kita akan melihat tantangan sebelumnya pada tahun 2018, dikelompokkan berdasarkan bahasa.

2018


C ++


Bug kromium

static const int kDaysInMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; bool ValidateDateTime(const DateTime& time) { if (time.year < 1 || time.year > 9999 || time.month < 1 || time.month > 12 || time.day < 1 || time.day > 31 || time.hour < 0 || time.hour > 23 || time.minute < 0 || time.minute > 59 || time.second < 0 || time.second > 59) { return false; } if (time.month == 2 && IsLeapYear(time.year)) { return time.month <= kDaysInMonth[time.month] + 1; } else { return time.month <= kDaysInMonth[time.month]; } } 

Solusi
Bug yang ditemukan di Chromium ini mungkin merupakan tantangan yang paling "berjalan lama"; kami menawarkannya hingga 2018 dan memasukkannya ke dalam beberapa presentasi juga.

 if (time.month == 2 && IsLeapYear(time.year)) { return time.month <= kDaysInMonth[time.month] + 1; // <= day } else { return time.month <= kDaysInMonth[time.month]; // <= day } 

Badan blok If-else terakhir berisi kesalahan ketik dalam pernyataan pengembalian: time.month secara tidak sengaja ditulis untuk kedua kalinya alih-alih time.day . Kesalahan ini membuat fungsi kembali benar setiap saat. Bug dibahas secara rinci dalam artikel " 31 Februari " dan merupakan contoh keren bug yang tidak mudah dilihat oleh ulasan kode. Kasus ini juga merupakan demonstrasi yang baik tentang bagaimana kami menggunakan analisis dataflow.

Bug mesin tidak nyata

 bool VertInfluencedByActiveBone( FParticleEmitterInstance* Owner, USkeletalMeshComponent* InSkelMeshComponent, int32 InVertexIndex, int32* OutBoneIndex = NULL); void UParticleModuleLocationSkelVertSurface::Spawn(....) { .... int32 BoneIndex1, BoneIndex2, BoneIndex3; BoneIndex1 = BoneIndex2 = BoneIndex3 = INDEX_NONE; if(!VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[0], &BoneIndex1) && !VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[1], &BoneIndex2) && !VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[2]) &BoneIndex3) { .... } 

Solusi
Hal pertama yang perlu diperhatikan di sini adalah bahwa argumen terakhir dari fungsi VertInfluencedByActiveBone () memiliki nilai default dan tidak perlu ditentukan. Sekarang lihat blok if dalam bentuk yang disederhanakan:

 if (!foo(....) && !foo(....) && !foo(....) & arg) 

Bug sekarang terlihat jelas. Karena kesalahan ketik, panggilan ketiga fungsi VertInfluencedByActiveBone () dilakukan dengan tiga argumen, bukan empat, dengan nilai balik lalu berpartisipasi dalam & operasi (bitwise AND: operan kiri adalah nilai tipe bool yang dikembalikan oleh VertInfluencedByActiveBone ( ) , dan operan yang tepat adalah variabel integer BoneIndex3 ). Kode masih dapat dikompilasi. Ini adalah versi tetap (koma ditambahkan, tanda kurung tutup dipindahkan ke akhir ekspresi):

 if(!VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[0], &BoneIndex1) && !VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[1], &BoneIndex2) && !VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[2], &BoneIndex3)) 

Kesalahan ini awalnya disebutkan dalam artikel " Pemeriksaan yang Lama Ditunggu-tunggu dari Unreal Engine 4 ", di mana ia berjudul "kesalahan terbaik", yang saya setujui sepenuhnya.

Bug Android

 void TagMonitor::parseTagsToMonitor(String8 tagNames) { std::lock_guard<std::mutex> lock(mMonitorMutex); // Expand shorthands if (ssize_t idx = tagNames.find("3a") != -1) { ssize_t end = tagNames.find(",", idx); char* start = tagNames.lockBuffer(tagNames.size()); start[idx] = '\0'; .... } .... } 

Solusi
Programmer memiliki asumsi yang salah tentang presedensi operasi dalam kondisi blok if . Kode ini tidak berfungsi seperti yang diharapkan:

 if (ssize_t idx = (tagNames.find("3a") != -1)) 

Variabel idx akan diberi nilai 0 atau 1, dan apakah kondisinya benar atau salah akan bergantung pada nilai ini, yang merupakan kesalahan. Ini adalah versi tetap:

 ssize_t idx = tagNames.find("3a"); if (idx != -1) 

Bug ini disebutkan dalam artikel " Kami Memeriksa Kode Sumber Android oleh PVS-Studio, atau Tidak Ada yang Sempurna ."

Inilah tantangan non-sepele lainnya dengan bug Android:

 typedef int32_t GGLfixed; GGLfixed gglFastDivx(GGLfixed n, GGLfixed d) { if ((d>>24) && ((d>>24)+1)) { n >>= 8; d >>= 8; } return gglMulx(n, gglRecip(d)); } 

Solusi
Masalahnya adalah dalam ekspresi (d >> 24) + 1 .

Programmer ingin memeriksa bahwa 8 bit paling signifikan dari variabel d diatur ke 1 tetapi tidak semuanya pada satu waktu. Dengan kata lain, mereka ingin memeriksa bahwa byte paling signifikan menyimpan nilai apa pun kecuali 0x00 dan 0xFF. Pertama programmer memeriksa bit yang paling signifikan untuk null menggunakan ekspresi (d >> 24). Kemudian mereka menggeser delapan bit paling signifikan ke byte paling tidak signifikan, mengharapkan bit tanda paling signifikan untuk diduplikasi di semua bit lainnya. Yaitu, jika variabel d memiliki nilai 0b11111111'00000000'00000000'00000000, itu akan berubah menjadi 0b11111111'11111111'11111111'11111111 setelah shift. Dengan menambahkan 1 ke nilai int 0xFFFFFFFF, programmer berharap mendapatkan 0 (-1 + 1 = 0). Jadi, ekspresi ((d >> 24) +1) digunakan untuk memeriksa bahwa tidak semua dari delapan bit paling signifikan diatur ke 1.

Namun, bit tanda paling signifikan tidak selalu mendapatkan "spread" ketika digeser. Ini adalah apa standar yang mengatakan: Β«Nilai E1 >> E2 adalah E1 kanan bergeser E2 menggigit posisi. Jika E1 memiliki tipe yang tidak ditandatangani atau jika E1 memiliki tipe yang ditandatangani dan nilai yang tidak negatif, nilai hasilnya adalah bagian integral dari hasil bagi dari E1 / 2 ^ E2. Jika E1 memiliki tipe yang ditandatangani dan nilai negatif, nilai yang dihasilkan ditentukan oleh implementasi . "

Jadi, ini adalah contoh perilaku yang ditentukan implementasi. Bagaimana tepatnya kode ini bekerja tergantung pada arsitektur CPU dan implementasi kompiler. Bit yang paling signifikan mungkin berakhir sebagai nol setelah shift, dan ekspresi ((d >> 24) +1) kemudian akan selalu mengembalikan nilai selain 0, yaitu nilai yang selalu benar.

Memang, itu adalah tantangan yang tidak sepele. Seperti bug sebelumnya, bug ini pada awalnya dibahas dalam artikel " Kami Memeriksa Kode Sumber Android oleh PVS-Studio, atau Tidak Ada yang Sempurna ".

2019


C ++


"Itu semua salah GCC"

 int foo(const unsigned char *s) { int r = 0; while(*s) { r += ((r * 20891 + *s *200) | *s ^ 4 | *s ^ 3) ^ (r >> 1); s++; } return r & 0x7fffffff; } 

Programmer menyalahkan kompiler GCC 8 untuk bug tersebut. Apakah ini benar-benar kesalahan GCC?

Solusi
Fungsi mengembalikan nilai negatif karena kompiler tidak menghasilkan kode untuk bitwise AND (&). Bug ini ada hubungannya dengan perilaku yang tidak terdefinisi. Kompiler memperhatikan bahwa variabel r digunakan untuk menghitung dan menyimpan jumlah, dengan hanya nilai-nilai positif yang terlibat. Variabel r tidak boleh overflow karena itu akan menjadi perilaku yang tidak terdefinisi, yang tidak harus diperhitungkan oleh kompiler sama sekali. Jadi disimpulkan bahwa karena r tidak dapat memiliki nilai negatif di akhir loop, operasi r & 0x7fffffff , yang menghapus bit tanda, tidak perlu, jadi itu hanya memberitahu fungsi untuk mengembalikan nilai r .

Kesalahan ini dijelaskan dalam artikel " PVS-Studio 6.26 Dirilis ".

Bug QT

 static inline const QMetaObjectPrivate *priv(const uint* data) { return reinterpret_cast<const QMetaObjectPrivate*>(data); } bool QMetaEnum::isFlag() const { const int offset = priv(mobj->d.data)->revision >= 8 ? 2 : 1; return mobj && mobj->d.data[handle + offset] & EnumIsFlag; } 

Solusi
Penunjuk mobj ditangani dengan cara yang tidak aman: pertama dereferensi, lalu diperiksa. Klasik.

Bug tersebut disebutkan dalam artikel " Pemeriksaan Ketiga Qt 5 dengan PVS-Studio ".

C #


Bug Infer.NET

 public static void WriteAttribute(TextWriter writer, string name, object defaultValue, object value, Func<object, string> converter = null) { if ( defaultValue == null && value == null || value.Equals(defaultValue)) { return; } string stringValue = converter == null ? value.ToString() : converter(value); writer.Write($"{name}=\"{stringValue}\" "); } 

Solusi
Dereferensi kosong dari variabel nilai dapat terjadi saat mengevaluasi nilai. Persamaan ( nilai default) . Ini akan terjadi ketika nilai-nilai variabel sedemikian rupa sehingga DefaultValue! = Null dan nilai == null .

Bug ini berasal dari artikel " Apa Kesalahan Mengintai di Kode Infer.NET? "

Bug FastReport

 public class FastString { private const int initCapacity = 32; private void Init(int iniCapacity) { sb = new StringBuilder(iniCapacity); .... } public FastString() { Init(initCapacity); } public FastString(int iniCapacity) { Init(initCapacity); } public StringBuilder StringBuilder => sb; } .... Console.WriteLine(new FastString(256).StringBuilder.Capacity); 

Apa yang akan dihasilkan program di konsol? Apa yang salah dengan kelas FastString ?

Solusi
Program akan menampilkan nilai 32. Alasannya adalah nama yang salah eja dari variabel yang diteruskan ke metode Init di konstruktor:

 public FastString(int iniCapacity){ Init(initCapacity); } 

Parameter konstruktor iniCapacity tidak akan digunakan; apa yang dilewatkan adalah initCapacity yang konstan.

Bug itu dibahas dalam artikel " Laporan Tercepat di Wild West - dan Segenggam Bug ... "

Roslyn bug

 private SyntaxNode GetNode(SyntaxNode root) { var current = root; .... while (current.FullSpan.Contains(....)) { .... var nodeOrToken = current.ChildThatContainsPosition(....); .... current = nodeOrToken.AsNode(); } .... } public SyntaxNode AsNode() { if (_token != null) { return null; } return _nodeOrParent; } 

Solusi
Potensi null dereference of current dalam ekspresi current.FullSpan.Contains (....) . Variabel saat ini dapat diberi nilai null sebagai hasil dari memanggil metode nodeOrToken.AsNode () .

Bug ini berasal dari artikel " Memeriksa Kode Sumber Roslyn ".

Bug persatuan

 .... staticFields = packedSnapshot.typeDescriptions .Where(t => t.staticFieldBytes != null & t.staticFieldBytes.Length > 0) .Select(t => UnpackStaticFields(t)) .ToArray() .... 

Solusi
Kesalahan ketik: operator & digunakan sebagai ganti && . Ini menghasilkan eksekusi t.staticFieldBytes.Length> 0 periksa semua waktu, bahkan jika variabel t.staticFieldBytes adalah null , yang, pada gilirannya, mengarah ke null dereference.

Bug ini awalnya ditampilkan di artikel " Membahas Kesalahan dalam Komponen Sumber Terbuka Unity3D ".

Jawa


Bug IntelliJ IDEA

 private static boolean checkSentenceCapitalization(@NotNull String value) { List<String> words = StringUtil.split(value, " "); .... int capitalized = 1; .... return capitalized / words.size() < 0.2; // allow reasonable amount of // capitalized words } 

Mengapa program salah menghitung jumlah kata yang ditulis dengan huruf besar?

Solusi
Fungsi ini diharapkan mengembalikan true jika jumlah kata dalam huruf kapital kurang dari 20%. Tetapi pemeriksaan tidak berfungsi karena pembagian bilangan bulat, yang hanya mengevaluasi ke 0 atau 1. Fungsi akan mengembalikan false hanya jika semua kata dikapitalisasi. Kalau tidak, pembagian akan menghasilkan 0 dan fungsi akan mengembalikan true .

Bug ini berasal dari artikel " PVS-Studio untuk Java ".

Bug Spotbugs

 public static String getXMLType(@WillNotClose InputStream in) throws IOException { .... String s; int count = 0; while (count < 4) { s = r.readLine(); if (s == null) { break; } Matcher m = tag.matcher(s); if (m.find()) { return m.group(1); } } throw new IOException("Didn't find xml tag"); .... } 

Apa yang salah dengan pencarian tag xml?

Solusi
Kondisi hitungan <4 akan selalu benar karena jumlah variabel tidak bertambah di dalam loop. Tag xml dimaksudkan untuk dicari dalam empat baris pertama file, tetapi karena penambahan yang kurang, program akan membaca seluruh file.

Seperti bug sebelumnya, bug ini dijelaskan dalam artikel " PVS-Studio untuk Java ".

Itu saja untuk hari ini. Datang melihat kami di acara mendatang - mencari unicorn. Kami akan menawarkan tantangan baru yang menarik dan, tentu saja, memberikan hadiah. Sampai jumpa!

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


All Articles