Artikel Teknis

FillChar pada Hasil Fungsi Membocorkan String di Object Pascal

Sebuah fungsi Delphi atau FPC yang mengembalikan sebuah record tidak mendapatkan Result yang baru dan ternolkan pada setiap pemanggilan. Variabel Result tersembunyi itu dimulai dari nol tepat satu kali, dan tidak ada apa pun yang menolkannya ulang secara otomatis di antara pemanggilan, sehingga membersihkannya saat masuk adalah pekerjaan fungsi itu sendiri. Lakukan pembersihan itu dengan FillChar(Result, SizeOf(Result), 0) dan, mulai dari pemanggilan kedua, rutin itu menimpa sebuah referensi string atau dynamic-array yang hidup alih-alih melepaskannya, membuat yatim piatu blok heap apa pun yang ditunjuk referensi itu

Skenario tempat ini menggigit itu remeh. Sebuah proses batch membuka setumpuk PDF pihak ketiga dan menelusuri setiap anotasi pada setiap halaman, menarik teks komentar ke dalam sebuah log audit. Tidak ada apa pun tentang loop itu yang terlihat berbahaya: setiap pemanggilan adalah sebuah fungsi polos yang mengembalikan sebuah record polos, tidak ada pointer terlihat, tidak ada apa pun yang menyerupai manajemen memori manual sama sekali. Reference counting di dalam sebuah record adalah sebuah aturan akuntansi Object Pascal biasa, bukan sebuah keanehan khusus untuk library tertentu, dan codebase Delphi atau FPC mana pun yang mencampur FillChar dengan tipe record yang memegang string atau dynamic array terekspos pada defek yang sama

Mengapa FillChar pada Hasil Record Membocorkan String?

FillChar membocorkan string karena tidak memiliki gambaran apa pun tentang jenis data yang ditimpanya. FillChar(X, Count, Value) bekerja pada variabel apa pun sama sekali: ia mengambil sebuah blok untyped berisi Count byte dan mencap setiap satunya dengan Value, dan itulah seluruh kontraknya. Inilah persis yang membuat FillChar cepat dan serba-guna, karena ia tidak pernah memeriksa tipe X dan tidak pernah bercabang berdasarkan apa arti byte yang mendasarinya. Sebuah field UnicodeString atau WideString di dalam sebuah record bukan karakternya sendiri; ia adalah sebuah pointer ke sebuah blok heap yang membawa sebuah reference count di depan data karakternya. FillChar melihat segelintir byte yang kebetulan memegang sebuah nilai pointer dan menimpanya dengan nol persis seperti ia akan menimpa sebuah field Integer atau Double. Pointer itu menghilang, reference count yang seharusnya diturunkan lebih dulu tidak pernah tersentuh, dan blok yang ditunjuknya duduk teralokasi tanpa apa pun tersisa yang merujuk padanya

Bagaimana Compiler Melacak String dan Dynamic Array di Dalam Sebuah Record

Object Pascal menyebut sebuah tipe terkelola ketika compiler harus menjalankan kode ekstra untuk menjaganya tetap benar melintasi assignment dan keluar-lingkup. Tipe long string seperti AnsiString, UnicodeString, dan WideString memenuhi syarat, begitu pula dynamic array, interface, dan Variant, beserta record atau fixed array apa pun yang mengandung salah satunya sebagai sebuah field. Untuk setiap field terkelola, compiler diam-diam memancarkan pembukuan yang jika tidak akan merepotkan dan mudah salah dilakukan dengan tangan: menaikkan sebuah reference count saat assignment, menurunkannya ketika variabel yang memegangnya ditimpa atau keluar lingkup, dan membebaskan blok yang mendasarinya begitu hitungan itu mencapai nol. Mesin itulah yang membuat kode Pascal biasa tidak pernah secara manual mengalokasikan atau melepaskan sebuah string, dan mengapa menetapkan satu dynamic array ke yang lain adalah operasi murah dan aman alih-alih sebuah loop penyalinan manual. System.Default dan Finalize adalah dua cara terdokumentasi untuk memanggil logika pelepasan yang sama itu sesuai permintaan, dan itulah yang seharusnya dipanggil kode pembersihan sebuah record alih-alih sebuah fill memori mentah

type
  TLineItem = record
    Description: string;  // managed: reference-counted
    Quantity: Integer;    // unmanaged: plain ordinal
  end;

function GetLineItem(Index: Integer): TLineItem;
begin
  FillChar(Result, SizeOf(Result), 0);  // clears bytes, not the reference
  Result.Quantity := Source[Index].Qty;
  Result.Description := Source[Index].Text;
end;

var
  Item: TLineItem;
  I: Integer;
begin
  for I := 0 to High(Source) do
  begin
    Item := GetLineItem(I);  // second pass onward: leaks the prior Description
    Log.Add(Item.Description);
  end;
end;

Mengapa Kebocoran Hanya Dimulai pada Pemanggilan Kedua?

Pemanggilan pertama dalam sebuah loop selalu tidak berbahaya, dan itulah persis yang membuat defek ini mudah terlewat dalam pengujian. Sebuah variabel lokal bertipe record terkelola dimulai dari nol, dan tidak ada apa pun yang menolkannya ulang secara otomatis di antara satu langkah loop dan berikutnya, sehingga kali pertama sebuah loop menetapkan nilai kembali sebuah fungsi ke dalam variabel itu, field Description atau ContentsText-nya masih nil. FillChar menimpa nil dengan nol, yang tidak mengubah apa pun sejauh reference count peduli, dan pemanggilan itu kembali terlihat sepenuhnya benar. Pemanggilan kedua berbeda: variabel lokal yang sama itu sudah memegang apa pun yang ditulis pemanggilan pertama ke dalamnya, dan Result pemanggilan baru itu ditulis langsung ke dalam penyimpanan yang sama itu alih-alih ke dalam memori baru dan kosong. FillChar di puncak pemanggilan kedua itu menolkan sebuah field yang tidak lagi nil, dan segala sesuatu di hilir pola byte itu diam-diam salah sejak saat itu. Sebuah test yang memanggil fungsi itu sekali dan memeriksa hasilnya tidak akan pernah melihat masalah tersebut; hanya sebuah loop, atau jalur kode apa pun yang memanggil fungsi itu berulang kali terhadap tujuan yang sama, yang mengungkapkannya

Sebuah Kebocoran Sungguhan: Anotasi, Bookmark, dan Record Link

PDFiumPas mengirimkan persis defek ini sebelum versi 1.56.4, dalam tiga fungsi yang masing-masing mengembalikan sebuah record yang memegang setidaknya satu field terkelola: reader anotasi tingkat-halaman mengembalikan sebuah TPdfAnnotation yang membawa string ContentsText dan AuthorText, reader bookmark mengembalikan sebuah TBookmark yang membawa sebuah string Title, dan reader link-annotation mengembalikan sebuah TLinkAnnotation yang membawa sebuah string ActionPath dan sebuah dynamic array Points. Ketiganya dibuka dengan bentuk yang sama yang ditunjukkan di bawah: bersihkan Result dengan sebuah FillChar mentah, lalu isi field satu per satu dari data halaman yang mendasarinya. Menelusuri setiap anotasi pada sebuah halaman satu per satu, cara biasa membangun sebuah daftar audit atau sebuah panel review, memanggil reader anotasi dalam sebuah loop dan membocorkan teks anotasi sebelumnya pada setiap langkah setelah yang pertama; sebuah PDF yang dirancang dengan jumlah anotasi pembawa-teks yang tidak biasa besar bisa menumbuhkan memori sebuah proses berjalan-lama selama proses itu terus berjalan. Perbaikannya menyentuh satu baris dalam setiap fungsi: mengganti FillChar(Result, SizeOf(Result), 0) dengan Result := Default(TPdfAnnotation) sudah cukup, karena menetapkan Default ke sebuah record terkelola menjalankan urutan pelepasan-lalu-bersihkan biasa milik compiler alih-alih sebuah fill memori mentah

function GetPageAnnotation(Page: FPDF_PAGE; Index: Integer): TPdfAnnotation;
var
  Annotation: FPDF_ANNOTATION;
  ContentLength: LongWord;
begin
  Annotation := FPDFPage_GetAnnot(Page, Index);
  FillChar(Result, SizeOf(Result), 0);   // clears bytes, not a live reference
  Result.Subtype := DecodeAnnotationSubtype(FPDFAnnot_GetSubtype(Annotation));
  ContentLength := FPDFAnnot_GetStringValue(Annotation,
    FPDFANNOT_TEXTTYPE_Contents, nil, 0);
  if ContentLength >= 4 then
  begin
    SetLength(Result.ContentsText, ContentLength div 2 - 1);
    FPDFAnnot_GetStringValue(Annotation, FPDFANNOT_TEXTTYPE_Contents,
      Pointer(Result.ContentsText), ContentLength);
  end;
end;

Bahaya yang Sama di Balik Sebuah Parameter var

Reader bookmark menunjukkan sebuah versi lebih halus dari masalah yang sama, karena record yang dibersihkan dengan FillChar bukan Result fungsi itu sendiri tetapi sebuah parameter var satu pemanggilan lebih dalam. SetBookmarkData menerima outputnya sebagai var Data: TBookmark dan dulunya membersihkan Data di puncak body-nya dengan FillChar; GetBookmark, fungsi publik yang benar-benar mengembalikan sebuah TBookmark, memanggil SetBookmarkData dan meneruskan Result-nya sendiri langsung sebagai argumen var itu. Sebuah parameter var diteruskan by reference, sehingga Data di dalam SetBookmarkData dan Result di dalam GetBookmark adalah penyimpanan yang sama di bawah dua nama, dan risiko aliasing apa pun yang berlaku pada Result fungsi itu sendiri berlaku sama langsungnya pada rutin helper mana pun yang menerimanya by reference. Meninjau hanya fungsi yang secara literal mendeklarasikan sebuah tipe kembali record melewatkan bentuk ini; pencariannya harus mengikuti setiap parameter var dan out yang diteruskan sebuah Result ke dalamnya juga

procedure TPdf.SetBookmarkData(Bookmark: FPDF_BOOKMARK; var Data: TBookmark);
var
  BufferSize: LongWord;
begin
  Data := Default(TBookmark);   // fixed: was FillChar(Data, SizeOf(Data), 0)
  Data.Handle := Bookmark;
  if Bookmark <> nil then
  begin
    BufferSize := FPDFBookmark_GetTitle(Bookmark, nil, 0);
    if BufferSize >= 4 then
    begin
      SetLength(Data.Title, BufferSize div 2 - 1);
      FPDFBookmark_GetTitle(Bookmark, PWideChar(Data.Title), BufferSize);
    end;
  end;
end;

function TPdf.GetBookmark(const Title: WString): TBookmark;
begin
  CheckActive;
  SetBookmarkData(FPDFBookmark_Find(FDocument, PWideChar(Title)), Result);
end;

Kapan FillChar Masih Pemanggilan yang Tepat?

FillChar tetap benar, dan seringkali sedikit lebih murah, untuk sebuah record yang sepenuhnya dibangun dari ordinal, field floating-point, fixed-size array dari itu, atau record polos lain yang terbuat dari hal yang sama, karena tidak ada apa pun di dalamnya yang perlu di-finalize compiler. Tipe rectangle milik PDFiumPas sendiri persis kasus itu: TPdfRectangle memegang empat field Double dan tidak ada lagi, dan membersihkan satu dengan FillChar tidak melepaskan apa pun karena tidak ada apa pun yang ter-reference-count untuk dilepaskan. Pemeriksaan yang memisahkan kedua kasus itu sederhana untuk dinyatakan: apakah field mana pun dari record itu, pada kedalaman nesting mana pun, bertipe string, AnsiString, WideString, sebuah dynamic array, sebuah interface, atau sebuah Variant? Sebuah record bisa terlihat sepenuhnya numerik pada tingkat teratas dan tetap gagal tes itu jika salah satu field-nya sendiri sebuah record yang mengubur sebuah string beberapa lapis di bawahnya, sehingga pemeriksaan itu harus mengikuti record bersarang sepenuhnya alih-alih berhenti pada daftar field terluar. Mengaudit sebuah codebase yang ada untuk pola ini bersifat mekanis alih-alih menyeluruh: cari setiap pemanggilan FillChar yang targetnya sebuah variabel record, lalu periksa daftar field record itu terhadap daftar tipe-terkelola di atas. Audit v1.56.4 milik PDFiumPas sendiri menjalankan persis pencarian itu di seluruh library dan menemukan eksposur ini dalam satu unit; setiap titik pemanggilan FillChar lainnya sudah membersihkan sebuah record numerik polos, di mana FillChar dulu, dan tetap, alat yang tepat

Perilaku compiler yang sama yang membuat sebuah Result yang digunakan ulang berbahaya di sini juga mendorong sebuah keluarga terkait perbedaan-pendapat Delphi-versus-FPC di tempat lain dalam codebase ini; sebuah artikel pendamping tentang jebakan lintas-compiler membahas sebuah kasus di mana FPC dan Delphi tidak sepakat persis kapan sebuah temporary hasil-record di-finalize di dalam satu ekspresi tunggal, sebuah gejala berbeda dari fakta mendasar yang sama bahwa Result record sebuah fungsi tidak selalu penyimpanan baru dan privat sebagaimana terlihat. Loop anotasi yang digunakan sebagai contoh berjalan sepanjang artikel ini juga bukan hipotetis: itu adalah penelusuran halaman-demi-halaman yang sama yang akan Anda tulis saat membangun sebuah panel review anotasi, yang persis merupakan bentuk kode yang mengubah satu baris FillChar menjadi kebocoran memori lambat sejak awal

Tak satu pun dari ini membutuhkan penggantian library atau mengejar sebuah bug dalam kode terkompilasi milik orang lain: ini adalah sebuah sifat bahasa Object Pascal itu sendiri, satu yang dikerjakan setiap developer Delphi dan FPC setiap hari, dan perbaikannya adalah satu pemanggilan fungsi tunggal begitu Anda tahu apa yang dicari. API anotasi, bookmark, dan link-annotation yang dijelaskan di sini disertakan sebagai bagian dari PDFium Component untuk Delphi, C++Builder, dan Lazarus/FPC, berdampingan dengan sisa permukaan pembacaan, rendering, dan anotasi PDF yang dibahas di tempat lain di blog ini