De render lock van PDFiumPas is een kritieke sectie per document — EnterRenderLock en LeaveRenderLock, ondersteund door een TRTLCriticalSection-veld op TPdf — bedoeld om elke aanroep naar de rasterizer van PDFium te omhullen zodat een pagina niet onderuit een lopende render kan worden ontladen of herladen. Zes methoden, gelijk verdeeld over TPdf en TPdfView, riepen de bitmap- en thumbnail-extractie-API's van PDFium rechtstreeks aan en sloegen die lock volledig over, een gat dat PDFiumPas v2.26.0 dichtte door alle zes in hetzelfde lockpaar te wikkelen dat elk ander render-toegangspunt al gebruikte
Het hier behandelde gat is niet de ABI-verhardingsronde die elders op deze blog wordt behandeld, die een mismatch in de cdecl-aanroepconventie en een FPC Win64-pointerbreedte-afkapping in dezelfde PDFium-binding doornam. Wat volgt is smaller en mechanischer: een lock-dekkingschecklist voor zes aanroeppunten die allemaal in het renderpad van PDFium reiken, waarom elk daarvan gemakkelijk over het hoofd werd gezien, en waarom de race die volgt op het ontbreken van de lock een van de moeilijkere defecten in deze codebase is om op verzoek te reproduceren
Wat de render lock daadwerkelijk beschermt
PDFiumPas serialiseert rendering omdat de geladen pagina van PDFium niet veilig is om vanuit de ene thread te lezen terwijl een andere thread vrij is om deze vrij te geven. TPdf bezit een TRTLCriticalSection in FRenderLock, geïnitialiseerd in de constructor en bewaakt door een vlag FRenderLockReady, zodat een aanroep die na de afbraak binnenkomt een stille no-op wordt in plaats van een verwijderde kritieke sectie binnen te gaan. EnterRenderLock en LeaveRenderLock zijn de enige goedgekeurde manier in en uit die sectie
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile, en RenderPageProgressive volgden die discipline al voordat deze specifieke audit ooit begon, elk nam de lock voordat het PDFium aanriep en gaf deze vrij in een finally-blok, zodat een achtergrond-vooraf-render en een voorgrond-UnloadPage op dezelfde TPdf-instantie niet kunnen overlappen. Het gat dat PDFiumPas v2.26.0 vond, zat niet in die voor de hand liggende toegangspunten — het dook op in zes methoden die eerder als accessors lezen dan als renders, ook al vraagt elk daarvan PDFium om pixels te rasteriseren voordat er iets kan worden teruggegeven
Welke zes aanroepen sloegen de render lock over?
TPdf.GetObjectBitmap, TPdf.GetBitmap, en TPdf.GetThumbnail vormden de helft van de lijst, en TPdfView.GetObjectBitmap, TPdfView.GetBitmap, en TPdfView.GetThumbnail vormden de andere helft — dezelfde drie bewerkingen, gedupliceerd over de twee componentklassen die dezelfde onderliggende pagina blootstellen. Alle zes roepen uiteindelijk ofwel FPDFImageObj_GetBitmap ofwel FPDFPage_GetThumbnailAsBitmap aan, en beide van die PDFium-toegangspunten rasteriseren ter plekke in plaats van een verwijzing terug te geven naar iets dat al gerenderd is. Niets in een van de zes methodenamen zegt render, wat een redelijke verklaring is voor waarom ze de eerste keer niet tegen dezelfde checklist als RenderPage en RenderTile zijn geschreven
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;
Waarom bewaakt TPdfView zijn lock-aanroep met een nil-controle?
TPdfView bezit geen eigen kritieke sectie — elk van zijn zes lock-aanroepen stuurt door naar FPdf.EnterRenderLock en FPdf.LeaveRenderLock, verpakt in een controle die eerst nagaat dat de bijbehorende TPdf-verwijzing niet nil is. Die bewaking bestaat omdat een TPdfView op een formulier kan zitten tijdens ontwerptijd, of kortstondig tussen het sluiten van het ene document en het openen van het volgende, zonder dat er al een TPdf aan FPdf is toegewezen. De bewaking overslaan zou de ene crash voor de andere inruilen, aangezien een lock-aanroep tegen een nil-verwijzing niet minder hardhandig faalt dan de race die de lock beoogt te voorkomen
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;
Waarom hoort RenderPage(HDC) in dezelfde audit thuis?
TPdfView.RenderPage tegen een apparaatcontext is niet een van de zes — deze dook een release eerder op, in PDFiumPas v2.25.0, en verdient een plek in deze checklist omdat het hetzelfde defect is met een andere handtekening. Die overload riep FPDF_RenderPage rechtstreeks aan zonder EnterRenderLock en zonder de aanroep SetArithmeticMask die beschermt tegen FPU-uitzonderingen op oudere Delphi-compilers, terwijl de TBitmap-overload een paar regels lager in dezelfde klasse beide al droeg. Twee auditrondes die dezelfde faalmodus een release na elkaar oppikken, zegt minder over een enkele methode en meer over de vorm van de bug: die verschuilt zich in welke overload dan ook die niemand opnieuw leest zodra zijn broertje er correct uitziet
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;
Waarom is deze race bijna onmogelijk te reproduceren?
Het render-lock-gat van PDFiumPas faalt niet bij elke run, of zelfs niet bij de meeste runs, omdat er twee specifieke dingen tegelijk op dezelfde TPdf-instantie moeten samenkomen: een rasterisatie-aanroep die al gaande is, en een gelijktijdige UnloadPage of ReloadPage die binnen datzelfde venster arriveert. Single-threaded testen oefent het pad helemaal nooit uit, en zelfs werkelijk multi-threaded workloads triggeren het alleen wanneer een achtergrondrender en een documentlevenscyclusgebeurtenis toevallig overlappen binnen de levensduur van één pagina. De meest realistische trigger is achtergrond-PDF-vooraf-rendering gebouwd op annuleerbare futures, waar een workerthread de volgende pagina rasteriseert terwijl de UI-thread de huidige herlaadt of ontlaadt op gebruikersinvoer
FPDFImageObj_GetBitmap en FPDFPage_GetThumbnailAsBitmap doorlopen pagina-objectstructuren die UnloadPage vrij is om vrij te geven middenin de doorloop, dus een race die daadwerkelijk afgaat produceert ook niet altijd meteen een access violation. Een structuur die net iets te laat wordt gelezen, kan net zo gemakkelijk rommelpixels teruggeven, of heap-metadata beschadigen die pas verschillende ongerelateerde toewijzingen later crasht, in een functie die nooit een PDF-pagina heeft aangeraakt. Dat is de eerlijke reden waarom deze klasse bug een codebase over verschillende releasecycli heen kan overleven: de stacktrace op het moment van falen wijst zelden ergens in de buurt van de zes regels die daadwerkelijk een lock misten
Wat verandert er voor aanroepers
GetBitmap, GetObjectBitmap, GetThumbnail, en de HDC-overload van RenderPage behouden hun publieke signatures precies zoals ze waren, aangezien de fix interne locking is die rond bestaande aanroepen is toegevoegd in plaats van een migratie. Het is de moeite waard om te onthouden dat de render lock per TPdf-instantie is gescoped, niet globaal voor het proces, dus twee threads die twee afzonderlijk geladen documenten renderen, draaien nog steeds volledig parallel — de lock serialiseert alleen bewerkingen tegen het ene document dat beide threads toevallig delen. Als uw locking al deugdelijk is en renders nog steeds traag aanvoelen bij zoomen of scrollen, is dat een andere vraag, beantwoord in het artikel over de PDFium-rendercache en zoomprestatietactieken — correctheid en snelheid zijn hier aparte assen, en deze fix raakt alleen de eerste
Zes methoden en één zusteroverload zijn een klein deel van het PDFium-oppervlak dat PDFiumPas blootstelt, maar het waren precies de fractie die zich alleen misdroeg onder belasting die toevallig niemand in een debugger draaide. De render lock zelf, en de volledige set render-toegangspunten die deze nu dekt, worden geleverd als onderdeel van de PDFium-component voor Delphi, C++Builder, en Lazarus/FPC