PDF Library for Delphi, CCITT, TIFF, PNG, Flate ve akış arabelleği kodlarını Free Pascal üzerinde çalıştırırken beş kod çözücü kusuru buldu ve bunların her biri yıllardır Delphi test paketinin tamamından geçiyordu. Hiçbiri derleyici hatası değildi. Her biri, Delphi'nin bir uygulama ayrıntısı sayesinde doğru yürüttüğü Pascal koduydu: çağıranın dizisini takma adla paylaşan gizli bir sonuç parametresi, kimsenin ötesini okumadığı aralık dışı bir dal, tek koruması aralık denetimi anahtarı olan sıfır uzunluklu bir arabellek, yalnızca tek bir kod yolunun 1 diye geçtiği 1 tabanlı bir ofset ve bellek içi akışların hiç sınamadığı bir TStream.Read sözleşmesi. Derleyiciyi değiştirin ya da aynı koda bozuk bir dosya verin, o tesadüf ayakta kalmaz
Aşağıda her birinin somut şekli, düzeltmesi ve bunlardan çıkan disiplin var: aynı kaynak artık her iki derleyicide de aynı belge semantiğini üretmek zorunda ve bir test include dosyası bunu denetliyor. Bir Pascal PDF ayrıştırıcısını kötü niyetli dosyalara karşı sağlamlaştırma yazısı tamsayı genişliğini, özyineleme derinliğini ve başlatılmamış arabellekleri ele almıştı. Bu yazı ise bambaşka bir hata sınıfıyla ilgili: baştan beri yanlış olan ve arkasını sessizce bir derleyicinin topladığı kod
Dinamik dizi döndüren bir fonksiyon Delphi'de neden SetLength olmadan çalışır?
Çünkü Delphi gizli sonuç parametresi olarak çağıranın kendi değişkenini geçirir; böylece sonucunu hiç ayırmayan bir fonksiyon yine de çağıranın ayırdığı diziye yazabilir. TPLCCITTDecoder.GetNextChangingElement(a0: Integer; IsWhite: Boolean): TCCITTIntegerArray, iki boyutlu Group 3 ve Group 4 çözmenin kalbindeki referans satırı aramasıdır: geçerli konum a0 ve geçerli koşunun rengi verildiğinde önceki tarama satırının değişen elemanlarını, yani ITU-T T.4 ve T.6 iki boyutlu kodlama şemasının b1 ve b2'sini arar ve bunları iki gözlü bir dizi olarak döndürür. Özgün fonksiyon Result[0] ve Result[1] yazıyor, Result üzerinde hiç SetLength çağırmıyordu
Bu, ilk yazmada hata vermeliydi ve Free Pascal'da veriyor. Delphi'de ise hiç vermedi, çünkü çözücüdeki iki çağrı yeri de şöyle görünür: b: TCCITTIntegerArray bildirilir, tarama satırı döngüsünden önce bir kez SetLength(b, 2) çalıştırılır, sonra döngü içinde b := GetNextChangingElement(a0, IsWhite) atanıp b[0] ve b[1] okunur. Delphi dil kılavuzu, sonucu uzun dize, dinamik dizi ya da başka bir yönetilen tür olan bir fonksiyonun bu sonucu ek bir var parametresi olarak aldığını söyler ve pratikte derleyici atama hedefinin adresini geçirir. Yani fonksiyonun içindeki Result, zaten iki elemanlı olan b'nin ta kendisidir ve her yazma çağıranın sahip olduğu belleğe düşer. Free Pascal ise fonksiyona taze bir nil dizi verip sonucu sonradan b'ye atar; bu, kodun baştan yazılması gereken sözleşme okumasıdır
Takma ad paylaşımı ayrıca çözücünün bağımlı olduğu bir semantik taşıyordu. Result[0] yalnızca tarama a0'dan büyük bir eleman bulduğunda, Result[1] ise yalnızca ondan sonra bir eleman olduğunda atanır; dolayısıyla arama kaçtığında gözler, önceki yinelemenin b içinde bıraktığı şeyi korur. Bariz düzeltme olan iki göz ayırıp her çağrıda sıfırlamak, bu devralmayı yok eder ve Delphi'de çözülen çıktıyı değiştirirdi. Sevk edilen düzeltme sıfırlama değil bir korumadır: Delphi'de ölü koddur ve çözme yolu bayt bayt eskisi gibi kalır; Free Pascal'da ise bir hatayı amaçlanan davranışa çevirir. Bu asimetri işin tamamıdır, çünkü düzeltmenin, kodun zaten doğrulanmış çıktı ürettiği derleyicide no-op olması gerekiyordu
Function TPLCCITTDecoder.GetNextChangingElement(a0: Integer;
IsWhite: Boolean): TCCITTIntegerArray;
Begin
// Delphi buraya çağıranın iki elemanlı dizisinin takma adıyla
// Result olarak gelir, yani bu satır orada no-op. FPC nil ile gelir.
If (Length(Result) < 2) Then
SetLength(Result, 2);
...
// Result[0] / Result[1] yine yalnızca isabet olduğunda yazılır,
// böylece kaçırma önceki yinelemenin değerlerini aynen korur
End;
Verisinden uzun yaşayan bir sayım: TIFF dizin girdisi
Bir diziyi geçersiz kıldığınızda sayımını da aynı ifadede geçersiz kılmalısınız, aksi hâlde sayıma, diziyi hiç görmeyen kod inanır. Bir TIFF görüntü dosyası dizin girdisi (TIFF 6.0 §2, tag, type, count ve value-or-offset alanlarından oluşan 12 baytlık düzen) doğrudan dosyadan gelen 32 bitlik bir sayım taşır ve PDF Library for Delphi her birini PopDE: TTIFFEntry üzerinden okur; bu kayıtta Tag, TagType, Length, Offset ile çözülmüş IntegerValues ve DoubleValues dizileri bulunur. Özgün kod Offset + TypeSize * Length toplamının dosyanın sonunu aşıp aşmadığını kontrol ediyor, aşıyorsa iki diziyi de sıfır uzunluğa ayarlıyordu. Result.Length ise dosyadan gelen değerde kalıyordu
Oradan sonra iki şey ters gitti. Fonksiyon, "if Length sıfırsa girdiye sıfır değerli bir eleman ver" diyen bir yedek dala sahipti; böylece çağıranlar sıfırıncı elemanı her zaman okuyabilirdi. Length aralık dışı yolunda hiç temizlenmediği için bu yedek, tam da var olma sebebi olan durumda hiç çalışmadı. Çağıranlar ise sıfırıncı elemanı koşulsuz okur: Width, Height, BitsPerSample, PhotometricInterpretation, FillOrder, SamplesPerPixel, RowsPerStrip ve bir düzine kadarı E.IntegerValues[0] alır; şerit tabloları da Move(E.IntegerValues[0], StripOffsets[0], E.Length * 4) yaparak hiç elemanı olmayan bir diziden Length çarpı dört bayt kopyalar. Temizlenmiş bir dizi ile canlı bir sayım, denetlenmemiş bir diziden kesinlikle daha tehlikelidir, çünkü denetlenmemiş olan en azından iddia ettiği baytları tutar
İkinci sorun sıralamaydı. İki SetLength çağrısı aralık testinden önce çalışıyor ve dosyadaki sayıma göre boyutlanıyordu; yani düşmanca bir girdi, tek bir geçerlilik kontrolünden önce çok gigabaytlık bir ayırma talep edebilirdi. Delphi'de ortaya çıkan istisna, görüntü yükleme yolunun yukarısındaki bir işleyici tarafından yakalanıyor ve dosya yalnızca yüklenemiyordu; kimsenin fark etmemesinin sebebi buydu, oysa gerçekte olan, dosyanın seçtiği bir bellek yetersizliği olayıydı. Düzeltme ayırmayı testten sonraya taşır ve sayımın veriyle birlikte yolculuk etmesini sağlar
OutOfRange := Int64(ValueOffset) + Int64(TypeSize) * Result.Length
> Length(Source);
If OutOfRange Then
Begin
Result.Length := 0; // sayım değerlerle birlikte gider
SetLength(Result.IntegerValues, 0);
SetLength(Result.DoubleValues, 0);
End
Else
Begin
SetLength(Result.IntegerValues, Result.Length); // ancak şimdi
SetLength(Result.DoubleValues, Result.Length);
End;
// ... sonra, mevcut yedek dal nihayet var olma sebebi olan duruma ulaşır:
If (Result.Length = 0) Then
Begin
SetLength(Result.IntegerValues, 1);
Result.IntegerValues[0] := 0;
End;
Bu düzeltmenin derleyiciye özel hiçbir yanı yok ve onu bu listeye sokan da tam olarak bu. Kusur Delphi'de de Free Pascal'da da aynı nedenle gizliydi: hiçbir test dosyasında dosyanın sonunu işaret eden bir dizin girdisi yoktu. Taşıma işlemi onu açığa çıkarmadı. Kodu "Delphi burada benim yerime ne yapıyor da ben yapmıyorum" sorusuyla okumak çıkardı
Bir PNG IHDR, biçimin tanımlamadığı bir renk türü iddia ederse ne olur?
PDF Library for Delphi artık görüntüyü satır filtreleri çalışmadan önce reddeder; v3.539.2'den önce sıfır baytlık bir tarama satırı hesaplayıp filtresiz döngülere boş bir arabellek veriyordu. ISO 15948 §11.2.2 IHDR chunk'ını tanımlar ve Tablo 11.1 altı yasal renk türü ve bit derinliği bileşimini listeler: 1, 2, 4, 8 veya 16 bit gri tonlama; 1, 2, 4 veya 8 indeksli renk; ve 8 ya da 16 bit truecolor, alfa ile gri tonlama ve alfa ile truecolor. TPNGReader, IHDR'ın sıkıştırma yöntemi ve filtre yöntemi alanlarını doğruluyor, FColorType ile bit derinliğini ise dokunmadan geçiriyordu
Satır filtresi kodu her şeyi, her renk türünü bir bileşen sayısına eşleyen bir Case FColorType Of üzerinden boyutlar. Altı türün dışında kalan bir renk türü Else dalına düşer; orada SourceComponents 0 olur, dolayısıyla ScanlineByteCount 0 olur, dolayısıyla SetLength(PreviousScanline, 0) çağrısını hemen FillChar(PreviousScanline[0], ScanlineByteCount, 0) izler. Boş bir dinamik dizinin sıfırıncı elemanına indeksleme, nil üzerinden hesaplanan bir adrestir. Aralık denetimi kapalıyken o adres üzerinden sıfır baytlık bir doldurma sessiz bir no-op'tur ve çözücü var olmayan satırların arasından yürümeye devam eder; aralık denetimi açıkken ilk görüntüde bir ERangeError alırsınız; onu izleyen Move çağrıları ise bir erişim ihlalinden bir adım uzaktadır. Hangisini aldığınız, çözücünün verdiği bir karara değil derleyiciye ve derleme anahtarlarına bağlıdır ve asıl ipucu da budur: çözücü hiç karar vermemiştir
Düzeltme, diğer IHDR alanlarının zaten denetlendiği yerde uygulanan spesifikasyon tablosudur: COLOR_GRAYSCALE FSourceBitDepth in [1, 2, 4, 8, 16] kabul eder, COLOR_PALETTE [1, 2, 4, 8] kabul eder ve COLOR_RGB, COLOR_GRAYSCALEALPHA ile COLOR_RGBALPHA [8, 16] kabul eder; başka her şey ValidImage değerini temizler ve görüntü, tanılama için genişliği ve yüksekliği korunarak reddedilir. Dokuz baytından kısa bir pHYs chunk'ı da aynı geçişte kapatıldı, çünkü DPI okuyucusu kısa chunk'ın boş bıraktığı bir dizenin S[1] ile S[8] arasına indeksliyordu
0 tabanlı gösterici gibi ele alınan 1 tabanlı bir ofset
InflateStrFromPosition(Const Input: AnsiString; StartPos, MaxOutput: Integer; Out Consumed: Integer): AnsiString 1 tabanlı bir StartPos alır, çünkü girdisi bir AnsiString'tir ve Delphi uygulaması zlib girdisine @Input[StartPos] olarak erişir. Her iki Windows hedefi de sıkıştırmayı statik olarak bağlasın diye paszlib hedeflenerek yazılan Free Pascal uygulaması ise next_in değerini PAnsiChar(Input) + StartPos, avail_in değerini Length(Input) - StartPos yapıyordu. Bu bir gösterici aritmetiğidir ve 0 tabanlıdır. Bu fonksiyonda "baştan başla" demek olan 1 değerini geçin, FPC derlemesi çözmeye ikinci bayttan başlar ve sondan bir bayt önce durur
Bunun ayakta kalmasının sebebi, testlerin çoğunun ulaştığı tek çağıranın 0 geçen InflateStr olmasıydı. Sıfır, 0 tabanlı doğru ofset olduğu için iki derleme her düz InflateStr çağrısında ve ondan geçen her testte anlaşıyordu. Tek FlateDecode akışlarını okunabilir biçime açmak için SaveQDFToFile ile ConvertFileToQDF tarafından kullanılan TPDFDocument.DecodeAllStreams ise 1 geçer. FPC derlemesinde atlanan zlib başlığı inflate'i başarısız kıldı, ancak zlib akışı incelediği baytlar için yine de sıfırdan farklı bir Consumed bildirdi; böylece DecodeAllStreams boş yükü başarılı bir çözme saydı ve her içerik akışını boş bir dizeyle değiştirdi. Ortaya çıkan QDF doğru sayfa sayısına, geçerli yapıya ve hiç sayfa içeriğine sahipti; yani her görüntüleyicide hatasız açılan ve hiçbir şey göstermeyen bir dosya
// InflateStrFromPosition fonksiyonunun FPC dalı, v3.539.16 sonrası.
// StartPos, Delphi dalındaki gibi 1 tabanlıdır; önce kenetleyin, sonra
// sınırda tam olarak bir kez 0 tabanlı gösterici ofsetine çevirin.
If (StartPos < 1) Then
StartPos := 1;
If (Length(Input) = 0) Or (StartPos > Length(Input)) Then
Exit;
...
strm.next_in := Pointer(PAnsiChar(Input) + StartPos - 1);
strm.avail_in := Length(Input) - StartPos + 1;
Onu koruyan regresyon mümkün olan en küçüğüdür: bir yükü deflate edip 0 konumundan ve 1 konumundan inflate edin, ardından ikisinin de aynı yükü döndürdüğünü ve Consumed değerinin her ikisinde de akışın tam uzunluğuna eşit olduğunu doğrulayın. Bir RFC 1950 akışının iki baytlık bir başlığı ve dört baytlık bir Adler-32 kuyruğu vardır; bu yüzden iki uçtan birindeki birimlik kayma ince bir bozulma değil, ya başlayamayan ya bitemeyen bir akıştır. Ders zlib ile değil sınırla ilgilidir: bir fonksiyonun parametresi bir indeks tabanında tanımlanıp altındaki uygulama diğerini kullanıyorsa, dönüşüm tam olarak bir satırda yer alır ve bir testin onu iki tabanı birbirinden ayıran değerle çağırması gerekir
Kısa bir TStream.Read neden akışın sonu değildir?
Çünkü TStream.Read, istediği herhangi bir sebeple istenenden daha az bayt döndürebilir ve yalnızca 0 dönüşü "artık bir şey yok" demektir. Yerel diskteki TMemoryStream ve TFileStream isteği neredeyse her zaman tam doldurur; "istediğimden az döndü"yü dosya sonu sayan kod da bu yüzden onları kullanan her testten geçer. Ağ üzerinden beslenen akışlar, sıkıştırma açma akışları ve bir müşterinin yazdığı herhangi bir TStream torunu, altmış dört bin istendiğinde iki bayt döndürebilir ve arkasında hâlâ gigabaytlar olabilir
TPLBuffer, PDF Library for Delphi'deki her ayrıştırıcının geçtiği okuyucudur ve bir AnsiString, bir gösterici, bir bayt dizisi ya da bir TStream sarabilir. Hepsi Int64 döndüren dört tarama sorgusu DistanceToByte, DistanceToOtherByte, DistanceToAnyByte ve DistanceToOtherBytes, kaynağı 64 KB'lık bloklar hâlinde okuyup bir ayırıcı arar ve mantıksal konumu hareket ettirmeden ne kadar uzakta olduğunu bildirir. Her döngü Until ReadCount < BlockSize ile bitiyordu. Bellek içi üç kaynak için bu doğrudur, çünkü ReadIntoBuffer sonuncuya kadar her zaman bloğun tamamını verir. Akış kaynağı için ise bu, taramanın ilk kısa okumada pes ettiği, ayırıcıyı yok saydığı ve üstündeki tokenizer'ın nesnenin bittiği yeri yanlış belirlediği anlamına gelir
// TPLBuffer.DistanceToByte, v3.539.6 sonrasındaki döngü.
// Sıfır, TStream.Read'in tanımladığı tek veri sonu sinyalidir.
TempPosition := FPosition;
Try
Repeat
ReadCount := ReadIntoBuffer(@TempBuffer[0], BlockSize);
For TestPos := 0 To ReadCount - 1 Do
If TempBuffer[TestPos] = Value Then
Begin
Result := TotalSkipped + TestPos;
Exit;
End;
Inc(TotalSkipped, ReadCount);
Until ReadCount = 0;
Finally
FPosition := TempPosition; // bir göz atma okuyucuyu hareket ettirmemeli
End;
Bunu sabitleyen test, Read override'ı her isteği iki baytla sınırlayan bir TMemoryStream torunudur. aaaaaX dizesini ona sarın, arabellek konumunu 1 yapın; dört sorgunun da X için 4 uzaklık bildirmesi, konumu sonrasında 1'de bırakması ve olmayan bir bayt için -1 döndürmesi gerekir. Düzeltmeden önce ilk sorgu iki bayt görüp akışın tükendiği sonucuna varıyor ve -1 döndürüyordu. finally en az döngü koşulu kadar önemlidir: taramanın içinden bir Exit olağan başarı yoludur ve mantıksal konum yalnızca döngü sonuna kadar çalıştığında değil, o yolda da geri yüklenmelidir
Tek kaynak, iki derleyici, tek doğrulama kümesi
Bu beşinden çıkan disiplin şudur: "Delphi derlemesi geçiyor" Delphi hakkında bir kanıttır, kaynak hakkında değil. v3.539.16'dan bu yana hem Delphi DUnitX paketi hem de Free Pascal konsol paketi aynı Tests\CrossCompilerSemantics.inc dosyasını içerir: RunCrossCompilerFileSemantics adlı tek bir rutin, TPDFlib üzerinden sıkıştırılmış içerikli iki sayfalık bir belge kurar, kaydeder, SaveQDFToFile ile QDF olarak yeniden kaydeder, QDF'yi RepairQDFFile ile onarır, düz dosyayı EncryptFile ve EncodePermissions kaynaklı bir izin maskesiyle AES-128 ile şifreler; ardından her çıktıyı yeniden yükleyip iki derleyicide de aynı şeyleri doğrular: sayfa sayısı 2'dir, başlık korunur, ikinci sayfanın metni düz, onarılmış ve şifrelenmiş dosyalardan bozulmadan çıkarılır, yanlış parola sıfırdan farklı bir LastErrorCode ile reddedilir, EncryptionStrength 128'dir, EncryptionAlgorithm 2'dir ve GetUserPermissions kaynaklı tek tek izin bitleri kodlandığı gibi geri döner
Karşılaştırma bilinçli olarak bayt bayt değil normalleştirilmiştir. Şifreleme rastgele tuzlar çeker ve yazıcı belge tanımlayıcıları atar; bu yüzden iki derlemenin özdeş dosyalar üretmesi beklenmez, aynı anlama gelen dosyalar üretmesi beklenir ve doğrulamalar da bu düzeyde ifade edilir. QDF ayağı özellikle ofset hatası yüzünden vardır: iki sayfalı ve içeriksiz bir QDF, sayfa sayısı kontrolünden geçer ve metin çıkarma kontrolünden kalır; matris de ikincisini doğrular. Bir derleyicide no-op olup diğerinde davranış değişikliği olan her gelecek düzeltme — ki bu yukarıdaki beşin dördünü tarif eder — artık sevk edilmeden önce aynı doğrulamaları iki kez geçmek zorunda
Aynı taşımanın bağlama zamanına ait yarısı, Delphi'nin OMF nesneleri ile Free Pascal'ın COFF beklentilerini uzlaştırmak, FPC Win32 OMF - COFF nesne bağlama yazısında kendi hikâyesi olarak duruyor; aynı TIFF okuyucusunun BigTIFF ve döşemeli dosyalara karşı yapısal sağlamlaştırması ise yerleşik TIFF çözücü notları içinde. Bu yazıdaki çözücüler ve artık altlarında duran derleyiciler arası test, Delphi, C++Builder ve Free Pascal için PDF Library for Delphi ile gelir; orada aynı kaynağın, hedeflediği her derleyicide aynı sonucu birinden hazır alması değil hak etmesi beklenir