Taşma denetimi kapalıyken 2000000000 + 2000000000, her iki operand Delphi Integer değeriyse -294.967.296 olarak değerlenir. O sayı, bir kapsayıcı ayrıştırıcısı bir kayıt başlığından bir uzaklık ve bir uzunluk okuyup gömülü PDF'in tamponun içine sığıp sığmadığını görmek için bunları topladığı anda bilmece olmaktan çıkar. Toplam negatife sarar, Length(Data) ile karşılaştırma geçer ve bir sonraki satır, dizinin sonundan kabaca iki gigabayt ötedeki bir baytın adresini alır. Daha büyük bir tamponun ortasından bir PDF yüklemek, asla toplama yapmayan bir aralık denetimi ve belge açıldığında baytların sahibinin kim olduğuna dair net bir yanıt ister
Bu yazı, hâlihazırda RAM'de duran, başka bir şeyin içinde bilinen bir uzaklıkta oturan belgeleri kapsar. Bellekte tutulamayacak kadar büyük belgeler başka bir sorundur; PDFium VCL ile büyük PDF'leri talep üzerine akıtma yazısında anlatıldığı gibi blokların diskten talep üzerine çekilmesiyle ele alınır. Son bölüm çizgiyi çeker
Bilinen bir uzaklıktaki gömülü PDF
Bir posta deposu, mesaj gövdelerini ve ekleri tek bir kayıtta tutar. Bir arşiv kapsayıcısı bir manifesti, birkaç görseli ve bir PDF'i arka arkaya ekler. Özel bir kablo protokolü, belgeyi uzunluk önekli bir başlığın arkasına çerçeveler. Kayıt 40 MB olsun, PDF 1.182.336 baytta başlayıp 312 KB sürsün. Alışılmış kod, dilim kesmek için Copy(Data, Index, Count) çağırır ve o dilimi, kendi tamponuna bir kez daha kopyalayan bir yükleyiciye uzatır. Kapsayıcıyı hesaba katarsak aynı 312 KB artık üç kez vardır ve bir posta kutusu taraması bunu her mesaj için yineler
PDFium Component pencereye doğrudan seslenir. TPdf, LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean = True) bildirir; Index sıfır tabanlı bir bayt konumu, Count bir bayt uzunluğudur. Metot her iki sayıyı doğrular, tek bir işaretçi hesaplar ve belgeyi FPDF_LoadMemDocument64 üzerinden açan LoadDocument(Data: Pointer; Size: NativeUInt; Buffered: Boolean) işaretçi overload'una devreder. Tüm dizi biçimi olan LoadDocument(const Data: TBytes; Buffered: Boolean), bayt aralığı biçimine 0 ve Length(Data) ile tek satırlık bir çağrıdır; yani her TBytes yüklemesi aynı doğrulamadan geçer
Kodda dilim tamamen ortadan kalkar. LocateEmbeddedPdf, kendi kapsayıcı ayrıştırıcınızın yerini tutar
uses
System.SysUtils, System.IOUtils, PDFium;
procedure PreviewEmbedded(const RecordFile: string);
var
Frame: TBytes; // tüm kapsayıcı kayıt, onlarca megabayt
Offset, Size: Integer;
Pdf: TPdf;
begin
Frame := TFile.ReadAllBytes(RecordFile);
LocateEmbeddedPdf(Frame, Offset, Size); // sizin ayrıştırıcınız, bileşenin parçası değil
Pdf := TPdf.Create(nil);
try
// Copy(Frame, Offset, Size) yok: pencere yerinde adreslenir
Pdf.LoadDocument(Frame, Offset, Size); // Buffered varsayılan olarak True
Frame := nil; // burada sorun yok: yalnızca PDF penceresi kopyalandı
Writeln('Pages: ', Pdf.PageCount);
finally
Pdf.Free; // belgeyi kapatır
end;
end;
Küresel EnableV8Engine hâlâ False iken ve PDFium kütüphanesi henüz yüklenmemişken, bellek yükleyicileri gelen baytları XFA işaretleri için tarar ve bulurlarsa V8 yetenekli DLL yüklensin diye EnableV8Engine'i açarlar. Bayt aralığı çağrısında o tarama yalnızca pencereyi görür. Aynı kayıtta başka bir yerde duran bir manifest ya da ikinci bir belge onu tetikleyemez
Index artı Count sınır kontrolünü neden taşırıyor?
Çünkü Index ile Count ikisi de 32 bitlik işaretli Integer değeridir ve iki büyük pozitifin toplamı güvenilir biçimde büyük pozitif bir sonuç üretmez. Uzaklık ve uzunluk bir dosya başlığından geliyorsa, ikisini de bir saldırgan ya da bozuk bir kayıt yönetir. Aynı tuzak, o sayıları önce hesaplayan kendi kapsayıcı ayrıştırıcınızda da pusuda bekler
// Kendi kapsayıcı ayrıştırıcınız için bir yardımcı (öneri; bileşenin parçası değil)
function IsValidWindow(const Data: TBytes; Index, Count: Integer): Boolean;
begin
// YANLIŞ: Index + Count Integer olarak değerlendirilir ve negatife sarabilir
// Result := (Index >= 0) and (Index + Count <= Length(Data));
// DOĞRU: işaretleri reddet, Index'i kendi başına sınırla, sonra Count'ı
// kalana karşı karşılaştır; tek aritmetik, saramayacak bir çıkarmadır
Result := (Index >= 0) and (Count >= 0) and
(Index <= Length(Data)) and
(Count <= Length(Data) - Index);
end;
Başarısız durumu elden geçirin. Index = 2000000000 ve Count = 2000000000 iken gerçek toplam dört milyardır ama 32 bit işaretli aritmetik onu -294.967.296 olarak saklar. O değer herhangi bir Length(Data)'dan rahatça küçüktür; yanlış denetim geçer ve @Data[Index], iki gigabaytlık bir uzunluk eklenmiş şekilde dizinin iyice dışına işaret eder. Çökme iyi sonuçtur. Kötüsü, PDFium'un orada tesadüfen duran süreç belleğini ayrıştırmasıdır
Doğru sıralama asla toplamaz. Negatif değerler, hiçbir şey indekslenmeden önce reddedilir. Index sonra tek başına Length(Data) ile sınırlanır; bu, Length(Data) - Index'in negatif olmayan bir Integer olmasını garanti eder ve ancak o zaman Count o kalana karşı karşılaştırılır. Delphi and'i varsayılan olarak kısa devre mantığıyla değerlendirir ({$B-}); sonraki her terim, öncekileri sağlanmadan koşmaz. Güvenlik ağı olarak {$Q+}'a yaslanmayın: sürüm derlemeleri taşma denetimi kapalı olarak gönderilir ve açıkken bile, bir bellek güvenliği deliğini bir doğrulama yordamının ortasından kaçan bir EIntOverflow'a takas etmiş olursunuz
Bayt aralığı LoadDocument aynı dört testi aynı sırada koşar ve başarısız kuralı adıyla söyleyen bir mesajla EPdfError fırlatır: "PDF byte range index cannot be negative", "PDF byte range count cannot be negative", "PDF byte range index exceeds data length" ve "PDF byte range exceeds data length". Her birini dört baytlık bir diziyle yeniden üretebilirsiniz: Index = 5, Count = 0 üçüncü mesajı tetikler, Index = 2, Count = 3 dördüncüyü ve her reddin ardından Pdf.Active hâlâ False'tur. Güvenilmeyen uzunlukların yerel sınıra bu şekilde muamelesi, Delphi'de PDFium VCL ABI'sini ve bellek güvenliğini sertleştirme yazısında daha geniş ele alınır
Sıfır uzunluklu bir pencere hâlâ yasal bir çağrıdır
Boş bir pencere bir nil işaretçi geçirir; çünkü @Data[Index], doğrulamanın kabul ettiği her Index için yasal bir ifade değildir. Count = 0 ile Index = Length(Data), tamponun kuyruğunda iyi biçimli boş bir penceredir ve boş bir TBytes, sıfır numaralı elemanı bulunmayan bir dizide Index = 0 verir. Her iki durumda da adres almak sonun ötesini okur ya da bir nil dinamik diziyi referans eder ve aralık denetimi açıksa Delphi, yükleyici daha koşmadan ERangeError fırlatır
Bu yüzden bayt aralığı overload'u dallanır: Count = 0 nil verir, başka her sayı @Data[Index] verir. İşaretçi overload'u nil'i yalnızca sıfır boyutla birlikte kabul eder, PDFium sonra boş bir tamponu açmayı reddeder ve çağrı, PDFium'un son hata metnini taşıyan sıradan bir EPdfError ile, PDFium hiçbiri bildirmediyse "Cannot load PDF document" ile biter. Sıfır baytlık bir ek bildiren bir başlık, öteki her kötü girdiyle aynı yakalanabilir istisnayı alır. Bunu ayrıştırıcınızda, mesajda kayıt numarasıyla reddetmek, destek günlüğünde yine de daha iyi okunur
Buffered, baytların sahibini kararlaştırır
Buffered, sonuçları çağrının ötesine taşan tek parametredir. Buffered = True iken bileşen, seçili pencereyi ve yalnızca pencereyi, yüklemeden önce kendi tamponuna kopyalar. 40 MB'lık örnekte 312 KB kopyalanır ve kapsayıcıya dokunulmaz. LoadDocument döner dönmez kapsayıcıyı serbest bırakabilir, yeniden kullanabilir ya da üzerine yazabilirsiniz
Buffered = False iken @Data[Index] doğrudan FPDF_LoadMemDocument64'e gider ve PDFium, belge açık kaldığı sürece o işaretçiden okumayı sürdürür. Arkasındaki tüm TBytes ödünç verilmiş olur. Active := False set edene ya da TPdf'i serbest bırakana kadar canlı ve değişmemiş kalmalıdır. Önemli olan pencere değil tüm dizidir: bir Delphi dinamik dizisi tek blok olarak referans sayılır, yani kodunuzun herhangi bir yerinde son referansın düşmesi, PDFium'un hâlâ okuduğu belleği serbest bırakır. SetLength de aynı derecede ölümcüldür; bir yeniden ayırma bloğu taşıyabilir
İki Delphi ayrıntısı, bunu göründüğünden daha kolay bozabilir. Dinamik dizilerin, dizelerin aksine kopyala-yaz yoktur; Other := FFrame; Other[0] := 0;, PDFium'un ayrıştırdığı baytların ta kendisine yazar. Ve ağ kodunuzun sonraki çerçeve için yeniden kullandığı bir alım tamponu, açık belgenin altında üzerine yazılır. Pdf.LoadDocument(ReadRecord(Id), Offset, Size, False) gibi bir fonksiyon sonucunu doğrudan geçmek, diziyi çağıran yordamla ölen gizli bir geçiciye bırakır; belge ise yaşamaya devam eder
İş parçacığından geçen, yeniden kullanılabilir bir tamponun arkasında duran ya da onu yükleyen yordamdan uzun yaşayan her şey için Buffered = True tutun. Ek maliyet, PDF'in kendi boyutunda bir kopyadır ve karşılığında çağrı döner dönmez 40 MB'lık bir kaydı düşürme özgürlüğü satın alırsınız. Ödünç alma, yalnızca kapsayıcının hâlihazırda bütün oturum boyunca tuttuğunuz uzun ömürlü bir alan olduğu zaman hak eder; kullanıcının eki arasında sayfa çevirirken arşiv kaydını açık tutan bir görüntüleyici gibi. O durumda sahipliği tek bir sınıfta açık hale getirin
type
TFrameSession = class
private
FFrame: TBytes; // FPdf'te açık bir belge varken destek deposunun sahibi
FPdf: TPdf;
public
constructor Create(const AFrame: TBytes);
destructor Destroy; override;
procedure OpenEmbedded(Offset, Size: Integer);
end;
constructor TFrameSession.Create(const AFrame: TBytes);
begin
inherited Create;
FFrame := AFrame; // diziyi paylaşır; artık başkası yazamaz
FPdf := TPdf.Create(nil);
end;
procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
FPdf.Active := False; // önce varsa önceki pencereyi kapat
FPdf.LoadDocument(FFrame, Offset, Size, False); // FFrame bu belgeden uzun yaşamalı
end;
destructor TFrameSession.Destroy;
begin
FPdf.Active := False; // 1. ödünç verileni serbest bırak
FPdf.Free;
FFrame := nil; // 2. depolama ancak şimdi gidebilir
inherited;
end;
Kurucu, çağıranın dizisini kopyalamak yerine paylaşır; yani çağıranın artık ona yazmayı bırakması gerekir. Bunu vaat edemiyorsanız kurucuya Copy(AFrame) geçirin ve oturum başına bir kez, yükleme başına değil, yapılan tek kopyaya geri dönersiniz
Pencerenin %PDF- ile başladığını denetlemek
Birkaç bayt sapmış bir pencere ince başarısızlıktır, çünkü belge yine de açılabilir. ISO 32000-1 §7.5.4, her çapraz referans girdisini dosyanın başından bir bayt uzaklığı olarak tanımlar ve §7.5.2 %PDF- başlığını başa koyar. Ayrıştırıcınız bir öneki yanlış sayar ve pencere sekiz bayt erken başlarsa, o uzaklıklar sekiz bayt yanlıştır. PDFium başlığı ilk 1024 bayt içinde arar ve baştaki çöpü telafi eder; geç başlayan ya da erken biten bir pencere, onu bulduğu nesne işaretlerinden çapraz referans tablosunu yeniden kurmaya itebilir. Her iki durumda da sayfa sayısı doğru görünür. Ayrıştırıcınızdaki uzaklık hâlâ yanlıştır ve o kaydı okuyan bir sonraki okuyucu daha az affedici olabilir
LoadDocument(const Data: TBytes; Index, Count: Integer; const Options: TPdfLoadOptions; out Report: TPdfLoadReport; Buffered: Boolean = True) overload'u bunu görünür kılar. Report.HeaderOffset, %PDF-'in pencerenin ilk 1024 baytı içinde bulunduğu yerdir; bulunamadıysa -1. Report.UsedRecovery, PDFium çapraz referans tablosunu geçerli kabul etmediyse True'dur. Rapordaki tüm uzaklıklar kapsayıcıdan değil, pencerenin ilk baytından sayılır. Çok kısa bir Count, sondaki %%EOF'u keser ve TPdfLoadOptions.Default(plmStrict), RequireFinalEndOfFileMarker'ı açar; yani katı mod o penceri tümüyle reddeder
var
Options: TPdfLoadOptions;
Report: TPdfLoadReport;
begin
Options := TPdfLoadOptions.Default; // plmCompatible
Pdf.LoadDocument(Frame, Offset, Size, Options, Report, True);
// LogWarning sizin günlük yordamınız
if Report.HeaderOffset > 0 then
LogWarning(Format('Window starts %d bytes before %%PDF-, check Offset',
[Report.HeaderOffset]))
else if Report.HeaderOffset < 0 then
LogWarning('No %PDF- header in the first 1024 bytes of the window');
if Report.UsedRecovery then
LogWarning('Cross-reference table was rebuilt, check Offset and Size');
end;
O alanların arkasındaki denetimin, ödünç yüklemeler için önemli bir maliyeti vardır. Pencere Options.AuditByteLimit'e (varsayılan 256 MB) sığıyorsa denetim pencerenin geçici bir kopyasını okur; yani Buffered = False bir raporla birleştiğinde yükleme artık kopyasız değildir. AuditByteLimit'i 0 yapmak denetimi ve kopyayı atlar, HeaderOffset'i -1'de bırakır. Kapsayıcı ayrıştırıcısını geliştirirken ve kayıtların yeni kaynaklarına karşı denetimli biçimi koşun; uzaklıklara güvenildikten sonra düz overload'a geçin. Raporun geri kalanı PDFium yük raporuyla PDF yükleme başarısızlıklarını yakalama yazısında ele alınır
Byte aralığının yanlış araç olduğu zaman
Bayt aralığı overload'u, tüm kapsayıcının hâlihazırda bellekte olduğunu varsayar ve Count bir Integer olduğundan tek bir pencere MaxInt baytı aşamaz. Diskteki 6 GB'lık bir arşiv ya da geri sarılamayan bir soket o kalıba sığmaz; içindeki bir pencereyi adreslemek için her şeyi TBytes'e okumak ise amacı yener. O durumlar için LoadCustomDocument(AStream: TStream; AOwnsStream: Boolean), PDFium'un blokları aramalı bir akıştan talep üzerine çekmesine izin verir. Her Seek'e PDF'in başlangıç uzaklığını ekleyen küçük bir TStream torununuz, gömülü belgeyi sıfır konumunda başlıyormuş gibi gösterir; talep üzerine akıtma yazısı bunu gösterir
Öteki uyumsuzluk, henüz düz bir PDF olmayan içeriktedir. Gömülü baytlar sıkıştırılmışsa, kapsayıcı tarafından şifrelenmişse ya da başka bir kodlamaya sarılmışsa, dönüştürülmüş bir kopyadan kaçınamazsınız ve o yeni diziyi varsayılan Buffered = True ile yüklemek düz sayılan cevaptır. Bayt aralığı tek biçimde parayı öder: bitişik, değişmemiş PDF baytları, hâlihazırda yerleşik, ayrıştırıcınızın toplama yapmadan doğruladığı bir uzaklıkta
Bayt aralığı çağrısı, aynı TPdf bileşeninde dosya, akış, ham işaretçi ve talep üzerine yüklemanın yanında durur. PDFium Component ürün sayfası, desteklenen Delphi ve C++Builder sürümlerini, lisanslama seçeneklerini ve yükleme API'sinin geri kalanını listeler