PDFium Component, TPdfParserResourceBudget adlı tek bir kaydı açığa çıkarır; kütüphanedeki her PDF parser'ı için özyineleme derinliğini, token sayısını, nesne sayısını, çözülmüş baytları, genişleme oranını, tahsisi ve bir duvar-saati deadline'ını sınırlar. ConfigurePdfParserResourceBudget onu süreç için atomik olarak ayarlar ve her ayrıştırma bir snapshot kopyalar; böylece eşikleri işlem ortasında kayamaz
Altı parser, altı özel tavan
Bunu zorlayan durum sıradandı ve muhtemelen tanıdıktır. Sparse xref gezinmesinin kendi nesne tavanı vardı. Stream denetçisinin kendi çözülmüş-bayt sınırı vardı. XMP'de bir tane vardı, XFDF ve FDF'de başka bir tane, içerik normalizasyonunda üçüncüsü ve preflight varsayılanlarında dördüncüsü. Bu sayıların her biri kendi başına savunulabilirdi ve hiçbiri dışarıdan erişilebilir değildi; bu yüzden "bu giriş yolu anonim yüklemeleri işliyor, burada hiçbir şey 64 MiB'den fazla tahsis etmemeli ya da dört saniyeden uzun çalışmamalı" demek isteyen bir çağıranın söyleyecek yeri yoktu. İkinci problem sessiz olduğu için daha kötüydü: bir limit değişken bir globalde yaşadığında, bir politika altında başlayan bir ayrıştırma başka bir politika altında bitebilirdi. Bir thread setter'ı çağırırken bir diğeri bir nesne stream'inin ortasında olabilirdi ve o gezinmenin ilk yarısı eski tavanla, ikinci yarısı yenisiyle sınırlanırdı. Hiçbir şey çökmez, hiçbir şey loglanmaz ve sonrasında işlemin gerçekte hangi limitler altında çalıştığını söyleyemezsin; limitlere tam olarak bu özellik için ihtiyacın vardı
TPdfParserResourceBudget gerçekte neyi sınırlar?
Yedi boyut; her biri, diğerlerinin içinden geçirdiği bir girdi sınıfını yakaladığı için seçildi. MaxRecursionDepth varsayılan olarak 1024'tür ve iç içe sözlüklerle dizilerin ISO 32000-1 §7.3 nesne sözdizimini bir stack overflow'a çevirmesini durdurur. MaxTokens varsayılan olarak 10.000.000'dur ve lexing işini dosya boyutundan bağımsız olarak sınırlar. MaxObjects varsayılan olarak 4.000.000'dur ve cross-reference alt bölüm sayılarını, nesne-stream /N değerlerini ve grafik gezinmelerini kapsar. MaxDecodedBytes varsayılan olarak 256 MiB ve MaxAllocationBytes 512 MiB'dir; böylece bir dosya tahsisten önce reddedilebilir, sonrasında değil. MaxExpansionRatio varsayılan olarak 1000.0'dır ve insanların hafife aldığı boyuttur: ISO 32000-1 §7.4 stream filtreleri genel amaçlı sıkıştırmadır, birkaç kilobayt Flate verisi meşru biçimde yüzlerce megabayta genişleyebilir ve bir oran tavanı sıkıştırma bombasına karşı standart savunmadır. Mutlak bayt sınırına diktir, çünkü 200 MiB olmak isteyen 200 KiB'lık bir stream 256 MiB tavanının altındadır ve yine de saçmadır. DeadlineMilliseconds varsayılan olarak 0'dır, yani deadline yok; bu boyut kesinlikle opt-in'dir. Varsayılanlar bir küme olarak bilerek cömerttir; sıradan iş belgelerinin hiçbir şeyi tetiklemesini engelleyecek boyutta, sertleştirilmiş bir profil olacak şekilde değil; girdin halka açık bir yükleme formundan geliyorsa bir Delphi PDF önizleme bölmesini sertleştirmek yazısında anlatılan kurallarla birlikte aşağıdaki sayılara daha yakın bir şey istersin
uses
FPdfPdfCommon;
procedure ApplyUntrustedIntakeBudget;
var
Budget: TPdfParserResourceBudget;
begin
Budget := TPdfParserResourceBudget.Default;
// Varsayılanlar korunur: MaxRecursionDepth 1024, MaxTokens 10000000,
// MaxObjects 4000000
Budget.MaxDecodedBytes := Int64(32) * 1024 * 1024; // varsayılan 256 MiB
Budget.MaxAllocationBytes := Int64(64) * 1024 * 1024; // varsayılan 512 MiB
Budget.MaxExpansionRatio := 50.0; // varsayılan 1000.0
Budget.DeadlineMilliseconds := 4000; // varsayılan 0 = kapalı
ConfigurePdfParserResourceBudget(Budget);
end;
Global'i okumak yerine neden snapshot alınır?
Çünkü uygulanırken değişebilen bir limit, limit değildir. Bu, tüm özelliğin çekirdek tasarım noktasıdır ve her giriş noktasında tek satırlık bir disiplindir: politikayı işlemin başında bir kez yerel bir TPdfParserResourceBudget değişkenine oku, sonra her checkpoint'in o yerel kopyayla karşılaştırmasını sağla. GetPdfParserResourceBudget kritik bölümü alır ve kaydın değer kopyasını geri verir; ondan sonra ayrıştırma değişken global duruma bir daha asla dokunmaz. Sonuç: başka bir thread'den gelen ConfigurePdfParserResourceBudget çağrısı bir sonraki işlemi etkiler, uçtaki işlemi asla. Eşzamanlı ayrıştırmalar birbirine karışmadan farklı politikalar altında çalışır, bir kaynak limitinde başarısız olan bir işlem tek ve tutarlı bir eşik kümesine karşı başarısız olur ve sıcak yolda kilit çekişmesi yoktur, çünkü kilit token başına değil ayrıştırma başına bir kez alınır
type
TCustomScanState = record
Budget: TPdfParserResourceBudget;
StartTick: Cardinal;
TokenCount: Int64;
Depth: Integer;
end;
procedure BeginScan(var S: TCustomScanState);
begin
S.Budget := GetPdfParserResourceBudget; // snapshot önden bir kez alınır
S.StartTick := PdfParserStartTick;
S.TokenCount := 0;
S.Depth := 0;
end;
function ScanCheckpoint(var S: TCustomScanState): Boolean;
begin
Result := False;
if PdfParserDeadlineReached(S.StartTick, S.Budget) then
Exit;
Inc(S.TokenCount);
if S.TokenCount > S.Budget.MaxTokens then
Exit;
if S.Depth > S.Budget.MaxRecursionDepth then
Exit;
Result := True;
end;
Kütüphanenin üzerine kendi parser'ını yazıyorsan token döngünün içinde GetPdfParserResourceBudget çağırmak yerine o biçimi kopyala. Token başına çağırmak doğru ama anlamsızdır: her yinelemede kritik bölüm ödersin ve snapshot'ın önlemek için var olduğu işlem-ortası kaymayı geri getirirsin
Bayt tavanı patolojik bir dosyayı neden durdurmaz?
Çünkü PDF'de boyut ve maliyet yalnızca gevşek biçimde ilişkilidir. /Index ve /W dizileri makul ama düşmanca bir girdi sayısı anlatan ISO 32000-1 §7.5.8 cross-reference stream'i, aynı ofsetleri yeniden ziyaret eden bir nesne-stream zinciri ya da her biri göründüğünden pahalı operatörler içeren bir içerik stream'i olan 400 KiB'lık bir dosya, ayarladığın her bayt ve nesne tavanının altında rahatça oturur ve yine de bir worker thread'i dakikalarca meşgul eder. Bayt sayıları belleği sınırlar; zamanı sınırlamaz. PdfParserStartTick ve PdfParserDeadlineReached tam olarak bunun içindir. PdfParserStartTick işlemin başında bir tick değeri yakalar ve PdfParserDeadlineReached geçen süreyi DeadlineMilliseconds ile karşılaştırır; deadline 0 olduğunda False döndürür. Çıkarma işaretsiz aritmetikte çalışır, bu yüzden tick sayacı sarımı sahte bir timeout üretmez. Cross-reference ayrıştırma, nesne-stream çözme, içerik token normalizasyonu ve FDF ile XFDF okuyucuları hepsi onu döngü başlarında kontrol eder; böylece bütçe aşan bir ayrıştırma dışarıdan öldürülmek yerine bir checkpoint'te durur
İki dürüst uyarı. Bu duvar-saati zamanıdır, CPU zamanı değil; takas yapan bir makine ya da yük altındaki bir build sunucusu, sessiz bir makinenin geçeceği bir deadline'ı tetikleyebilir; bu da onu gözlemlediğin en kötü durumun oldukça üzerine kurmanı gerektirir, medyanına yakın değil. Ayrıca varsayılan olarak kapalıdır, çünkü ay sonu toplu işleme sırasında meşru biçimde büyük bir belgede ateşlenen bir deadline, önlediğinden daha kötü bir başarısızlıktır. Global olarak değil giriş yolu başına opt-in yap ve cross-reference stream'lerini doğrulamak yazısındaki yapısal kontrollerle eşleştir
Bütçe bir tavandır, format limitlerinin yerine geçmez
Her parser kendi format-spesifik sabitlerini tutar ve ikisini birden uygular. XFDF en net örnektir: alan okuyucusu snapshot değerinin yanında XfdfMaxTopLevelFields ve 100000'lik bir MaxFieldNodes sabitini uygular ve ikisinden biri ayrıştırmayı durdurabilir
const
XfdfMaxTopLevelFields = 100000; // format-yerel, her zaman uygulanır
...
// İki tavan da canlıdır; hangisi düşükse o kazanır
if (FieldCount >= XfdfMaxTopLevelFields) or
(FieldCount >= Budget.MaxObjects) then
Exit(False);
Bu bilinçlidir ve var olmayan bir düğmeyi aramaya gitmeden önce içselleştirmeye değer. 4.000.000 nesnelik bir global bütçe, bir XFDF dosyasına 4.000.000 alan bahşetmez, çünkü 100000 alan gerçek bir ek açıklama dışa aktarımının ürettiği her şeyin zaten çok dışındadır ve global bir tavanı yükseltmek format-spesifik bir akıl sağlığı kontrolünü sessizce genişletmemelidir. İçerik normalizasyonu diğer yönden aynı şekilde çalışır: TPdfContentNormalizeOptions.Default kendi sayfa-başına limitlerinden başlar ve sonra her birini snapshot'a kadar kıstırır; böylece MaxTokensPerPage bütçe tarafından yalnızca düşürülebilir, asla yükseltilemez. Global politika sıkılaştırır; asla gevşetmez
Kötü yapılandırma, yapılandırdığında başarısız olur
ConfigurePdfParserResourceBudget atamadan önce doğrular. Altı sayısal boyuttan herhangi birinde pozitif olmayan bir değer anında yükselir ve MaxDecodedBytes değerinin High(Integer) üzerinde olması, bir decoder'ın derinliklerinde sessizce kırpılmak yerine platform limitlerini aştığı için reddedilir. Bu, duyulduğundan daha önemlidir: eksik anahtarlı bir config dosyasından kurulan bir bütçe sana sıfırlar verir ve başlangıçta kabul edilen sıfır nesne tavanı üç saat sonra ilk belgede akıl karıştırıcı bir ayrıştırma hatasına dönüşür; o noktada kimse config'den şüphelenmez. Aynı özen, bütçeyi geçici olarak sıkılaştıran her koda uygulanır, testler dahil. Politika süreç genelindedir; bu yüzden bir fixture bittikten sonra 4 saniyelik bir deadline'ı kurulu bırakmak sonraki her şeyi zehirler; değişikliğin etrafında kaydet ve geri yükle
var
Saved: TPdfParserResourceBudget;
begin
Saved := GetPdfParserResourceBudget;
try
ApplyUntrustedIntakeBudget;
ProcessInboundDocument(FileName);
finally
ConfigurePdfParserResourceBudget(Saved);
end;
end;
ResetPdfParserResourceBudget daha kaba versiyondur ve TPdfParserResourceBudget.Default değerini doğrudan geri yükler. Bilinen bir durum istediğin test teardown'unda onu kullan ve üretim kodunda kaydet-ve-geri-yükle kullan; orada host uygulamanın başlangıçta yapılandırdığı her ne ise dönmen gereken durum odur
Bütçenin yardım etmeyi bıraktığı yer
Tek bir süreç geneli politikadır ve ana sınırlaması budur. Bir worker thread'e sıkı bir profil, diğerine hoşgörülü bir profil veremezsin, çünkü tek bir kritik bölümün arkasında tek bir kayıt vardır; gerçekten kiracı başına limit gerekiyorsa işe yarayan biçim, her politika sınıfını kendi sürecine serileştirmek ya da işi kaydet-ve-geri-yükle ile parantezlemek ve parantezin yalnızca başka hiçbir şey eşzamanlı ayrıştırmadığında güvenli olduğunu kabul etmektir. Ayrıca ayrıştırmayı sınırlar, render'ı değil: rasterizasyon belleği, font cache büyümesi ve görüntü çözme bu yedi boyutun dışındadır ve kendi kontrollerine ihtiyaç duyar. Ve bir bütçe sana bir işlemin çok pahalı olduğunu söyler, nedenini asla. Ateşlenen tavan belirtidir ve arkasındaki bozuk /Index dizisi ya da kaçak filtre zinciri yine de elle teşhis edilmek zorundadır; nesne stream ve predictor çözme yazısında anlatılan çözme yolunu sayıları oynatmaya başlamadan önce anlamaya değer kılan da budur. Bütçeyi, kötü bir belgenin süreci onunla birlikte aşağı çekmesini engelleyen şey olarak ele al ve belgenin ne yaptığını bulacak kadar iyi log tut
Tam parser API yüzeyi, bütçe kaydı ve Delphi, C++Builder ve Lazarus için kaynak kodlu bir build ürün sayfasında: PDFium Component