Penjelasan singkat tentang teknologi yang digunakan dalam alat PVS-Studio yang secara efektif dapat mendeteksi sejumlah besar pola kesalahan dan kerentanan potensial. Artikel ini menjelaskan implementasi alat analisis untuk kode C dan C ++, namun, informasi di atas juga berlaku untuk modul yang bertanggung jawab untuk menganalisis kode C # dan Java.
Pendahuluan
Ada kesalahpahaman bahwa penganalisa kode statis adalah program yang cukup sederhana, yang didasarkan pada pencarian pola kode menggunakan ekspresi reguler. Ini jauh dari kebenaran. Selain itu, mengidentifikasi sebagian besar kesalahan menggunakan ekspresi reguler sama sekali
tidak mungkin .
Kesalahan muncul berdasarkan pengalaman pemrogram ketika bekerja dengan beberapa alat yang ada 10-20 tahun yang lalu. Pekerjaan alat sering benar-benar datang untuk menemukan pola kode berbahaya dan fungsi seperti
strcpy ,
strcat , dll. Sebagai perwakilan dari kelas alat ini dapat disebut
RATS .
Alat-alat semacam itu, meskipun bisa bermanfaat, pada umumnya bodoh dan tidak efektif. Dari saat-saat itulah banyak programmer masih memiliki ingatan bahwa analisa statis adalah alat yang sangat tidak berguna yang lebih banyak mengganggu pekerjaan daripada membantunya.
Waktu berlalu, dan analis statis mulai membentuk solusi kompleks yang melakukan analisis kode mendalam dan menemukan kesalahan yang tetap ada dalam kode bahkan setelah peninjauan kode yang cermat. Sayangnya, karena pengalaman negatif masa lalu, banyak programmer masih menganggap metodologi analisis statis tidak berguna dan tidak terburu-buru untuk memperkenalkannya ke dalam proses pengembangan.
Pada artikel ini saya akan mencoba sedikit memperbaiki situasi. Saya meminta pembaca meluangkan waktu 15 menit untuk berkenalan dengan teknologi yang digunakan dalam penganalisa kode statis PVS-Studio untuk mendeteksi kesalahan. Mungkin setelah itu Anda akan melihat segar pada alat analisis statis dan ingin menerapkannya dalam pekerjaan Anda.
Analisis Aliran Data
Analisis aliran data memungkinkan Anda menemukan berbagai kesalahan. Diantaranya: keluar dari batas array, kebocoran memori, selalu kondisi benar / salah, mendereferensi penunjuk nol dan sebagainya.
Juga, analisis data dapat digunakan untuk mencari situasi ketika data yang tidak diverifikasi yang datang ke program dari luar digunakan. Seorang penyerang dapat menyiapkan serangkaian input data untuk membuat fungsi program sesuai kebutuhan. Dengan kata lain, ini dapat menggunakan kesalahan kontrol input yang tidak memadai sebagai kerentanan. Untuk mencari penggunaan data yang tidak terverifikasi di PVS-Studio, diagnostik khusus
V1010 telah diimplementasikan dan terus ditingkatkan.
Analisis aliran data (
Data-Flow Analysis ) adalah menghitung nilai-nilai variabel yang mungkin pada berbagai titik dalam program komputer. Sebagai contoh, jika pointer dereferenced, dan diketahui bahwa saat ini bisa menjadi nol, maka ini adalah kesalahan, dan penganalisa statis akan melaporkannya.
Mari kita lihat contoh praktis menggunakan analisis aliran data untuk mencari kesalahan. Di depan kami adalah fungsi dari proyek Protokol Buffer (protobuf), yang dirancang untuk memeriksa kebenaran tanggal.
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]; } }
Alat analisis PVS-Studio mendeteksi dua kesalahan logis dalam fungsi sekaligus dan menampilkan pesan berikut:
- V547 / CWE-571 Ekspresi 'time.month <= kDaysInMonth [time.month] + 1' selalu benar. waktu.cc 83
- V547 / CWE-571 Ekspresi 'time.month <= kDaysInMonth [time.month]' selalu benar. waktu.cc 85
Perhatikan subekspresi βtime.month <1 || waktu.bulan> 12 ". Jika nilai
bulan di luar kisaran [1..12], maka fungsi tersebut berhenti bekerja. Penganalisa memperhitungkan hal ini dan mengetahui bahwa jika pernyataan kedua
jika mulai dieksekusi, maka nilai
bulan tepat berada dalam kisaran [1..12]. Demikian pula, dia tahu tentang berbagai variabel lain (tahun, hari, dll.), Tetapi mereka tidak menarik bagi kita sekarang.
Sekarang mari kita lihat dua operator identik untuk mengakses elemen array:
kDaysInMonth [time.month] .
Array diatur secara statis, dan penganalisa mengetahui nilai dari semua elemennya:
static const int kDaysInMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
Karena bulan diberi nomor dari 1, penganalisis tidak mempertimbangkan 0 pada awal array. Ternyata nilai dalam rentang [28..31] dapat diekstraksi dari array.
Bergantung pada apakah tahun itu tahun kabisat atau tidak, 1 ditambahkan ke jumlah hari, tetapi ini juga tidak menarik bagi kita sekarang. Perbandingan itu sendiri penting:
time.month <= kDaysInMonth[time.month] + 1; time.month <= kDaysInMonth[time.month];
Kisaran [1..12] (jumlah bulan) dibandingkan dengan jumlah hari dalam sebulan.
Menimbang bahwa dalam kasus pertama bulan selalu Februari (
time.month == 2 ), kami mendapatkan bahwa rentang berikut dibandingkan:
- 2 <= 29
- [1..12] <= [28..31]
Seperti yang Anda lihat, hasil perbandingan selalu benar, yang merupakan hal yang diperingatkan oleh penganalisa PVS-Studio. Memang, kode tersebut berisi dua kesalahan ketik yang identik. Sisi kiri ekspresi harus menggunakan anggota kelas
hari , bukan
sebulan sama sekali.
Kode yang benar harus seperti ini:
if (time.month == 2 && IsLeapYear(time.year)) { return time.day <= kDaysInMonth[time.month] + 1; } else { return time.day <= kDaysInMonth[time.month]; }
Kesalahan yang dibahas di sini juga sebelumnya dijelaskan dalam artikel "
31 Februari ".
Eksekusi Simbolik
Pada bagian sebelumnya, kami mempertimbangkan metode di mana penganalisa menghitung nilai variabel yang mungkin. Namun, untuk menemukan beberapa kesalahan, tidak perlu mengetahui nilai-nilai variabel.
Eksekusi Simbolik berarti menyelesaikan persamaan dalam bentuk simbolis.
Saya tidak menemukan demo yang sesuai di
database kesalahan kami, jadi pertimbangkan contoh kode sintetis.
int Foo(int A, int B) { if (A == B) return 10 / (A - B); return 1; }
Analyzer PVS-Studio menghasilkan peringatan V609 / CWE-369 Divide by zero. Penyebut 'A - B' == 0. test.cpp 12
Nilai variabel
A dan
B tidak diketahui oleh penganalisa. Tetapi penganalisa tahu bahwa pada saat menghitung ekspresi
10 / (A - B), variabel
A dan
B adalah sama. Oleh karena itu, pembagian dengan 0 akan terjadi.
Saya mengatakan bahwa nilai-nilai
A dan
B tidak diketahui. Untuk kasus umum, ini benar. Namun, jika penganalisa melihat panggilan fungsi dengan nilai spesifik dari argumen aktual, maka itu akan mempertimbangkan ini. Pertimbangkan sebuah contoh:
int Div(int X) { return 10 / X; } void Foo() { for (int i = 0; i < 5; ++i) Div(i); }
Analyzer PVS-Studio memperingatkan pembagian dengan nol: V609 CWE-628 Dibagi dengan nol. Penyebut 'X' == 0. Fungsi 'Div' memproses nilai '[0..4]'. Periksa argumen pertama. Periksa baris: 106, 110. consoleapplication2017.cpp 106
Campuran teknologi sudah bekerja di sini: analisis aliran data, eksekusi simbolis dan anotasi metode otomatis (kita akan membahas teknologi ini di bagian berikutnya). Penganalisa melihat bahwa variabel
X digunakan sebagai pembagi dalam fungsi
Div . Berdasarkan ini, anotasi khusus secara otomatis dibangun untuk fungsi
Div . Lebih lanjut diperhitungkan bahwa rentang nilai [0..4] diteruskan ke fungsi sebagai argumen
X. Penganalisa menyimpulkan bahwa pembagian dengan 0 harus terjadi.
Anotasi Metode
Tim kami telah mencatat ribuan fungsi dan kelas yang disediakan di:
- Winapi
- C library standar
- perpustakaan templat standar (STL),
- glibc (Perpustakaan GNU C)
- Qt
- Mfc
- zlib
- libpng
- Openssl
- dan sebagainya
Semua fungsi dijelaskan secara manual, yang memungkinkan Anda untuk mengatur banyak karakteristik yang penting dalam hal menemukan kesalahan. Sebagai contoh, ditentukan bahwa ukuran buffer yang diteruskan ke fungsi
fread harus tidak kurang dari jumlah byte yang direncanakan untuk dibaca dari file. Hubungan antara argumen ke-2, ke-3 dan nilai yang dapat dikembalikan fungsi juga ditunjukkan. Itu semua terlihat seperti ini:
Berkat anotasi ini, kode berikut, yang menggunakan fungsi
ketakutan , akan segera mengungkapkan dua kesalahan.
void Foo(FILE *f) { char buf[100]; size_t i = fread(buf, sizeof(char), 1000, f); buf[i] = 1; .... }
Peringatan PVS-Studio:
- V512 CWE-119 Panggilan fungsi 'ketakutan' akan menyebabkan meluapnya buffer 'buf'. test.cpp 116
- V557 CWE-787 Array overrun dimungkinkan. Nilai indeks 'i' bisa mencapai 1000. test.cpp 117
Pertama, penganalisa mengalikan argumen aktual ke-2 dan ke-3 dan menghitung bahwa fungsi tersebut dapat membaca hingga 1000 byte data. Dalam hal ini, ukuran buffer hanya 100 byte, dan mungkin meluap.
Kedua, karena fungsi dapat membaca hingga 1000 byte, kisaran nilai yang mungkin dari variabel
i adalah [0..1000]. Dengan demikian, akses ke array dapat terjadi pada indeks yang salah.
Mari kita lihat contoh sederhana lain dari suatu kesalahan, deteksi yang dimungkinkan berkat peningkatan fungsi
memset . Berikut ini cuplikan kode dari proyek CryEngine V.
void EnableFloatExceptions(....) { .... CONTEXT ctx; memset(&ctx, sizeof(ctx), 0); .... }
Alat analisa PVS-Studio menemukan kesalahan ketik: V575 Fungsi 'memset' memproses elemen '0'. Periksa argumen ketiga. crythreadutil_win32.h 294
Membingungkan argumen 2 dan 3 dari fungsi. Akibatnya, fungsi memproses 0 byte dan tidak melakukan apa pun. Penganalisa memperhatikan anomali ini dan memperingatkan programmer tentang hal itu. Kami sebelumnya menggambarkan kesalahan ini dalam artikel "
Validasi CryEngine V yang ditunggu-tunggu ".
Alat analisis PVS-Studio tidak terbatas pada anotasi yang kami atur secara manual. Selain itu, ia secara mandiri mencoba membuat anotasi dengan mempelajari tubuh fungsi. Ini memungkinkan Anda menemukan kesalahan penggunaan fungsi yang tidak tepat. Sebagai contoh, penganalisa mengingat bahwa suatu fungsi dapat mengembalikan nullptr. Jika pointer yang dikembalikan oleh fungsi ini digunakan tanpa pemeriksaan pendahuluan, penganalisa akan memperingatkan tentang hal ini. Contoh:
int GlobalInt; int *Get() { return (rand() % 2) ? nullptr : &GlobalInt; } void Use() { *Get() = 1; }
Peringatan: V522 CWE-690 Mungkin ada referensi dereferensi pointer nol 'Get ()'. test.cpp 129
Catatan Anda dapat mendekati pencarian kesalahan yang baru saja diperiksa dengan cara yang berlawanan. Jangan ingat apa pun, dan setiap kali panggilan ke fungsi
Get ditemui, analisislah dengan mengetahui argumen yang sebenarnya. Algoritme semacam itu secara teoritis memungkinkan Anda menemukan lebih banyak kesalahan, tetapi memiliki kompleksitas eksponensial. Waktu analisis program tumbuh ratusan ribu kali, dan kami menganggap pendekatan ini sebagai jalan buntu dari sudut pandang praktis. Dalam PVS-Studio, kami sedang mengembangkan arah anotasi fungsi secara otomatis.
Pencocokan pola
Teknologi yang cocok dengan suatu pola, pada pandangan pertama, mungkin tampak seperti pencarian dengan ekspresi reguler. Faktanya, ini tidak benar, dan semuanya jauh lebih rumit.
Pertama, seperti yang sudah saya
katakan , ekspresi reguler pada umumnya tidak berharga. Kedua, penganalisa tidak bekerja dengan baris teks, tetapi dengan pohon sintaks, yang memungkinkan seseorang untuk mengenali pola kesalahan yang lebih kompleks dan tingkat tinggi.
Perhatikan dua contoh, satu lebih sederhana dan satu lagi kompleks. Kesalahan pertama yang saya temukan saat memeriksa kode sumber untuk Android.
void TagMonitor::parseTagsToMonitor(String8 tagNames) { std::lock_guard<std::mutex> lock(mMonitorMutex); if (ssize_t idx = tagNames.find("3a") != -1) { ssize_t end = tagNames.find(",", idx); char* start = tagNames.lockBuffer(tagNames.size()); start[idx] = '\0'; .... } .... }
Alat analisis PVS-Studio mengenali pola kesalahan klasik yang terkait dengan kesalahpahaman programmer tentang prioritas operasi di C ++: V593 / CWE-783 Pertimbangkan untuk meninjau ekspresi dari jenis 'A = B! = C'. Ekspresi dihitung sebagai berikut: 'A = (B! = C)'. TagMonitor.cpp 50
Perhatikan garis ini:
if (ssize_t idx = tagNames.find("3a") != -1) {
Programer mengasumsikan bahwa tugas dilakukan di awal, dan hanya kemudian perbandingan dengan
-1 . Bahkan, perbandingannya lebih dulu. Klasik Kesalahan ini
dijelaskan secara lebih rinci dalam
artikel yang ditujukan untuk verifikasi Android (lihat bab "Kesalahan Lain").
Sekarang pertimbangkan opsi pencocokan pola tingkat tinggi.
static inline void sha1ProcessChunk(....) { .... quint8 chunkBuffer[64]; .... #ifdef SHA1_WIPE_VARIABLES .... memset(chunkBuffer, 0, 64); #endif }
Peringatan PVS-Studio: V597 CWE-14 Kompiler dapat menghapus panggilan fungsi 'memset', yang digunakan untuk membersihkan buffer 'chunkBuffer'. Fungsi RtlSecureZeroMemory () harus digunakan untuk menghapus data pribadi. sha1.cpp 189
Inti masalahnya adalah bahwa setelah mengisi buffer dengan nol menggunakan fungsi
memset , buffer ini tidak digunakan di mana pun. Ketika mengkompilasi kode dengan flag optimasi, kompiler akan memutuskan bahwa panggilan fungsi ini berlebihan dan akan menghapusnya. Dia memiliki hak untuk ini, karena dari sudut pandang bahasa C ++, memanggil fungsi tidak memiliki perilaku yang dapat diamati pada program. Segera setelah mengisi buffer
chunkBuffer , fungsi
sha1ProcessChunk berakhir. Karena buffer dibuat pada stack, setelah keluar dari fungsinya, buffer tidak akan tersedia untuk digunakan. Oleh karena itu, dari sudut pandang kompiler, tidak masuk akal untuk mengisinya dengan nol.
Akibatnya, suatu tempat di tumpukan akan tetap menjadi data pribadi, yang dapat menyebabkan masalah. Topik ini dibahas lebih rinci dalam artikel "
Pembersihan Data Pribadi yang Aman ".
Ini adalah contoh pencocokan pola tingkat tinggi. Pertama, penganalisa harus menyadari keberadaan kelemahan keamanan ini, diklasifikasikan menurut Pencacahan Kelemahan Umum sebagai
CWE-14: Penghapusan Kompiler Kode untuk Menghapus Buffer .
Kedua, ia harus menemukan dalam kode semua tempat di mana buffer dibuat pada stack, itu dihapus menggunakan fungsi
memset dan tidak digunakan di tempat lain.
Kesimpulan
Seperti yang Anda lihat, analisis statis adalah metodologi yang sangat menarik dan bermanfaat. Ini memungkinkan Anda untuk menghilangkan sejumlah besar kesalahan dan kerentanan potensial pada tahap paling awal (lihat
SAST ). Jika Anda masih belum sepenuhnya dianalisa dengan statis, maka saya mengundang Anda untuk membaca
blog kami, di mana kami secara teratur menganalisis kesalahan yang ditemukan menggunakan PVS-Studio di berbagai proyek. Anda tidak bisa tetap acuh tak acuh.
Kami akan senang melihat perusahaan Anda di antara para
pelanggan kami dan membantu menjadikan aplikasi Anda lebih baik, lebih dapat diandalkan, dan lebih aman.

Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan ke terjemahan: Andrey Karpov.
Teknologi yang digunakan dalam penganalisa kode PVS-Studio untuk menemukan bug dan kerentanan potensial .