Perl 5: bagaimana makro menyembunyikan kesalahan


Untuk melengkapi daftar bahasa pemrograman open source yang diuji menggunakan analisa kode statis PVS-Studio, Perl 5. dipilih. Artikel ini adalah tentang kesalahan yang ditemukan dan kesulitan dalam melihat hasil analisis. Jumlah makro dalam kode begitu besar sehingga tampaknya kode itu tidak ditulis dalam C, tetapi dalam beberapa dialek aneh itu. Meskipun kesulitan saat melihat kode, saya berhasil mengumpulkan masalah menarik, yang akan dibahas dalam artikel ini.

Pendahuluan


Perl adalah bahasa pemrograman tujuan umum dinamis tingkat tinggi yang diartikan (Perl adalah keluarga dari dua bahasa pemrograman tingkat tinggi, tujuan umum, ditafsirkan, dinamis). Perl 5 diluncurkan pada tahun 1994. Setelah beberapa dekade, kode C dengan banyak makro menyebabkan kegugupan bagi programmer modern.

Kode sumber untuk Perl 5 diambil dari repositori resmi (cabang blead ). Untuk memeriksa proyek, kami menggunakan penganalisa kode statis PVS-Studio . Analisis dilakukan pada sistem operasi Linux, tetapi penganalisa juga tersedia untuk Windows dan macOS.

Melihat hasil analisis bukanlah tugas yang mudah. Faktanya adalah bahwa penganalisa memeriksa file .i preprocessed , di mana semua arahan preprocessor sudah dibuka, dan menghasilkan peringatan pada file dengan kode sumber. Ini adalah perilaku penganalisa yang benar, Anda tidak perlu mengubah apa pun, tetapi banyak peringatan dikeluarkan untuk makro! Dan di belakang makro adalah kode yang tidak dapat dibaca.

Operator ternary tidak berfungsi seperti yang Anda pikirkan


V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '-'. toke.c 9494

STATIC char * S_scan_ident(pTHX_ char *s, char *dest, STRLEN destlen, I32 ck_uni) { .... if ((s <= PL_bufend - (is_utf8) ? UTF8SKIP(s) : 1) && VALID_LEN_ONE_IDENT(s, PL_bufend, is_utf8)) { .... } .... } 

Mari kita mulai ulasan dengan kesalahan yang indah. Setiap beberapa ulasan kode harus mengulangi bahwa operator ternary hampir memiliki prioritas terendah dalam komputasi.

Pertimbangkan fragmen dengan kesalahan:

 s <= PL_bufend - (is_utf8) ? UTF8SKIP(s) : 1 

Urutan operasi yang diharapkan oleh programmer:
  1. ?:
  2. -
  3. <=

Apa yang sebenarnya terjadi:
  1. -
  2. <=
  3. ?:

Simpan label dengan prioritas operasi: " Prioritas operasi di C / C ++ ."

V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '=='. re_exec.c 9193

 STATIC I32 S_regrepeat(pTHX_ regexp *prog, char **startposp, const regnode *p, regmatch_info *const reginfo, I32 max _pDEPTH) { .... assert(STR_LEN(p) == reginfo->is_utf8_pat ? UTF8SKIP(STRING(p)) : 1); .... } 

Kode sederhana dengan kesalahan serupa. Tetapi jika Anda tidak tahu prioritas operasi, maka Anda dapat membuat kesalahan dalam ekspresi ukuran berapa pun.

Tempat lain dengan menegaskan:

  • V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '=='. re_exec.c 9286

V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '&&'. pp_hot.c 3036

 PP(pp_match) { .... MgBYTEPOS_set(mg, TARG, truebase, RXp_OFFS(prog)[0].end); .... } 

Dan inilah peringatan untuk makro ... Untuk memahami apa yang terjadi di sana, bahkan penerapan makro tidak akan membantu, karena menggunakan beberapa makro lagi!

Oleh karena itu, saya lampirkan sebuah fragmen dari file yang telah diproses untuk baris kode ini:

 (((targ)->sv_flags & 0x00000400) && (!((targ)->sv_flags & 0x00200000) || S_sv_only_taint_gmagic(targ)) ? (mg)->mg_len = ((prog->offs)[0].end), (mg)->mg_flags |= 0x40 : ((mg)->mg_len = (((targ)->sv_flags & 0x20000000) && !__builtin_expect(((((PL_curcop)->cop_hints + 0) & 0x00000008) ? (_Bool)1 :(_Bool)0),(0))) ? (ssize_t)Perl_utf8_length( (U8 *)(truebase), (U8 *)(truebase)+((prog->offs)[0].end)) : (ssize_t)((prog->offs)[0].end), (mg)->mg_flags &= ~0x40)); 

Di suatu tempat di sini, penganalisa meragukan penggunaan yang benar dari operator ternary (ada 3 dari mereka), tetapi saya tidak menemukan kekuatan untuk memahami apa yang sedang dilakukan dalam kode ini. Kami telah melihat bahwa pengembang membuat kesalahan seperti itu, jadi di sini juga sangat mungkin terjadi.

Tiga kegunaan lagi dari makro ini:

  • V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '&&'. pp_ctl.c 324
  • V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '&&'. regexec.c 7335
  • V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '&&'. re_exec.c 7335

Catatan rekan Andrei Karpov. Saya merenungkan kode ini selama 10 menit dan saya cenderung percaya bahwa tidak ada kesalahan. Tetapi bagaimanapun juga, sangat menyakitkan untuk membaca kode seperti itu, dan lebih baik tidak menulis seperti itu.

Kesalahan dalam kondisi


V523 Pernyataan 'lalu' sama dengan pernyataan 'lain'. toke.c 12056

 static U8 * S_add_utf16_textfilter(pTHX_ U8 *const s, bool reversed) { .... SvCUR_set(PL_linestr, 0); if (FILTER_READ(0, PL_linestr, 0)) { SvUTF8_on(PL_linestr); } else { SvUTF8_on(PL_linestr); } PL_bufend = SvEND(PL_linestr); return (U8*)SvPVX(PL_linestr); } 

Saya pikir Anda bisa melakukannya tanpa memeriksa isi makro untuk memastikan bahwa ada duplikat kode yang mencurigakan.

V564 The '|' operator diterapkan pada nilai tipe bool. Anda mungkin lupa menyertakan tanda kurung atau bermaksud menggunakan '||' operator. op.c 11494

 OP * Perl_ck_rvconst(pTHX_ OP *o) { .... gv = gv_fetchsv(kidsv, o->op_type == OP_RV2CV && o->op_private & OPpMAY_RETURN_CONSTANT ? GV_NOEXPAND : iscv | !(kid->op_private & OPpCONST_ENTERED), iscv // <= ? SVt_PVCV : o->op_type == OP_RV2SV ? SVt_PV : o->op_type == OP_RV2AV ? SVt_PVAV : o->op_type == OP_RV2HV ? SVt_PVHV : SVt_PVGV); .... } 

Kode yang sangat aneh Ekspresi "iscv | ! (kid-> op_private & OPpCONST_ENTERED) "tidak digunakan dengan cara apa pun. Jelas ada kesalahan ketik di sini. Sebagai contoh, mungkin Anda harus menulis di sini:

 : iscv = !(kid->op_private & OPpCONST_ENTERED), iscv // <= 

Ekspresi V547 'RETVAL == 0' selalu benar. Typemap.c 710

 XS_EUPXS(XS_XS__Typemap_T_SYSRET_pass); XS_EUPXS(XS_XS__Typemap_T_SYSRET_pass) { dVAR; dXSARGS; if (items != 0) croak_xs_usage(cv, ""); { SysRet RETVAL; #line 370 "Typemap.xs" RETVAL = 0; #line 706 "Typemap.c" { SV * RETVALSV; RETVALSV = sv_newmortal(); if (RETVAL != -1) { // <= if (RETVAL == 0) // <= sv_setpvn(RETVALSV, "0 but true", 10); else sv_setiv(RETVALSV, (IV)RETVAL); } ST(0) = RETVALSV; } } XSRETURN(1); } 

Variabel RETVAL diperiksa dua kali berturut-turut. Selain itu, dapat dilihat dari kode bahwa variabel ini selalu nol. Mungkin dalam satu atau kedua kondisi mereka ingin memeriksa pointer RETVALSV , tetapi mereka membuat kesalahan ketik.

Melempar peringatan tentang ukuran operator


Ada beberapa jenis aturan diagnostik dalam penganalisa yang mencari kesalahan menggunakan operator sizeof . Di proyek Perl 5, dua diagnostik ini menghasilkan total sekitar seribu peringatan. Dalam hal ini, bukan penganalisa yang harus disalahkan, tetapi makro.

V568 Aneh bahwa argumen operator sizeof () adalah ekspresi 'len + 1'. util.c 1084

 char * Perl_savepvn(pTHX_ const char *pv, I32 len) { .... Newx(newaddr,len+1,char); .... } 

Kode ini memiliki banyak makro yang mirip. Saya memilih satu sebagai contoh, kami tertarik pada argumen "len + 1".

Preprocessor makro berkembang menjadi kode berikut:

 (newaddr = ((void)(__builtin_expect(((((( sizeof(size_t) < sizeof(len+1) || sizeof(char) > ((size_t)1 << 8*(sizeof(size_t) - sizeof(len+1)))) ? (size_t)(len+1) : ((size_t)-1)/sizeof(char)) > ((size_t)-1)/sizeof(char))) ? (_Bool)1 : (_Bool)0),(0)) && (S_croak_memory_wrap(),0)), (char*)(Perl_safesysmalloc((size_t)((len+1)*sizeof(char)))))); 

Peringatan analisa dikeluarkan untuk ukuran (len +1) konstruksi. Faktanya adalah bahwa tidak ada perhitungan yang dibuat dalam argumen ukuran operator. Banyak makro diperluas ke kode serupa. Ini mungkin kode warisan lama di mana tidak ada yang mau menyentuh apa pun, tetapi pengembang saat ini terus menggunakan makro lama, menunjukkan perilaku mereka yang berbeda.

Dereferencing null pointer


V522 Dereferencing dari pointer nol 'sv' mungkin terjadi. pp_ctl.c 577

 OP * Perl_pp_formline(void) { .... SV *sv = ((void *)0); .... switch (*fpc++) { .... case 4: arg = *fpc++; f += arg; fieldsize = arg; if (mark < sp) sv = *++mark; else { sv = &(PL_sv_immortals[2]); Perl_ck_warner( (28 ), "...."); } .... break; case 5: { const char *s = item = ((((sv)->sv_flags & (....)) == 0x00000400) ? .... .... } .... } 

Fragmen kode ini sepenuhnya diambil dari file pracroses, karena tidak mungkin untuk memverifikasi keberadaan masalah dari file sumber, sekali lagi karena makro.

Pointer sv diinisialisasi ke nol setelah deklarasi. Penganalisa menemukan bahwa ketika melewati 5 dalam pernyataan switch , pointer ini dereferenced, yang tidak pernah diinisialisasi sebelumnya. Perubahan dalam pointer sv hadir di cabang dengan nilai 4 , tetapi pada akhir blok kode ini adalah pernyataan break . Kemungkinan besar, kode tambahan diperlukan di tempat ini.

V595 Pointer 'k' digunakan sebelum diverifikasi terhadap nullptr. Periksa baris: 15919, 15920. op.c 15919

 void Perl_rpeep(pTHX_ OP *o) { .... OP *k = o->op_next; U8 want = (k->op_flags & OPf_WANT); // <= if ( k // <= && k->op_type == OP_KEYS && ( want == OPf_WANT_VOID || want == OPf_WANT_SCALAR) && !(k->op_private & OPpMAYBE_LVSUB) && !(k->op_flags & OPf_MOD) ) { .... } 

Dalam cuplikan kode ini, penganalisa menemukan pointer k , yang mendereferensi satu baris lebih awal dari pemeriksaan validitasnya. Ini bisa berupa kesalahan atau kode tambahan.

Diagnostik V595 menemukan banyak peringatan dalam proyek apa pun, Perl 5 tidak terkecuali. Tidak mungkin untuk memasukkan semua ini ke dalam artikel, oleh karena itu kami akan membatasi diri pada satu contoh, dan pengembang akan memeriksa proyek sendiri jika mereka mau.

Lain-lain


V779 Kode tidak terjangkau terdeteksi. Mungkin saja ada kesalahan. universal.c 457

 XS(XS_utf8_valid); XS(XS_utf8_valid) { dXSARGS; if (items != 1) croak_xs_usage(cv, "sv"); else { SV * const sv = ST(0); STRLEN len; const char * const s = SvPV_const(sv,len); if (!SvUTF8(sv) || is_utf8_string((const U8*)s,len)) XSRETURN_YES; else XSRETURN_NO; } XSRETURN_EMPTY; } 

Pada baris dengan XSRETURN_EMPTY, alat analisa mendeteksi kode yang tidak dapat dijangkau. Ada dua pernyataan pengembalian dalam fungsi ini dan croak_xs_usage - makro yang diperluas ke fungsi noreturn:

 void Perl_croak_xs_usage(const CV *const cv, const char *const params) __attribute__((noreturn)); 

Di tempat-tempat serupa dalam kode Perl 5, makro NOT_REACHED digunakan untuk menunjukkan cabang yang tidak terjangkau.

V784 Ukuran bit mask kurang dari ukuran operan pertama. Ini akan menyebabkan hilangnya bit yang lebih tinggi. inffast.c 296

 void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { .... unsigned long hold; /* local strm->hold */ unsigned bits; /* local strm->bits */ .... hold &= (1U << bits) - 1; .... } 

Penganalisis mendeteksi operasi yang mencurigakan saat bekerja dengan topeng bit. Sebagai bitmask, variabel resolusi lebih rendah digunakan daripada variabel holding . Ini menghasilkan hilangnya bit tinggi. Pengembang harus memperhatikan kode ini.

Kesimpulan


Gambar 6



Menemukan kesalahan melalui makro sangat sulit. Melihat laporan itu membutuhkan banyak waktu dan upaya. Namun demikian, artikel tersebut memuat kasus-kasus yang sangat menarik yang mirip dengan kesalahan nyata. Laporan analisa cukup besar, pasti ada jauh lebih menarik di sana. Tapi saya tidak bisa melihatnya lebih jauh :). Saya menyarankan pengembang untuk memeriksa proyek sendiri dan menghilangkan cacat yang akan dapat mereka deteksi.

PS Kami pasti ingin mendukung proyek yang menarik ini, dan kami siap memberikan pengembang dengan lisensi selama beberapa bulan.



Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan ke terjemahan: Svyatoslav Razmyslov. Perl 5: Cara Menyembunyikan Kesalahan di Macro

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


All Articles