HotXLS bisa mem-crash sebuah worker thread Delphi tanpa exception yang bisa ditangkap ketika ia melakukan checksum sebuah bagian XML worksheet besar dalam satu pemanggilan: zlib-ng beralih ke algoritma Chorba-nya di atas kira-kira 119 KB input, dan varian generic-C dari algoritma itu mengalokasikan sebuah array scratch yang cukup besar untuk menembus stack thread default 1 MB. Delphi tidak pernah mendapat kesempatan untuk bereaksi, karena sebuah stack overflow bukan jenis exception yang dirancang untuk ditangkap try/except
HotXLS adalah sebuah library native Delphi dan C++Builder untuk membaca dan menulis workbook Excel, dan crash itu ditelusuri kembali ke penulis worksheet-nya. Tanda pertama masalah adalah sebuah tiket dukungan: sebuah job ekspor semalaman crash kira-kira dua kali seminggu, selalu di tengah proses, tanpa dialog exception Delphi dan tanpa error yang tercatat, hanya sebuah proses yang lenyap dan sebuah entri Windows Error Reporting yang tidak menunjuk ke mana pun yang berguna. Mereproduksinya di meja kerja adalah masalah yang sama sekali berbeda. Workbook kecil tersimpan baik-baik saja. Workbook besar juga tersimpan baik-baik saja, selama penyimpanan berjalan di main thread dengan sebuah debugger yang sudah terpasang. Butuh sebuah batch sungguhan file berukuran-produksi yang berjalan lewat jalur ekspor multi-threaded sungguhan untuk membawa crash itu pulang, dan pada titik itu I/O disk, tekanan memori, dan sebuah template yang dicurigai masing-masing sudah dikesampingkan
Bagaimana sebuah penyimpanan worksheet berubah menjadi satu pemanggilan CRC32 raksasa
File XLSX adalah container ZIP, dan format ZIP membutuhkan sebuah checksum CRC-32 untuk setiap entri, dicatat baik di local file header maupun central directory. HotXLS menghitung checksum itu dengan memanggil sebuah wrapper kecil bernama ZLibCRC32, yang gilirannya memanggil rutin crc32 milik zlib-ng sendiri begitu SaveAs selesai merakit XML sebuah worksheet di memori, dan untuk waktu yang lama pemanggilan itu membawa seluruh buffer tak-terkompresi dalam satu invokasi tunggal. Itu adalah desain yang masuk akal untuk sebuah worksheet kecil. Ia menjadi satu pemanggilan yang sangat besar begitu sebuah sheet adalah jenis yang dibahas panduan kami untuk performa workbook besar di HotXLS, di mana XML satu sheet secara rutin berjalan melewati beberapa ratus kilobyte sebelum pernah dikompresi
Mengapa zlib-ng membutuhkan sebuah buffer stack raksasa untuk CRC32?
zlib-ng tidak menggunakan satu implementasi CRC-32 untuk setiap pemanggilan. Di bawah sebuah ambang ukuran ia menelusuri buffer dengan lookup tabel dan trik folding yang tidak membutuhkan memori ekstra yang berarti, dan di atas ambang itu, kira-kira 119 KB, tepatnya 118.960 byte pada build yang di-link HotXLS, ia beralih ke sebuah algoritma cepat khusus bernama Chorba. Implementasi generic-C dari jalur itu menukar memori dengan kecepatan: ia mengalokasikan sebuah array scratch pada stack alih-alih heap, berukuran untuk membuat loop dalam algoritma tersebut cepat, bukan untuk pas dengan nyaman di dalam anggaran stack apa pun yang kebetulan dibawa thread pemanggil. Tak satu pun dari itu terlihat dari sisi pemanggil. Sebuah fungsi checksum normalnya sebuah leaf call, baca beberapa byte, kembalikan sebuah angka, tanpa alokasi yang layak dibahas, dan asumsi itu bertahan untuk mayoritas besar pemanggilan ke zlib-ng persis sampai sebuah buffer cukup besar untuk melewati ambang Chorba masuk ke dalam salah satunya
function BuildWorksheetPartCrc(const XmlBytes: TBytes): LongWord;
begin
// One call over the whole worksheet XML buffer: fine for a small
// sheet, but a large enough input pushes zlib-ng onto its Chorba
// fast path and that path's stack-hungry scratch buffer
Result := ZLibCRC32(0, XmlBytes[0], Length(XmlBytes));
end;
Mengapa worker thread mengalaminya dan debugging interaktif tidak pernah
Memicu crash ini membutuhkan dua kondisi pada saat yang sama: sebuah bagian XML worksheet cukup besar untuk melewati ambang Chorba milik zlib-ng, dan sebuah thread yang hanya memiliki stack default biasa alih-alih sesuatu yang lebih lapang. Job ekspor produksi menabrak keduanya. Mereka berjalan sebagai job batch sisi-server yang menyebarkan penulisan HotXLS ke seluruh pool worker thread, masing-masing membawa stack default 1 MB yang dicadangkan Windows kecuali pemanggil meminta lebih banyak, dan masing-masing memproses workbook pelanggan yang cukup besar untuk berarti. Debugging di meja kerja tidak menabrak keduanya secara andal: file sampel biasanya lebih kecil dari ambang tersebut, dan penjalanan single-step cenderung terjadi di main thread alih-alih di dalam sebuah worker yang baru saja dimunculkan, sehingga kedua kondisi yang harus sejajar dalam produksi nyaris tidak pernah sejajar di meja seorang developer
Mengejar sebuah crash yang menyalahkan fungsi yang salah
Laporan crash yang bisa didapatkan tim menunjuk ke sebuah lokasi di dalam fungsi deflate milik zlib-ng, bukan pada kode HotXLS mana pun, dan juga tidak secara jelas pada kode CRC-32-nya sendiri. Detail tunggal itu mengirim langkah pertama investigasi ke arah jalur kompresi: ukuran buffer yang diserahkan ke deflate, window bits, level kompresi, semua tersangka biasa untuk sebuah crash native yang keluar dari sebuah codec. Tak satu pun dari mereka bertahan
Sebuah frame teratas yang menyesatkan
Sebuah stack overflow adalah jenis crash yang aneh untuk disimbolkan, karena pada saat ia dilaporkan, stack pointer sudah berjalan melewati ruang yang dicadangkan untuknya. Apa pun yang menghasilkan laporan crash itu kemungkinan besar menyelesaikan alamat yang gagal ke simbol terdekat yang masih bisa ditemukannya, dan entry point yang diekspor terdekat yang duduk di sebelah pelaku sebenarnya kebetulan adalah deflate. Fault sesungguhnya duduk di dalam alokasi buffer-scratch Chorba di dalam jalur CRC-32, dikompilasi ke dalam library yang sama, cukup dekat dalam binary untuk disalahkirakan sebagai fungsi yang sebenarnya sedang berjalan
Bisecting dengan timestamp alih-alih debugger
Sebuah crash yang menjatuhkan seluruh proses tidak meninggalkan apa pun untuk ditangkap sesi debugger Delphi normal, sehingga tim jatuh kembali ke checkpoint GetTickCount yang ditempatkan di sekitar setiap pemanggilan yang dicurigai dan sebuah bisection manual di seluruh jalur penyimpanan, mempersempit operasi mana yang sedang berlangsung pada saat proses itu mati. Berdampingan dengan itu, sebuah build baseline yang diketahui-baik menjalankan file produksi yang sama berdampingan dengan yang saat ini, secara khusus untuk mengesampingkan sebuah regresi dalam perubahan ronde itu sendiri sebelum melihat lebih jauh ke hulu. Hanya setelah kedua pemeriksaan kembali bersih investigasi menetap pada sebuah ketergantungan pihak ketiga yang melakukan sesuatu yang tak terduga dengan sebuah input yang sepenuhnya valid
Mengapa try/except gagal menangkap sebuah stack overflow?
Sebuah stack overflow bukan sebuah exception yang pernah dimunculkan kode Delphi dengan sengaja, dan juga tidak dikirimkan dengan cara yang sama Windows mengirimkan sebuah access violation atau sebuah divide-by-zero. Ia muncul sebagai sebuah hardware guard-page fault, dilaporkan lewat mekanisme structured exception handling yang sama yang menjadi dasar try/except milik Delphi, tetapi pada saat persis ia terpicu biasanya tidak ada ruang stack tersisa untuk menjalankan sebuah handler, membatalkan kode cleanup, atau bahkan menyelesaikan pelaporan fault tersebut dengan bersih. Pada sebuah worker thread yang hanya membawa cadangan default 1 MB, dengan sebuah buffer scratch seukuran itu yang sudah mengonsumsi sebagian besar dari apa yang tersisa, tidak ada apa pun yang tersisa untuk dikerjakan runtime tersebut
procedure TExportWorker.Execute;
var
Workbook: TXLSXWorkbook;
begin
Workbook := TXLSXWorkbook.Create;
try
try
BuildWorksheet(Workbook);
Workbook.SaveAs(FTargetFile); // crashes the process here on a
// large enough sheet: try/except
// never gets a chance to run
except
on E: Exception do
LogError('Export failed: ' + E.Message);
end;
finally
Workbook.Free;
end;
end;
Blok except itu terlihat seperti sebuah jaring pengaman, dan terhadap kebanyakan kegagalan memang begitu, tetapi tidak melakukan apa-apa di sini. Tim mengonfirmasi hal itu dalam praktik: try/except tidak menangkap apa pun, blok finally juga tidak pernah mendapat kesempatan andal untuk berjalan, dan operator melihat sebuah proses mati tanpa entri log tingkat-aplikasi sama sekali, persis apa yang dijelaskan tiket dukungan asli
Perbaikannya: memberi CRC32 dalam potongan 64 KB alih-alih satu pemanggilan raksasa
Perbaikan yang dirilis HotXLS tidak mengubah apa pun tentang zlib-ng itu sendiri dan tidak apa pun tentang level kompresi yang digunakan untuk menulis workbook. ZLibCRC32 sekarang menelusuri input dalam potongan tetap 64 KB, 65536 byte masing-masing, memanggil crc32 milik zlib-ng sekali per potongan dan merangkai nilai checksum yang berjalan dari satu pemanggilan ke berikutnya. CRC-32 adalah sebuah algoritma inkremental secara konstruksi, sehingga sebuah checksum yang dibangun di atas beberapa potongan identik bit-demi-bit dengan satu yang dihitung dalam satu pemanggilan tunggal atas byte yang sama: perbaikan ini mengubah bagaimana pekerjaan itu dibagi, bukan apa yang dihitungnya
function ZLibCRC32(crc: LongWord; const buffer; count: Longint): LongWord;
const
// 64 KB keeps every call comfortably under the Chorba threshold
CrcChunkSize = 65536;
var
Cursor: PByte;
ThisChunk: Longint;
begin
Result := crc;
Cursor := PByte(@buffer);
while count > 0 do
begin
ThisChunk := count;
if ThisChunk > CrcChunkSize then
ThisChunk := CrcChunkSize;
Result := zng_crc32(Result, Cursor, Cardinal(ThisChunk));
Inc(Cursor, ThisChunk);
Dec(count, ThisChunk);
end;
end;
Tidak ada apa pun tentang pemanggilan SaveAs di sekitarnya yang perlu berubah agar ini bekerja, dan tidak ada apa pun tentang entri ZIP yang ditulis HotXLS yang berubah juga: nilai CRC-32 yang berakhir di local file header dan central directory persis sama dengan nilai yang akan dihasilkan satu pemanggilan raksasa, hanya dirakit dari potongan yang lebih kecil. Menurunkan versi zlib-ng atau jatuh kembali ke sebuah implementasi CRC-32 yang lebih lambat dan ringan-alokasi juga akan menghindari crash tersebut, tetapi dengan biaya sungguhan bagi setiap file yang bahkan tidak pernah mendekati ambang tersebut sejak awal, itulah sebabnya tak satu pun dari keduanya yang dirilis
Apa artinya ini jika Anda memanggil zlib-ng dari worker thread Anda sendiri
Mode kegagalan stack overflow yang dijelaskan di sini tidak ada hubungannya dengan spreadsheet secara khusus. Aplikasi apa pun yang menyerahkan sebuah buffer besar ke zlib-ng, baik untuk kompresi, dekompresi, atau sebuah checksum, dari sebuah thread yang hanya membawa stack default platform bisa menabrak jenis tembok yang sama, karena library itu memilih algoritmanya berdasarkan ukuran input dan beberapa algoritma itu mengasumsikan ada stack yang bisa disisakan. Dua pertahanan bekerja tanpa menyentuh zlib-ng itu sendiri: memberi buffer besar ke dalam rutin yang sensitif-ukuran dalam potongan tetap sepenuhnya menghilangkan kondisi pemicu untuk algoritma apa pun yang secara alami inkremental, dan di mana chunking bukan sebuah pilihan, memberikan thread pemanggil sebuah stack yang lebih besar dari default platform adalah tuas lainnya. Salah satunya lebih murah daripada mengetahui tentang sebuah ambang ukuran yang tidak terdokumentasi dari sebuah laporan crash produksi yang menyalahkan fungsi yang salah
Ambang khusus ini tetap tak terlihat sampai sebuah workbook produksi yang cukup besar melewatinya pada jenis thread yang salah, yang persis merupakan jenis kegagalan yang hanya muncul begitu kode berjalan terhadap file sungguhan alih-alih fixture kecil. Jalur CRC-32 yang dipotong sekarang disertakan sebagai bagian dari pipeline penulisan standar di HotXLS Excel Component untuk Delphi dan C++Builder, tanpa apa pun untuk dikonfigurasi pemanggil dan tanpa properti yang mengaktifkan atau menonaktifkannya