PDFiumPas' gengivelseslås er en per-dokument kritisk sektion — EnterRenderLock og LeaveRenderLock, bakket op af et TRTLCriticalSection-felt på TPdf — ment til at pakke hvert kald ind i PDFiums rasterizer, så en side ikke kan blive aflæst eller genindlæst under en igangværende gengivelse. Seks metoder delt jævnt mellem TPdf og TPdfView kaldte PDFiums bitmap- og miniaturebillede-udtrækknings-API'er direkte og sprang den lås helt over, et hul PDFiumPas v2.26.0 lukkede ved at pakke alle seks ind i det samme lås-par, hver anden gengivelses-indgangspunkt allerede brugte
Hullet dækket her er ikke den ABI-hærdnings-gennemgang dækket andre steder på denne blog, som gennemgik en cdecl-kaldekonvention-uoverensstemmelse og en FPC Win64-pointer-bredde-afkortning i den samme PDFium-binding. Det følgende er snævrere og mere mekanisk: en lås-dæknings-tjekliste for seks kaldesteder, der alle rækker ind i PDFiums gengivelsesvej, hvorfor hver var let at overse, og hvorfor det kapløb, der følger af den manglende lås, er en af de sværere defekter i denne kodebase at genskabe efter behov
Hvad gengivelseslåsen rent faktisk beskytter
PDFiumPas serialiserer gengivelse, fordi PDFiums indlæste side ikke er sikker at læse fra én tråd, mens en anden tråd frit kan frigive den. TPdf ejer en TRTLCriticalSection i FRenderLock, initialiseret i konstruktøren og bevogtet af et FRenderLockReady-flag, så et kald der ankommer efter nedlukning bliver en tavs no-op frem for at gå ind i en slettet kritisk sektion. EnterRenderLock og LeaveRenderLock er den eneste godkendte vej ind og ud af den sektion
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile og RenderPageProgressive fulgte allerede den disciplin, før netop denne audit nogensinde begyndte, hver tog låsen, før den kaldte ind i PDFium, og frigav den i en finally-blok, så en baggrunds-forgengivelse og en forgrunds-UnloadPage på den samme TPdf-instans ikke kan overlappe. Hullet PDFiumPas v2.26.0 fandt, var ikke i de indlysende indgangspunkter — det dukkede op i seks metoder, der læser som accessorer frem for gengivelser, selvom hver af dem beder PDFium om at rasterisere pixels, før den kan returnere noget som helst
Hvilke seks kald sprang gengivelseslåsen over?
TPdf.GetObjectBitmap, TPdf.GetBitmap og TPdf.GetThumbnail udgjorde halvdelen af listen, og TPdfView.GetObjectBitmap, TPdfView.GetBitmap og TPdfView.GetThumbnail udgjorde den anden halvdel — de samme tre operationer, duplikeret på tværs af de to komponentklasser, der eksponerer den samme underliggende side. Alle seks kalder til sidst enten FPDFImageObj_GetBitmap eller FPDFPage_GetThumbnailAsBitmap, og begge de PDFium-indgangspunkter rasteriserer på stedet frem for at overdrage en reference til noget allerede gengivet. Intet i noget af de seks metodenavne siger render, hvilket er en fornuftig forklaring på, hvorfor de ikke blev skrevet mod den samme tjekliste som RenderPage og RenderTile første gang
function TPdf.GetObjectBitmap(Index: Integer): TBitmap;
var
Bitmap: FPDF_BITMAP;
begin
Result:= nil;
EnterRenderLock;
try
Bitmap:= FPDFImageObj_GetBitmap(GetObjectHandle(Index));
finally
LeaveRenderLock;
end;
if Bitmap<> nil then
try
Result:= ToBitmap(Bitmap);
finally
FPDFBitmap_Destroy(Bitmap);
end;
end;
Hvorfor bevogter TPdfView sit lås-kald med et nil-tjek
TPdfView ejer ikke sin egen kritiske sektion — hvert af dens seks lås-kald videresender til FPdf.EnterRenderLock og FPdf.LeaveRenderLock, pakket ind i et tjek af, at den tilknyttede TPdf-reference ikke er nil først. Den vagt findes, fordi en TPdfView kan sidde på en formular ved designtidspunkt, eller kortvarigt mellem ét dokument der lukker og det næste der åbner, uden nogen TPdf tildelt til FPdf endnu. At springe vagten over ville bytte ét crash for et andet, da et låse-kald mod en nil-reference ikke fejler mere elegant end det kapløb, låsen findes for at forhindre
function TPdfView.GetThumbnail: TBitmap;
var
PdfBitmap: FPDF_BITMAP;
begin
CheckActive;
Result:= nil;
if FPdf<> nil then
FPdf.EnterRenderLock;
try
PdfBitmap:= FPDFPage_GetThumbnailAsBitmap(Page);
finally
if FPdf<> nil then
FPdf.LeaveRenderLock;
end;
if PdfBitmap<> nil then
try
Result:= ToBitmap(PdfBitmap);
finally
FPDFBitmap_Destroy(PdfBitmap);
end;
end;
Hvorfor hører RenderPage(HDC) hjemme i den samme audit?
TPdfView.RenderPage mod en device-kontekst er ikke en af de seks — den dukkede op en udgivelse tidligere, i PDFiumPas v2.25.0, og den fortjener en plads i denne tjekliste, fordi det er den samme defekt, der bærer en anden signatur. Den overload kaldte FPDF_RenderPage lige igennem uden hverken EnterRenderLock eller det SetArithmeticMask-kald, der bevogter mod FPU-undtagelser på ældre Delphi-kompilere, mens TBitmap-overloaden siddende nogle linjer nedenunder i den samme klasse allerede bar begge dele. To audit-gennemløb, der fanger den samme fejltilstand en udgivelse fra hinanden, siger mindre om nogen enkelt metode og mere om bugens form: den gemmer sig i den overload, ingen genlæser, når dens søskende ser korrekt ud
procedure TPdfView.RenderPage(DeviceContext: HDC; Left, Top, Width,
Height: Integer; Rotation: TRotation; Options: TRenderOptions);
var
ArithmeticMask: TArithmeticMask;
begin
CheckActive;
if FPdf<> nil then
FPdf.EnterRenderLock;
ArithmeticMask:= SetArithmeticMask;
try
FPDF_RenderPage(DeviceContext, FPage, Left, Top, Width, Height,
Ord(Rotation), EncodeRenderOptions(Options));
finally
RestoreArithmeticMask(ArithmeticMask);
if FPdf<> nil then
FPdf.LeaveRenderLock;
end;
end;
Hvorfor er dette kapløb næsten umuligt at genskabe?
PDFiumPas' gengivelseslås-hul fejler ikke ved hvert kørsel, eller selv de fleste kørsler, fordi det kræver to specifikke ting, der lander på den samme TPdf-instans på samme tid: et rasteriseringskald allerede i gang, og en samtidig UnloadPage eller ReloadPage der ankommer inden for det samme vindue. Enkelt-trådet testning udøver aldrig vejen overhovedet, og selv genuint multi-trådede arbejdsbyrder udløser den kun, når en baggrundsgengivelse og en dokument-livscyklus-hændelse tilfældigvis overlapper inden for én sides levetid. Den mest realistiske udløser er baggrunds-PDF-forgengivelse bygget på annullerbare futures, hvor en arbejdstråd rasteriserer den næste side, mens UI-tråden genindlæser eller aflæser den aktuelle på brugerinput
FPDFImageObj_GetBitmap og FPDFPage_GetThumbnailAsBitmap gennemgår side-objekt-strukturer, som UnloadPage frit kan frigive midt i gennemgangen, så et kapløb der rent faktisk udløses, producerer ikke altid en øjeblikkelig adgangskrænkelse heller. En struktur læst et øjeblik for sent kan lige så let give skrammel-pixels tilbage, eller ødelægge heap-metadata, der først crasher flere urelaterede allokeringer senere, i en funktion der aldrig rørte en PDF-side. Det er den ærlige grund til, at denne klasse af bug kan overleve i en kodebase på tværs af flere udgivelsescyklusser: stack trace'en på fejlpunktet peger sjældent nogen steder i nærheden af de seks linjer, der rent faktisk manglede en lås
Hvad ændrer sig for kaldere
GetBitmap, GetObjectBitmap, GetThumbnail, og HDC-overloaden af RenderPage beholder deres offentlige signaturer nøjagtig som de var, da fixen er intern låsning tilføjet omkring eksisterende kald frem for en migrering. Det er værd at huske, at gengivelseslåsen er afgrænset per TPdf-instans, ikke global til processen, så to tråde der gengiver to separat indlæste dokumenter, kører stadig fuldt parallelt — låsen serialiserer kun operationer mod det ene dokument begge tråde tilfældigvis deler. Hvis ens låsning allerede er sund, og gengivelser stadig føles langsomme under zoom eller scroll, er det et andet spørgsmål, besvaret i artiklen om PDFium-gengivelses-cache og zoom-ydeevne-taktikker — korrekthed og hastighed er separate akser her, og denne fix rører kun den første
Seks metoder og én søskende-overload er en lille brøkdel af den PDFium-flade, PDFiumPas eksponerer, men de var den brøkdel, der kun opførte sig forkert under belastning, ingen tilfældigvis kørte i en debugger. Selve gengivelseslåsen, og det fulde sæt gengivelses-indgangspunkter, den nu dækker, leveres som en del af PDFium-komponenten til Delphi, C++Builder og Lazarus/FPC