PDFium Component eksponerer TPdfParserResourceBudget, én enkelt record der afgrænser rekursionsdybde, token-antal, objekt-antal, afkodede bytes, ekspansionsratio, allokering og en wall-clock-deadline for hver PDF-parser i biblioteket. ConfigurePdfParserResourceBudget sætter den atomisk for processen, og hver parse kopierer et snapshot så dens tærskler ikke kan flytte sig midt i operationen
Seks parsere, seks private lofter
Situationen der tvang dette frem, var hverdagsagtig og sikkert velkendt. Sparse-xref-gennemløb havde sit eget objekt-loft. Stream-inspektøren havde sin egen afkodet-byte-grænse. XMP havde én, XFDF og FDF en anden, content-normalisering en tredje og preflight-defaults en fjerde. Hvert af de tal var forsvarligt i sig selv, og ingen af dem kunne nås udefra, så en caller der ville sige "denne intake-sti håndterer anonyme uploads, intet her må allokere mere end 64 MiB eller køre længere end fire sekunder" havde intet sted at sige det. Det andet problem var værre, fordi det var lydløst: hvor en grænse nu levede i en mutabel global, kunne en parse der startede under én politik slutte under en anden. Én tråd kalder en setter mens en anden er midt i et object stream, og den første halvdel af det gennemløb er afgrænset af det gamle loft, mens anden halvdel er afgrænset af det nye. Intet crasher, intet logges, og bagefter kan du ikke sige hvilke grænser operationen faktisk kørte under — hvilket er præcis den egenskab du havde brug for grænserne til
Hvad afgrænser TPdfParserResourceBudget egentlig?
Syv dimensioner, valgt fordi hver enkelt fanger en klasse af input de andre slipper igennem. MaxRecursionDepth default 1024 og stopper indlejrede dictionaries og arrays fra at forvandle ISO 32000-1 §7.3-objektsyntaks til et stack overflow. MaxTokens default 10.000.000 og afgrænser lexing-arbejdet uafhængigt af filstørrelse. MaxObjects default 4.000.000 og dækker cross-reference-subsection-antal, object-stream-/N-værdier og graf-gennemløb. MaxDecodedBytes default 256 MiB og MaxAllocationBytes 512 MiB, så en fil kan afvises før allokeringen snarere end efter den. MaxExpansionRatio default 1000.0 og er den folk undervurderer: ISO 32000-1 §7.4-stream-filtre er generel komprimering, et par kilobyte Flate-data kan med rette ekspandere til hundredvis af megabyte, og et ratio-loft er standard-forsvaret mod en decompression bomb. Den er ortogonal i forhold til det absolutte byte-loft, fordi en 200 KiB-stream der vil blive til 200 MiB er under 256 MiB-loftet og stadig absurd. DeadlineMilliseconds default 0, hvilket betyder ingen deadline, så den dimension er strengt opt-in. Defaults'ene som sæt er bevidst generøse, dimensioneret til at holde almindelige forretningsdokumenter fra at ramme noget snarere end at være en hærdet profil; kommer dit input fra en offentlig upload-formular, vil du have noget tættere på tallene nedenfor, sammen med reglerne beskrevet i hærdning af en Delphi-PDF-preview-rude
uses
FPdfPdfCommon;
procedure ApplyUntrustedIntakeBudget;
var
Budget: TPdfParserResourceBudget;
begin
Budget := TPdfParserResourceBudget.Default;
// Defaults beholdt: MaxRecursionDepth 1024, MaxTokens 10000000,
// MaxObjects 4000000
Budget.MaxDecodedBytes := Int64(32) * 1024 * 1024; // default 256 MiB
Budget.MaxAllocationBytes := Int64(64) * 1024 * 1024; // default 512 MiB
Budget.MaxExpansionRatio := 50.0; // default 1000.0
Budget.DeadlineMilliseconds := 4000; // default 0 = fra
ConfigurePdfParserResourceBudget(Budget);
end;
Hvorfor tage et snapshot i stedet for at læse globalen?
Fordi en grænse der kan ændre sig mens den håndhæves, ikke er en grænse. Det er hele funktionens kerne-designpunkt, og det er én linje disciplin ved hvert entry point: læs politikken én gang ved operationens start ind i en lokal TPdfParserResourceBudget, og lad så hvert checkpoint sammenligne mod den lokale kopi. GetPdfParserResourceBudget tager critical section'en og giver en værdi-kopi af record'en tilbage, og derefter rører parsen aldrig mutabel global tilstand igen. Resultatet er at et kald til ConfigurePdfParserResourceBudget fra en anden tråd påvirker den næste operation, aldrig den der er i luften. Parallelle parses kører under forskellige politikker uden at forstyrre hinanden, en operation der fejlede på en ressource-grænse fejlede mod ét enkelt sammenhængende sæt tærskler, og der er ingen lock-contention på hot path'en, fordi låsen tages én gang per parse snarere end én gang per token
type
TCustomScanState = record
Budget: TPdfParserResourceBudget;
StartTick: Cardinal;
TokenCount: Int64;
Depth: Integer;
end;
procedure BeginScan(var S: TCustomScanState);
begin
S.Budget := GetPdfParserResourceBudget; // snapshot taget én gang, på forhånd
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;
Skriver du din egen parser oven på biblioteket, så kopier den form frem for at kalde GetPdfParserResourceBudget inde i din token-løkke. At kalde den per token er korrekt men meningsløst: du betaler en critical section ved hver iteration og genintroducerer den midt-operation-drift snapshot'et skulle forhindre
Hvorfor stopper et byte-loft ikke en patologisk fil?
Fordi størrelse og omkostning kun er løst forbundet i PDF. En 400 KiB-fil med en ISO 32000-1 §7.5.8-cross-reference-stream hvis /Index- og /W-arrays beskriver et plausibelt men fjendtligt indgangs-antal, en object-stream-kæde der genser de samme offsets, eller en content-stream af operatorer der hver koster mere end de ser ud til, sidder behageligt under ethvert byte- og objekt-loft du sætter og besætter stadig en worker-tråd i minutter. Byte-antal afgrænser hukommelse; de afgrænser ikke tid. Det er hvad PdfParserStartTick og PdfParserDeadlineReached er til. PdfParserStartTick fanger en tick-værdi ved operationens start, og PdfParserDeadlineReached sammenligner forløbet tid mod DeadlineMilliseconds og returnerer False når deadlinen er 0. Subtraktionen kører i unsigned-aritmetik, så et tick-counter-rollover producerer ikke en spurious timeout. Cross-reference-parsing, object-stream-afkodning, content-token-normalisering og FDF- og XFDF-læserne tjekker den alle ved deres løkke-hoveder, så en over-budget-parse stopper ved et checkpoint i stedet for at blive dræbt udefra
To ærlige forbehold. Det er wall-clock-tid, ikke CPU-tid, så en maskine der swapper eller en build-server under belastning kan udløse en deadline en stille maskine ville klare, hvilket taler for at sætte den godt over din observerede worst case frem for tæt på din median. Og den er fra som default, fordi en deadline der affyres på et legitimt stort dokument under månedsafslutnings-batchbehandling er en værre fejl end den den forhindrer. Opt in per intake-sti snarere end globalt, og par den med de strukturelle checks i validering af cross-reference streams
Budget'et er et loft, ikke en erstatning for format-grænser
Hver parser beholder sine egne format-specifikke konstanter og anvender begge. XFDF er det klareste eksempel: felt-læseren håndhæver XfdfMaxTopLevelFields og en MaxFieldNodes-konstant på 100000 ved siden af snapshot-værdien, og enhver af dem kan stoppe parsen
const
XfdfMaxTopLevelFields = 100000; // format-lokal, gælder altid
...
// Begge lofter er aktive; det laveste vinder
if (FieldCount >= XfdfMaxTopLevelFields) or
(FieldCount >= Budget.MaxObjects) then
Exit(False);
Det er bevidst og værd at internalisere før du går på jagt efter en knap der ikke findes. Et globalt budget på 4.000.000 objekter giver ikke en XFDF-fil 4.000.000 felter, fordi 100000 felter allerede er langt uden for hvad en ægte annotation-eksport producerer, og at hæve et globalt loft bør ikke lydløst udvide en format-specifik sanity-check. Content-normalisering virker på samme måde fra den anden retning: TPdfContentNormalizeOptions.Default starter fra sine egne per-page-grænser og klemmer så hver af dem ned mod snapshot'et, så MaxTokensPerPage kun nogensinde kan sænkes af budget'et, aldrig hæves. Den globale politik strammer; den løsner aldrig
Dårlig konfiguration fejler når du konfigurerer den
ConfigurePdfParserResourceBudget validerer før den tildeler. En ikke-positiv værdi i nogen af de seks numeriske dimensioner rejser med det samme, og MaxDecodedBytes over High(Integer) afvises som overskridelse af platform-grænserne snarere end lydløst at blive afkortet et eller andet sted dybt i en decoder. Det betyder mere end det lyder: et budget bygget fra en config-fil med en manglende nøgle giver dig nuller, og et nul-objekt-loft accepteret ved opstart bliver til en gådefuld parse-fejl på det første dokument tre timer senere, på hvilket tidspunkt ingen har mistanke til config'en. Den samme omhu gælder enhver kode der midlertidigt strammer budget'et, inklusive tests. Politikken er proces-dækkende, så at efterlade en 4-sekunders-deadline installeret efter en fixture er færdig forgifter enhver senere; gem og gendan omkring ændringen
var
Saved: TPdfParserResourceBudget;
begin
Saved := GetPdfParserResourceBudget;
try
ApplyUntrustedIntakeBudget;
ProcessInboundDocument(FileName);
finally
ConfigurePdfParserResourceBudget(Saved);
end;
end;
ResetPdfParserResourceBudget er den mere brute version, der gendanner TPdfParserResourceBudget.Default direkte. Brug den i test-teardown hvor du vil have en kendt tilstand, og brug save-and-restore i produktionskode, hvor uanset hvad host-applikationen konfigurerede ved opstart, er den tilstand du skal vende tilbage til
Hvor budget'et holder op med at hjælpe
Det er en enkelt proces-dækkende politik, og det er dens vigtigste begrænsning. Du kan ikke give én worker-tråd en streng profil og en anden en tilladende, for der er én record bag én critical section; har du reelt brug for per-tenant-grænser, er den form der virker at serialisere hver politik-klasse på sin egen proces, eller at indramme arbejde med save-and-restore og acceptere at indramningen kun er sikker når intet andet parser samtidig. Den afgrænser også parsing, ikke rendering: rasteriseringshukommelse, font-cache-vækst og billedafkodning ligger uden for de syv dimensioner og har brug for deres egne kontroller. Og et budget fortæller dig at en operation var for dyr, aldrig hvorfor. Loftet der affyrede er symptomet, og det fejlformede /Index-array eller den løbske filter-kæde bag det skal stadig diagnosticeres i hånden, hvilket er hvorfor afkodnings-stien beskrevet i object stream- og predictor-afkodning er værd at forstå før du begynder at flytte tal. Behandl budget'et som det der forhindrer et dårligt dokument i at tage processen med sig ned, og hold din logging god nok til at finde ud af hvad dokumentet lavede
Hele parser-API-overfladen, budget-record'en og en kildekode-bygning til Delphi, C++Builder og Lazarus er på produktsiden: PDFium Component