Skip to content

Rendering a layer page that draws an image through an unpromoted tiling pattern dereferences null in CPDF_ImageLoader::Finish #84

Closed

Description

@roanutil

Rendering a page of a layer opened with EPDFLayer_OpenLayer crashes when the page paints an image through a tiling pattern the layer hasn't promoted. On native it's a SIGSEGV at address 0x18. In wasm the same read hits linear memory instead of trapping, so it's probably silent corruption, though I haven't confirmed that. The viewer opens every open.fatMem document as a fresh layer (WorkerHost.ts:965), so any such PDF opened in the viewer goes down this path.

Repro, at runtime 1461b0c51 (the C below is the equivalent of the Rust repro I ran; I haven't compiled this exact snippet). The file is examples/react/public/report.pdf from the viewer repo; page index 12 is its first page that uses one of these patterns:

EPDF_BASE_DOCUMENT base = EPDF_LoadMemBaseDocument64(bytes, size, NULL);
EPDFLayerOpenStatus status;
FPDF_DOCUMENT layer = EPDFLayer_OpenLayer(base, NULL, NULL, &status);
FPDF_PAGE page = FPDF_LoadPage(layer, 12);
FPDF_BITMAP bitmap = FPDFBitmap_Create(612, 792, 1);
FS_MATRIX m = {1, 0, 0, 1, 0, 0};
FS_RECTF clip = {0, 0, 612, 792};
FPDF_RenderPageBitmapWithMatrix(bitmap, page, &m, &clip, 0);  // SIGSEGV

The same page renders fine from a document opened with FPDF_LoadMemDocument. Annotations aren't involved: it crashes with FPDF_ANNOT off.

Cause. GetPattern falls back to the base's page data for an unpromoted pattern (cpdf_docpagedata.cpp:435-438). So the pattern's form, and the images in it, belong to the base document. GetImage deliberately doesn't fall back, for exactly this reason (:490-495), but an image reached through a pattern never goes through it. When the layer page draws the pattern, CPDF_RenderTiling::DrawPatternBitmap renders its form with the layer page's image cache. StartGetCachedBitmap sees the document mismatch and returns false without setting cur_image_cache_entry_ (cpdf_pageimagecache.cpp:177-180). CPDF_ImageLoader::Start reads false as "done" and calls Finish() (cpdf_imageloader.cpp:44), and DetachCurBitmap() reads cur_bitmap_ (offset 0x18) through the null entry.

The stack from lldb:

#0  CPDF_PageImageCache::DetachCurBitmap()
#1  CPDF_ImageLoader::Finish()
#2  CPDF_ImageLoader::Start(...)
#3  CPDF_ImageRenderer::StartLoadDIBBase()
#9  CPDF_RenderTiling::DrawPatternBitmap(...)
#11 CPDF_RenderStatus::DrawTilingPattern(...)
#21 FPDF_RenderPageBitmapWithMatrix

Fix. I see two options. The first is to stop GetPattern falling back for tiling patterns, keeping the "images are layer-local" rule GetImage's comment states. GetFont needs the same treatment for Type3 fonts whose glyph procedures draw images. That fix costs a little memory, since each layer then parses its own copy of a shared pattern. The second is narrower: have the cross-document branch in StartGetCachedBitmap load the image uncached instead of returning false with no entry. That leaves the fallback alone, but it keeps a path where an image's document and the page's disagree. I'm leaning towards the first, and I'm putting it in my fork with a regression test; I'm happy to open a PR here with it if that's useful.

Activity

  1. bobsingor commented on Oct 28, 2026

    @bobsingor
    Contributor

    Thanks for the detailed report. This is fixed by #85

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions