Exit code: 0 Wall time: 37.1 seconds Output: Šest klicev PDFium, ki so v Delphiju pozabili na ključavnico izrisa

Tehnični članak

Šest klicev PDFium, ki so v Delphiju pozabili na ključavnico izrisa

Ključavnica izrisa PDFiumPas je kritični odsek za posamezen dokument — EnterRenderLock in LeaveRenderLock, podprta s poljem TRTLCriticalSection v TPdf — ki naj bi ovil vsak klic rasterizatorja PDFium, da strani ni mogoče odstraniti ali znova naložiti med izrisom, ki še poteka. Šest metod, enakomerno razdeljenih med TPdf in TPdfView, je neposredno klicalo API-je PDFium za pridobivanje bitnih slik in sličic ter v celoti preskočilo to ključavnico, kar je PDFiumPas v2.26.0 odpravil tako, da je vseh šest ovil v isti par ključavnic, ki so ga že uporabljale druge vstopne točke za izris

Obravnavana vrzel ni del utrjevanja ABI, opisanega drugje na tem spletnem dnevniku, kjer sta bila obravnavana neusklajenost klicne konvencije cdecl in skrajšanje širine kazalca FPC Win64 v isti vezavi PDFium. Nadaljevanje je ožje in bolj mehansko: kontrolni seznam pokritosti s ključavnico za šest klicnih mest, ki vsa dosežejo izrisno pot PDFium, razlog, zakaj je bilo vsako od njih zlahka spregledati, ter razlog, zakaj je tekmovalno stanje zaradi manjkajoče ključavnice ena težje ponovljivih napak v tej zbirki kode

Kako ključavnica izrisa dejansko ščiti

PDFiumPas izris zaporedno usklajuje, ker naložene strani PDFium ni varno brati v eni niti, medtem ko jo lahko druga nit sprosti. TPdf ima v FRenderLock objekt TRTLCriticalSection, inicializiran v konstruktorju in varovan z zastavico FRenderLockReady, zato klic po razgradnji postane tiha prazna operacija namesto vstopa v izbrisan kritični odsek. EnterRenderLock in LeaveRenderLock sta edini dovoljeni poti v ta odsek in iz njega

procedure TPdf.EnterRenderLock;
begin
  if FRenderLockReady then
    EnterCriticalSection(FRenderLock);
end;

procedure TPdf.LeaveRenderLock;
begin
  if FRenderLockReady then
    LeaveCriticalSection(FRenderLock);
end;

TPdf.RenderPage, RenderTile in RenderPageProgressive so to disciplino upoštevali že pred začetkom te revizije, saj vsaka metoda prevzame ključavnico pred klicem v PDFium in jo sprosti v bloku finally, zato se predizris v ozadju in osprednji UnloadPage na istem primerku TPdf ne moreta prekrivati. Vrzel, ki jo je odkril PDFiumPas v2.26.0, ni bila v teh očitnih vstopnih točkah — pojavila se je v šestih metodah, ki so bile videti kot dostopniki, ne kot izrisi, čeprav mora vsaka od njih pred vrnitvijo rezultata od PDFium zahtevati rasterizacijo slikovnih pik

Katerih šest klicev je preskočilo ključavnico izrisa

TPdf.GetObjectBitmap, TPdf.GetBitmap in TPdf.GetThumbnail so sestavljali polovico seznama, drugo polovico pa TPdfView.GetObjectBitmap, TPdfView.GetBitmap in TPdfView.GetThumbnail — iste tri operacije, podvojene v razredih obeh komponent, ki izpostavljata isto osnovno stran. Vseh šest prej ali slej pokliče FPDFImageObj_GetBitmap ali FPDFPage_GetThumbnailAsBitmap, obe vstopni točki PDFium pa rasterizirata takoj, namesto da bi vrnili sklic na nekaj že izrisanega. V imenih teh šestih metod ni ničesar, kar bi nakazovalo izris, zato je razumljivo, da pri prvem pisanju niso bile vključene na isti kontrolni seznam kot RenderPage in RenderTile

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;

Zakaj TPdfView klic ključavnice varuje s preverjanjem nil

TPdfView nima lastnega kritičnega odseka — vseh šest njegovih klicev ključavnice posreduje v FPdf.EnterRenderLock in FPdf.LeaveRenderLock, vendar šele po preverjanju, da povezani sklic TPdf ni nil. Varovalo obstaja, ker je lahko TPdfView med načrtovanjem na obrazcu ali kratek čas med zapiranjem enega dokumenta in odpiranjem naslednjega, ko v FPdf še ni dodeljen noben TPdf. Preskok varovala bi eno sesutje zamenjal za drugo, saj klic ključavnice prek sklica nil ni nič bolj varen od tekmovalnega stanja, ki naj bi ga ključavnica preprečila

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;

Zakaj je RenderPage(HDC) del iste revizije

TPdfView.RenderPage za kontekst naprave ni eden od šestih klicev — pojavil se je že v prejšnji izdaji, v PDFiumPas v2.25.0, vendar sodi na ta kontrolni seznam, ker gre za isto napako z drugačnim podpisom. Ta preobremenitev je neposredno poklicala FPDF_RenderPage brez EnterRenderLock in brez klica SetArithmeticMask, ki pri starejših prevajalnikih Delphi varuje pred izjemami FPU, medtem ko je preobremenitev za TBitmap nekaj vrstic niže v istem razredu že vsebovala oboje. Dve reviziji, ki sta v razmiku ene izdaje odkrili isti način odpovedi, povesta manj o posamezni metodi in več o obliki napake: skriva se v tisti preobremenitvi, ki je nihče več ne pregleda, ko je njena sorodna različica videti pravilna

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;

Zakaj je to tekmovalno stanje skoraj nemogoče ponoviti

Vrzel v ključavnici izrisa PDFiumPas ne povzroči napake pri vsakem zagonu ali celo pri večini zagonov, ker morata na istem primerku TPdf hkrati nastopiti dve stvari: klic rasterizacije mora že potekati, sočasni UnloadPage ali ReloadPage pa mora prispeti v istem časovnem oknu. Enonitno testiranje te poti sploh ne izvede, tudi resnično večnitne obremenitve pa jo sprožijo le, ko se izris v ozadju in dogodek življenjskega cikla dokumenta prekrivata med življenjsko dobo ene strani. Najverjetnejši sprožilec je predizris PDF v ozadju, zgrajen na preklicljivih prihodnjih opravilih, pri katerem delovna nit rasterizira naslednjo stran, uporabniška nit pa ob vnosu uporabnika znova naloži ali odstrani trenutno stran

FPDFImageObj_GetBitmap in FPDFPage_GetThumbnailAsBitmap prehajata po strukturah predmetov strani, ki jih lahko UnloadPage sprosti sredi prehoda, zato dejansko sproženo tekmovalno stanje tudi ne povzroči vedno takojšnje kršitve dostopa. Prepozno branje strukture lahko vrne popačene slikovne pike ali poškoduje metapodatke kopice, sesutje pa se zgodi šele pri več nepovezanih dodelitvah v funkciji, ki se strani PDF sploh ni dotaknila. To je iskren razlog, zakaj lahko takšna napaka preživi več izdaj: sled sklada na mestu odpovedi redko kaže blizu šestih vrstic, ki so jim dejansko manjkale ključavnice

Kaj to pomeni za klicatelje

GetBitmap, GetObjectBitmap, GetThumbnail in preobremenitev RenderPage za HDC ohranijo svoje javne podpise, saj je popravek notranja dodana ključavnica okoli obstoječih klicev, ne selitev API-ja. Ne pozabite, da velja ključavnica izrisa za posamezen primerek TPdf, ne globalno za proces, zato lahko dve niti, ki izrisujeta dva ločeno naložena dokumenta, še vedno delujeta povsem vzporedno — ključavnica zaporedno izvaja le operacije nad dokumentom, ki si ga niti delita. Če so vaše ključavnice že pravilne, izris pa je pri povečavi ali drsenju še vedno počasen, gre za drugo vprašanje, na katero odgovarja članek o predpomnilniku izrisa PDFium in zmogljivosti povečave — pravilnost in hitrost sta ločeni osi, ta popravek pa vpliva samo na prvo

Šest metod in ena sorodna preobremenitev predstavljajo majhen del površine PDFium, ki jo izpostavlja PDFiumPas, vendar so bile prav to metode, ki so se napačno obnašale samo pod obremenitvijo, ki je nihče ni izvajal v razhroščevalniku. Sama ključavnica izrisa in celoten nabor vstopnih točk izrisa, ki jih zdaj pokriva, sta del komponente PDFium Component za Delphi, C++Builder in Lazarus/FPC