Technical Article

Six PDFium Calls That Forgot the Render Lock in Delphi

PDFiumPas's render lock is a per-document critical section — EnterRenderLock and LeaveRenderLock, backed by a TRTLCriticalSection field on TPdf — meant to wrap every call into PDFium's rasterizer so a page cannot be unloaded or reloaded out from under an in-flight render. Six methods split evenly between TPdf and TPdfView called PDFium's bitmap and thumbnail extraction APIs directly and skipped that lock entirely, a gap that PDFiumPas v2.26.0 closed by wrapping all six in the same lock pair every other rendering entry point already used

The gap covered here is not the ABI-hardening pass covered elsewhere on this blog, which walked through a cdecl calling-convention mismatch and an FPC Win64 pointer-width truncation in the same PDFium binding. What follows is narrower and more mechanical: a lock-coverage checklist for six call sites that all reach into PDFium's rendering path, why each one was easy to miss, and why the race that follows from missing the lock is one of the harder defects in this codebase to reproduce on demand

What the Render Lock Actually Protects

PDFiumPas serializes rendering because PDFium's loaded page is not safe to read from one thread while another thread is free to release it. TPdf owns a TRTLCriticalSection in FRenderLock, initialized in the constructor and guarded by an FRenderLockReady flag so a call arriving after teardown becomes a silent no-op instead of entering a deleted critical section. EnterRenderLock and LeaveRenderLock are the only sanctioned way in and out of that section

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

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

TPdf.RenderPage, RenderTile, and RenderPageProgressive already followed that discipline before this particular audit ever started, each one taking the lock before calling into PDFium and releasing it in a finally block so a background pre-render and a foreground UnloadPage on the same TPdf instance cannot overlap. The gap that PDFiumPas v2.26.0 found was not in those obvious entry points — it turned up in six methods that read like accessors rather than renders, even though every one of them asks PDFium to rasterize pixels before it can return anything

Which Six Calls Skipped the Render Lock?

TPdf.GetObjectBitmap, TPdf.GetBitmap, and TPdf.GetThumbnail made up half the list, and TPdfView.GetObjectBitmap, TPdfView.GetBitmap, and TPdfView.GetThumbnail made up the other half — the same three operations, duplicated across the two component classes that expose the same underlying page. All six eventually call either FPDFImageObj_GetBitmap or FPDFPage_GetThumbnailAsBitmap, and both of those PDFium entry points rasterize on the spot rather than handing back a reference to something already rendered. Nothing in any of the six method names says render, which is a reasonable explanation for why they were not written against the same checklist as RenderPage and RenderTile the first time around

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;

Why TPdfView Guards Its Lock Call with a Nil Check

TPdfView does not own a critical section of its own — every one of its six lock calls forwards to FPdf.EnterRenderLock and FPdf.LeaveRenderLock, wrapped in a check that the associated TPdf reference is not nil first. That guard exists because a TPdfView can sit on a form at design time, or briefly between one document closing and the next opening, with no TPdf assigned to FPdf yet. Skipping the guard would trade one crash for another, since a locking call against a nil reference fails no more gracefully than the race the lock exists to prevent

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;

Why Does RenderPage(HDC) Belong in the Same Audit?

TPdfView.RenderPage against a device context is not one of the six — it turned up a release earlier, in PDFiumPas v2.25.0, and it earns a place in this checklist because it is the same defect wearing a different signature. That overload called FPDF_RenderPage straight through with neither EnterRenderLock nor the SetArithmeticMask call that guards against FPU exceptions on older Delphi compilers, while the TBitmap overload sitting a few lines below it in the same class already carried both. Two audit passes catching the same failure mode a release apart says less about any single method and more about the shape of the bug: it hides in whichever overload nobody re-reads once its sibling looks correct

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;

Why Is This Race Nearly Impossible to Reproduce?

PDFiumPas's render-lock gap does not fail on every run, or even on most runs, because it needs two specific things to land on the same TPdf instance at once: a rasterization call already in flight, and a concurrent UnloadPage or ReloadPage arriving inside that same window. Single-threaded testing never exercises the path at all, and even genuinely multi-threaded workloads only trip it when a background render and a document lifecycle event happen to overlap within one page's lifetime. The most realistic trigger is background PDF pre-rendering built on cancellable futures, where a worker thread rasterizes the next page while the UI thread reloads or unloads the current one on user input

FPDFImageObj_GetBitmap and FPDFPage_GetThumbnailAsBitmap walk page-object structures that UnloadPage is free to release mid-traversal, so a race that actually fires does not always produce an immediate access violation either. A structure read a moment too late can just as easily hand back garbage pixels, or corrupt heap metadata that only crashes several unrelated allocations later, in a function that never touched a PDF page. That is the honest reason this class of bug can survive in a codebase across several release cycles: the stack trace at the point of failure rarely points anywhere near the six lines that were actually missing a lock

What Changes for Callers

GetBitmap, GetObjectBitmap, GetThumbnail, and the HDC overload of RenderPage keep their public signatures exactly as they were, since the fix is internal locking added around existing calls rather than a migration. It is worth remembering that the render lock is scoped per TPdf instance, not global to the process, so two threads rendering two separately loaded documents still run fully in parallel — the lock only serializes operations against the one document both threads happen to share. If your locking is already sound and renders still feel slow under zoom or scroll, that is a different question, answered in the PDFium render cache and zoom performance tactics article — correctness and speed are separate axes here, and this fix only touches the first one

Six methods and one sibling overload are a small fraction of the PDFium surface that PDFiumPas exposes, but they were the fraction that only misbehaved under load nobody happened to be running in a debugger. The render lock itself, and the full set of rendering entry points it now covers, ship as part of the PDFium Component for Delphi, C++Builder, and Lazarus/FPC