Teknik Makale

Kazara Çalışan Delphi Kodu: Beş FPC Taşıma Tuzağı

CCITT, TIFF, PNG, Flate ve akış arabelleği kodlarını Free Pascal üzerinde çalıştırmak, yalnızca kazara Delphi'de çalışan Pascal kodu bulmanın güvenilir bir yoludur. Aşağıdaki beş desenin hiçbiri bir derleyici hatası içermez. Her biri, Delphi'nin bir uygulama ayrıntısı sayesinde doğru yürüttüğü koddur: ç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 tuzağın somut şekli, PDF Library for Delphi'nin onu nasıl ele aldığı ve arkasındaki disiplin var: aynı kaynak her iki derleyicide de aynı belge semantiğini üretmek zorundadır ve bunu bilmenin tek yolu iki tarafta da kontrol etmektir. 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 alır. Bu yazı ise bambaşka bir hata sınıfıyla ilgilidir: 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. Akla gelen uygulama Result[0] ve Result[1] yazar, Result üzerinde hiç SetLength çağırmaz

Bu, ilk yazmada hata vermeliydi ve Free Pascal'da veriyor. Delphi'de ise çağrı yerleri şöyle göründüğü sürece vermez: 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

PDFlibPas CCITT kod çözmede ayrışma: Delphi, çağıranın b dizisini GetNextChangingElement fonksiyonunun gizli var Result parametresi olarak geçirir, böylece yazmalar çağıranın sahip olduğu belleğe düşer ve kaçırılan bir arama önceki değerleri korur; Free Pascal ise fonksiyona taze bir nil dizi verir ve Length korumasının ilk yazmadan önce SetLength ile boyutlandırması gerekir
Delphi, çağıranın dizisini gizli Result parametresi olarak takma adla paylaşır ve korumasız yazmalar yine de sahipli belleğe düşer; Free Pascal nil ile gelir ve tek satırlık koruma, Delphi çözme yolunu hiç değiştirmeden hatayı amaçlanan davranışa çevirir

Takma ad paylaşımı ayrıca çözücünün bağımlı olduğu bir semantik taşır. 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 çözüm 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ştirir. PDF Library for Delphi sıfırlama yerine bir koruma kullanır: Delphi'de ölü koddur ve çözme yolu bayt bayt aynı kalır; Free Pascal'da ise bir hatayı amaçlanan davranışa çevirir. Bu asimetri işin tamamıdır, çünkü korumanın, kodun zaten doğrulanmış çıktı ürettiği derleyicide no-op olması gerekir

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. Naif aralık kontrolü, Offset + TypeSize * Length toplamının dosyanın sonunu aşıp aşmadığını test eder ve aşıyorsa iki diziyi de sıfır uzunluğa ayarlarken Result.Length'i dosyadan gelen değerde bırakır

Oradan sonra iki şey ters gider. Fonksiyon, "if Length sıfırsa girdiye sıfır değerli bir eleman ver" diyen bir yedek dalla biter; böylece çağıranlar sıfırıncı elemanı her zaman okuyabilir. Length aralık dışı yolunda hiç temizlenmediğinde bu yedek, tam da var olma sebebi olan durumda hiç çalışmaz. Ç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 tuzak sıralamadır. İki SetLength çağrısını, dosyadaki sayıma göre boyutlandırıp aralık testinden önce çalıştırın; düşmanca bir girdi, tek bir geçerlilik kontrolünden önce çok gigabaytlık bir ayırma talep edebilir. Delphi'de ortaya çıkan istisna tipik olarak görüntü yükleme yolunun yukarısındaki bir işleyici tarafından yakalanır ve dosya yalnızca yüklenemez; sorunun kolay kaçmasının sebebi budur, oysa gerçekte olan, dosyanın seçtiği bir bellek yetersizliği olayıdır. Doğru sıra, ayırmayı testten sonraya koyar ve sayımın veriyle birlikte yolculuk etmesini sağlar

PDFlibPas'te TIFF dizin girdisi sağlamlaştırması: 12 baytlık girdi dosyadan gelen bir sayım taşır, naif sıra dizileri aralık testinden önce bu sayıma göre ayırıp temizledikten sonra Result.Length değerini canlı bırakır, güvenli sıra ise önce Int64 aritmetiğini dosya uzunluğuna karşı test eder ve sayımı dizilerle birlikte temizler
Aralık testinden önce ayırma yapmak, düşmanca bir sayımın gigabaytlar istemesine izin verir ve boşaltılmış bir dizide canlı bir sayım bırakır; güvenli sıra önce ofseti test eder ve Result.Length değerini dizilerle aynı ifadede temizler
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, yedek dal var olma sebebi olan duruma ulaşır:
If (Result.Length = 0) Then
Begin
  SetLength(Result.IntegerValues, 1);
  Result.IntegerValues[0] := 0;
End;

Bunun derleyiciye özel hiçbir yanı yok ve onu bu listeye sokan da tam olarak bu. Tuzak, Delphi'de de Free Pascal'da da aynı nedenden gizli kalır: dosyanın sonunu işaret eden bir dizin girdisi gerekir ve sıradan test dosyalarında öyle bir girdi yoktur. Bir taşıma onu açığa çıkarmaz. Kodu "Delphi burada benim yerime ne yapıyor da ben yapmıyorum" sorusuyla okumak çıkarır

Bir PNG IHDR, biçimin tanımlamadığı bir renk türü iddia ederse ne olur?

PDF Library for Delphi görüntüyü satır filtreleri çalışmadan önce reddeder. 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. Tuzak, IHDR'ın sıkıştırma yöntemi ve filtre yöntemi alanlarını doğrulayıp FColorType ile bit derinliğini dokunmadan geçen bir okuyucudur: yasadışı bir renk türü o zaman sıfır baytlık bir tarama satırı üretir ve filtresiz döngüler boş bir arabellek alır

Satır filtresi kodu tipik olarak 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

PDF Library for Delphi, spesifikasyon tablosunu diğer IHDR alanlarının denetlendiği yerde uygular: 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'ı aynı yerde denetlenir; böylece DPI okuyucusu, kısa chunk'ın boş bıraktığı bir dizenin S[1] ile S[8] arasına asla indekslemez

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 aynı girdiye gösterici aritmetiği üzerinden erişmek zorundadır ve gösterici aritmetiği 0 tabanlıdır. next_in değerini PAnsiChar(Input) + StartPos, avail_in değerini Length(Input) - StartPos yapıp bu fonksiyonda "baştan başla" demek olan 1 değerini geçirin; FPC derlemesi çözmeye ikinci bayttan başlar ve sondan bir bayt önce durur

Bu tuzak kolay gizlenir, çünkü testlerin çoğunun ulaştığı çağıran, 0 geçen InflateStrdir. 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şır. 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. Birimlik kayma yerindeyken atlanan zlib başlığı inflate'i başarısız kılar, ancak zlib akışı incelediği baytlar için yine de sıfırdan farklı bir Consumed bildirir; Consumed'a güvenen bir çağıran böylece boş yükü başarılı bir çözme sayar ve her içerik akışını boş bir dizeyle değiştirir. Ortaya çıkan QDF doğru sayfa sayısına, geçerli yapıya ve hiç sayfa içeriğine sahiptir; yani her görüntüleyicide hatasız açılan ve hiçbir şey göstermeyen bir dosya

// InflateStrFromPosition fonksiyonunun FPC dalı.
// 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 yakalayan kontrol 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 bildirdiğini teyit edin. 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. Böyle bir döngü doğal olarak Until ReadCount < BlockSize ile biter. Bellek içi üç kaynak için bu doğrudur, çünkü ReadIntoBuffer sonuncuya kadar her zaman bloğun tamamını verir. Bir akış kaynağı içinse bu, taramanın ilk kısa okumada pes ettiği, ayırıcıyı yok bildirdiği ve üstündeki tokenizer'ın nesnenin bittiği yeri yanlış belirlediği anlamına gelir; TPLBuffer bu yüzden bir okuma 0 döndürene kadar döngü yapar

PDFlibPas akış arabelleğinde kısa okuma işleme: DistanceToByte 64 KB'lık bloklar tarar, eski döngü Until ReadCount < BlockSize koşulunu veri sonu sayıp ilk kısa okumada pes ediyordu, düzeltilmiş döngü ise ReadCount sıfıra eşit olana kadar döner, ayırıcıyı bulur ve konumu bir finally bloğunda geri yükler
Altmış dört bin istendiğinde bir akış iki bayt döndürebilir, bu yüzden taramanın güvenebileceği tek veri sonu sinyali sıfırdır; finally bloğu da ayırıcı bulunup döngü erken çıktığında mantıksal konumu geri yükler
// TPLBuffer.DistanceToByte tarama döngüsü.
// 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 sabitlemenin yolu, 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. İlk kısa okumada duran bir tarama iki bayt görür, akışın tükendiği sonucuna varır ve -1 döndürür. 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şinin arkasındaki disiplin şudur: "Delphi derlemesi geçiyor" Delphi hakkında bir kanıttır, kaynak hakkında değil. Sahip olmaya değer bir derleyiciler arası kontrol, tek bir rutini her iki derleyici altında çalıştırır: TPDFlib üzerinden sıkıştırılmış içerikli iki sayfalık bir belge kurun, kaydedin, SaveQDFToFile ile QDF olarak yeniden kaydedin, QDF'yi RepairQDFFile ile onarın, düz dosyayı EncryptFile ve EncodePermissions kaynaklı bir izin maskesiyle AES-128 ile şifreleyin; ardından her çıktıyı yeniden yükleyip iki derleyicide de aynı şeyleri kontrol edin: 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

Anlamları bayt bayt değil normalleştirilmiş olarak karşılaştırın. Ş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 kontroller de bu düzeyde ifade edilmelidir. QDF ayağı yerini ofset tuzağı sayesinde hak eder: iki sayfalı ve içeriksiz bir QDF, sayfa sayısı kontrolünden geçer ve metin çıkarma kontrolünden kalır; o zaman ikincisini kontrol edin. Bir derleyicide no-op olup diğerinde davranış değişikliği olan her değişiklik — ki bu yukarıdaki beşin dördünü tarif eder — aynı kontrolleri iki tarafta da geçmek zorundadır

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, 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