EnsurePageCount panics with nil pointer dereference when catalog object fails to parse
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.
# 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)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:2191Diagnosis
Each layer assumes "err == nil ⇒ value non-nil", but the invariant is broken at the first one:
Per spec 7.3.10,
indRefToObjectreturns the null object without error for the unresolvable root reference.Catalog()(xreftable.go:1097–1100, v0.15.0) propagates this as(nil, nil):o, _, err := xRefTable.indRefToObject(xRefTable.Root, true) if err != nil || o == nil { return nil, err }Pages()(xreftable.go:1145) callsIndirectRefEntry("Pages")on the nil dict and returns(nil, nil).EnsurePageCount()(xreftable.go:2191) dereferences*pageRootwithout a nil check: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.
Source: pdfcpu/pdfcpu