#1474·pdfcpu

EnsurePageCount panics with nil pointer dereference when catalog object fails to parse

Author: smileexpressionCreated Sep 6, 2026Updated Sep 7, 2026
Labelsbug

https://github.com/pdfcpu/pdfcpu/blob/105c0c28727afe7f85eb3179d03e9810d8774981/pkg/pdfcpu/model/xreftable.go#L1098

pdfcpu version: v0.11.1

Summary

Read/ReadWithContext succeeds without error on a PDF whose catalog object (the object referenced by trailer /Root) cannot be bound during parsing. A subsequent EnsurePageCount() panics with a nil pointer dereference.

Reproducer

The reproducer is a minimal, otherwise valid PDF with one defect: the catalog object is not terminated by endobj, so the object scanner cannot bind it and the root reference resolves to the null object.

python
# gen_reproducer.py — writes a 322-byte reproducer.pdf (pure ASCII)
objs = [
    b"<< /Type /Pages /Kids [2 0 R] /Count 1 >>\nendobj\n",
    b"<< /Type /Page /Parent 1 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
    b"<< /Type /Catalog /Pages 1 0 R >>\n",  # <-- no endobj: catalog cannot be bound
]
out = b"%PDF-1.4\n"
offsets = []
for i, body in enumerate(objs, 1):
    offsets.append(len(out))
    out += f"{i} 0 obj\n".encode() + body
xref = len(out)
out += b"xref\n0 4\n0000000000 65535 f \n"
for off in offsets:
    out += f"{off:010d} 00000 n \n".encode()
out += b"trailer\n<< /Size 4 /Root 3 0 R >>\nstartxref\n" + str(xref).encode() + b"\n%%EOF\n"
open("reproducer.pdf", "wb").write(out)
go
package main

import (
	"bytes"
	"context"
	"fmt"
	"os"

	"github.com/pdfcpu/pdfcpu/pkg/pdfcpu"
)

func main() {
	data, _ := os.ReadFile("reproducer.pdf")
	doc, err := pdfcpu.ReadWithContext(context.Background(), bytes.NewReader(data), nil)
	fmt.Println("read err:", err)          // read err: <nil>
	fmt.Println(doc.EnsurePageCount())     // panics
}

Output:

read err: <nil>
panic: runtime error: invalid memory address or nil pointer dereference

goroutine 1 [running]:
github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model.(*XRefTable).EnsurePageCount(...)
	.../[email protected]/pkg/pdfcpu/model/xreftable.go:2191

Diagnosis

Each layer assumes "err == nil ⇒ value non-nil", but the invariant is broken at the first one:

  1. Per spec 7.3.10, indRefToObject returns the null object without error for the unresolvable root reference.

  2. Catalog() (xreftable.go:1097–1100, v0.15.0) propagates this as (nil, nil):

    go
    o, _, err := xRefTable.indRefToObject(xRefTable.Root, true)
    if err != nil || o == nil {
        return nil, err
    }
  3. Pages() (xreftable.go:1145) calls IndirectRefEntry("Pages") on the nil dict and returns (nil, nil).

  4. EnsurePageCount() (xreftable.go:2191) dereferences *pageRoot without a nil check:

    go
    d, err := xRefTable.DereferenceDict(*pageRoot)

Expected behavior

An error instead of a panic for a PDF whose root object cannot be resolved.

Real-world context

We hit this in production with PDFs written by a specific PDF writer that omits endobj after the catalog object. Since Read accepts these files, the failure only surfaces later at EnsurePageCount, taking down the whole request.