Tentang kebocoran GDI dan pentingnya keberuntungan


Pada Mei 2019, saya diminta untuk melihat bug Chrome yang berpotensi berbahaya. Pada awalnya, saya mendiagnosis dia tidak penting, buang-buang dengan cara ini dua minggu. Kemudian, ketika saya kembali ke penyelidikan, itu berubah menjadi penyebab nomor satu proses browser macet di saluran beta Chrome. Ups

6 Juni, hari yang sama ketika saya menyadari kesalahan saya dalam menafsirkan data keberangkatan, bug itu ditandai sebagai ReleaseBlock-Stable. Ini berarti bahwa kami tidak akan dapat merilis versi baru Chrome untuk sebagian besar pengguna sampai kami mengetahui apa yang sedang terjadi.

Kecelakaan terjadi karena kami kehabisan objek GDI (Graphics Device Interface) , tetapi kami tidak tahu apa jenis objek GDI mereka, data diagnostik tidak memberikan petunjuk tentang di mana masalahnya, dan kami tidak dapat membuatnya kembali.

Banyak orang dari tim kami bekerja keras pada bug ini pada 6-7 Juni, mereka menguji teori mereka, tetapi tidak maju. Pada 8 Juni, saya memutuskan untuk memeriksa email saya, dan Chrome segera macet. Itu kegagalan yang sama .

Ironis sekali. Sementara saya mencari perubahan dan memeriksa laporan kerusakan, mencoba mencari tahu apa yang dapat menyebabkan proses browser Chrome membocorkan objek GDI, jumlah objek GDI di browser saya terus naik, dan pada pagi hari 8 Juni itu melampaui angka ajaib 10.000 . Pada titik ini, salah satu operasi alokasi memori untuk objek GDI gagal dan kami sengaja menutup browser. Itu adalah keberuntungan yang luar biasa.

Jika Anda dapat mereproduksi bug, maka Anda dapat memperbaikinya. Saya hanya harus mencari tahu bagaimana saya menyebabkan bug ini, setelah itu kita bisa menghilangkannya.

Sebagai permulaan, sejarah singkat masalah ini



Di sebagian besar tempat dalam kode Chromium, ketika kami mencoba mengalokasikan memori untuk objek GDI, kami pertama-tama memeriksa apakah alokasi ini berhasil. Jika tidak mungkin mengalokasikan memori, maka kami menulis beberapa informasi ke stack dan sengaja melakukan crash, seperti yang dapat dilihat pada kode sumber ini . Kegagalan disebabkan secara sengaja, karena jika kita tidak dapat mengalokasikan memori untuk objek GDI, maka kita tidak akan dapat merender di layar - lebih baik melaporkan masalah (jika laporan kerusakan diaktifkan) dan memulai kembali proses daripada menampilkan UI kosong. Secara default, Anda dapat membuat sebanyak 10.000 objek GDI per proses, dan biasanya hanya beberapa ratus yang digunakan. Karena itu, jika kita melampaui batas ini, maka ada yang salah.

Ketika kami mendapatkan salah satu laporan kerusakan yang mengatakan kesalahan alokasi memori untuk objek GDI, kami memiliki tumpukan panggilan dan segala macam informasi berguna lainnya. Baik! Tetapi masalahnya adalah bahwa dump crash seperti itu belum tentu terkait dengan bug. Ini karena kode yang menyebabkan kebocoran objek GDI dan kode yang melaporkan kegagalan mungkin bukan kode yang sama.

Artinya, secara kasar, kami memiliki dua jenis kode:

membatalkan GoodCode () {
   auto x = AllocateGDIObject ();
   jika (! x)
     CollectGDIUsageAndDie ();
   UseGDIObject (x);
   FreeGDIObject (x);
}

membatalkan BadCode () {
   auto x = AllocateGDIObject ();
   UseGDIObject (x);
}

Kode yang baik memperhatikan bahwa alokasi memori gagal, dan melaporkan hal ini, dan kode yang buruk mengabaikan crash dan menumpahkan benda, sehingga "mengganti" kode yang baik sehingga mengambil tanggung jawab.

Chromium berisi beberapa juta baris kode. Kami tidak tahu fungsi mana yang memiliki kesalahan, dan bahkan tidak tahu jenis objek GDI yang bocor. Salah satu kolega saya menambahkan kode yang melewati Blok Lingkungan Proses sebelum kecelakaan untuk mendapatkan jumlah objek GDI dari setiap jenis, tetapi untuk semua jenis yang disebutkan (konteks perangkat, area, bitmap, palet, kuas, bulu, dan tidak diketahui) jumlahnya tidak melebihi seratus. Ini aneh.

Ternyata objek yang kami mengalokasikan memori secara langsung ada di tabel ini, tetapi tidak ada objek yang dibuat oleh kernel atas nama kami, dan mereka ada di suatu tempat di manajer objek Windows. Ini berarti bahwa GDIView sama buta dengan masalah ini seperti kita (selain itu, GDIView hanya berguna ketika memainkan kegagalan secara lokal). Karena kami telah membocorkan kursor, dan kursor adalah objek USER32 dengan objek GDI melekat padanya; memori untuk objek GDI ini dialokasikan oleh kernel, dan kami tidak dapat melihat apa yang terjadi.

Salah tafsir


Fungsi kami CollectGDIUsageAndDie memiliki nama yang sangat jelas, dan saya pikir Anda akan setuju dengan saya dalam hal ini. Sangat mahal.

Masalahnya adalah ia melakukan terlalu banyak tindakan. CollectGDIUsageAndDie memeriksa sekitar selusin jenis kegagalan alokasi memori untuk objek GDI, dan sebagai hasil penyisipan kode, mereka menerima tanda tangan kegagalan yang sama sebagai hasilnya - mereka semua menabrak fungsi utama dan bergabung bersama. Oleh karena itu, salah satu kolega saya dengan bijak melakukan perubahan , memecah cek yang berbeda menjadi fungsi yang terpisah (bukan bawaan). Berkat ini, sekarang, pada pandangan pertama, kita bisa memahami cek mana yang berakhir dengan kegagalan.

Sayangnya, ini mengarah pada fakta bahwa ketika kami mulai mendapatkan laporan kerusakan dari CrashIfExcessiveHandles, Saya dengan yakin mengatakan: "ini bukan penyebab kegagalan, itu hanya disebabkan oleh perubahan tanda tangan."

Tapi saya salah. Ini adalah penyebab kegagalan dan perubahan tanda tangan. Ups Analisis canggung, Dawson. Tidak ada cookie untuk Anda.

Kembali ke kisah kita


Pada titik ini, saya sudah tahu bahwa sesuatu yang saya lakukan pada 7 Juni menggunakan hampir 10.000 objek GDI per hari. Jika saya bisa mengerti itu, saya akan memecahkan teka-teki itu.


Windows Task Manager memiliki kolom objek GDI tambahan yang dapat Anda gunakan untuk menemukan kebocoran. Pada 7 Juni, saya bekerja dari rumah, menghubungkan ke mesin kerja saya, dan kolom ini dinyalakan pada mesin kerja karena saya menjalankan tes dan mencoba mereproduksi skenario kecelakaan. Tetapi sementara itu, ada kebocoran benda GDI di browser di mesin rumah saya .

Tugas utama saya menggunakan browser di rumah adalah untuk terhubung ke mesin yang berfungsi menggunakan aplikasi Chrome Remote Desktop (CRD) . Jadi saya menyalakan kolom objek GDI di mesin rumah dan mulai bereksperimen. Segera saya mendapat hasilnya.

Faktanya, garis waktu bug menunjukkan bahwa dari saat "Saya mengalami kegagalan" (14:00) hingga "entah bagaimana terhubung dengan CRD", dan kemudian ke "case in cursors" hanya 35 menit berlalu. Saya sudah mengatakan betapa mudahnya untuk menyelidiki bug saat Anda dapat memutarnya secara lokal?

Ternyata setiap kali aplikasi CRD (atau aplikasi Chrome apa pun?) Mengubah kursor, ini menyebabkan kebocoran enam objek GDI. Jika Anda memindahkan mouse ke bagian layar yang diinginkan saat bekerja dengan Chrome Remote Desktop, ratusan objek GDI per menit dan ribuan per jam dapat bocor.

Setelah sebulan tidak ada kemajuan dalam menyelesaikan masalah ini, tiba-tiba berubah dari yang tidak dapat diperbaiki menjadi koreksi sederhana. Saya segera menulis perbaikan konsep, dan kemudian salah satu rekan saya (saya tidak mengerjakan bug ini) membuat perbaikan yang nyata. Itu diunduh pada 10 Juni pukul 11:16, dan dirilis pada 13:00. Setelah beberapa penggabungan, bug menghilang.

Itu saja?


Kami memperbaiki bug, dan itu hebat, tetapi jauh lebih penting bahwa bug seperti itu tidak pernah terjadi lagi. Jelas, sudah benar untuk menggunakan objek C ++ ( RAII ) untuk manajemen sumber daya , tetapi dalam kasus ini bug itu terkandung dalam kelas WebCursor.

Ketika datang ke kebocoran memori, ada satu set sistem yang dapat diandalkan. Microsoft memiliki tumpukan foto , Chromium memiliki profil tumpukan untuk versi pengguna dan penghapus kebocoranpada mesin uji. Tetapi tampaknya kebocoran benda-benda GDI telah kehilangan perhatian. Blok Informasi Proses berisi informasi yang tidak lengkap, beberapa objek GDI dapat didaftar hanya dalam mode kernel, dan tidak ada titik tunggal untuk mengalokasikan dan membebaskan memori untuk objek yang dapat memfasilitasi pelacakan. Ini bukan kebocoran pertama dari objek GDI yang harus saya tangani, dan itu tidak akan menjadi yang terakhir, karena tidak ada cara yang dapat diandalkan untuk melacaknya. Berikut adalah rekomendasi saya untuk rilis Windows berikut:

  • Jadikan proses mendapatkan jumlah semua jenis objek GDI sepele, tanpa harus membaca PEB secara tidak jelas (dan tanpa mengabaikan kursor)
  • Buat cara yang didukung untuk mencegat dan melacak semua operasi pembuatan dan penghancuran objek GDI untuk pelacakan yang andal; termasuk untuk mereka yang diciptakan secara tidak langsung
  • Renungkan semua ini dalam dokumentasi

Itu saja. Pelacakan semacam itu bahkan tidak sulit untuk diimplementasikan, karena objek GDI harus dibatasi sedemikian rupa sehingga memori tidak terbatas. Akan lebih bagus jika menggunakan objek GDI yang aneh tapi tak terhindarkan ini akan lebih aman. Bisa aja.

Di sini Anda dapat membaca diskusi tentang Reddit. Topik di Twitter dimulai di sini .

All Articles