Skip to content

texttotext: guard page_size against PageWidth/PageHeight overflow - #248

Merged
tillkamppeter merged 2 commits into
OpenPrinting:masterfrom
afonsojanu:fix/texttotext-pagewidth-overflow
Sep 14, 2026
Merged

tillkamppeter merged 2 commits into
OpenPrinting:masterfrom
afonsojanu:fix/texttotext-pagewidth-overflow

Conversation

@afonsojanu

Copy link
Copy Markdown
Contributor

What's going on

cfFilterTextToText() reads PageWidth and PageHeight straight from the
job options and only checks that they're positive:

i = atoi(val);
if (i > 0)
  num_columns = i;

There's no upper bound, so a job can ask for a page that's a billion
columns wide. That value feeds directly into the output page buffer size a
few lines later:

page_size = ((num_columns + 2) * num_lines + 2) * 4;
out_page = calloc(page_size, sizeof(char));

page_size is a plain int. For a page reported as PageWidth=1073741824 PageHeight=1, the multiplication wraps around instead of producing
something calloc() would balk at, so out_page ends up 16 bytes instead
of the ~4 GB the arithmetic actually meant. The formatting loop right after
still writes according to the real, unwrapped num_columns, so it walks
straight off the end of that 16-byte buffer while filling in the first
line of text.

I ran this through the real cfFilterTextToText() entry point (not a
re-implementation) built with AddressSanitizer + UBSan and confirmed it:

texttotext.c:774:51: runtime error: signed integer overflow: 1073741828 * 4 cannot be represented in type 'int'
==...==ERROR: AddressSanitizer: heap-buffer-overflow ...
WRITE of size 1 at ...
    #0 ... in cfFilterTextToText texttotext.c:1088
0x... is located 0 bytes after 16-byte region [...]
allocated by thread T0 here:
    #0 ... in calloc ...
    #1 ... in cfFilterTextToText texttotext.c:777

This looks like the same underlying issue reported in #198, just triggered
through the more direct PageWidth/PageHeight options path rather than
through the media-size/chars-per-inch calculation.

Fix

Reject page dimensions whose product would overflow the page_size
computation, and fall back to the existing default (80 columns x 66
lines), the same way the function already falls back when PageWidth or
PageHeight come back non-positive. The check is done in 64-bit before
num_columns/num_lines ever feed the int multiplication, so it can't
itself be fooled by the same kind of wraparound.

Testing

Added cupsfilters/test-texttotext-page-overflow.c and its .sh driver,
modeled directly on the existing test-pdftoraster-copy-height regression
test: the harness #includes the real texttotext.c so it's driving the
actual cfFilterTextToText() code, built under AddressSanitizer.

  • Against the unpatched source, the harness reproduces the
    heap-buffer-overflow above and aborts.
  • With the fix applied, cfFilterTextToText() logs the fallback and
    returns success with no ASan report.

Wired the new script into check_SCRIPTS/TESTS in Makefile.am next to
the other ASan-based regression tests, and added the .c harness to
EXTRA_DIST so it ships in make dist like the pdftoraster one does. The
script skips (exit 77) when AddressSanitizer isn't usable in the build
environment, matching the existing tests' convention.

cfFilterTextToText() takes "PageWidth" and "PageHeight" straight from the
job options with only a check that they are positive. Nothing caps how
large they can be, so a job that asks for an absurdly wide or tall page
(no special privilege needed, this is a plain job option) sails through
into the output-page-buffer size calculation:

    page_size = ((num_columns + 2) * num_lines + 2) * 4;

which is computed in a plain int. For a page reported as roughly a
billion columns by one line, that multiplication wraps around instead of
producing something calloc() would reject, so the allocation ends up
only a handful of bytes. The formatting loop right after it still writes
according to the real, unwrapped column count, so it walks straight past
the end of the buffer.

Reject page dimensions that would overflow that computation and fall
back to the existing default page size (80x66) the same way the function
already falls back when PageWidth/PageHeight are non-positive, using
64-bit arithmetic to check the product before it is ever narrowed to int.

Added cupsfilters/test-texttotext-page-overflow.c/.sh, following the same
shape as the existing test-pdftoraster-copy-height regression test: the
harness #includes the real texttotext.c so it drives the actual
cfFilterTextToText() entry point under AddressSanitizer. Without the fix
it aborts with a heap-buffer-overflow on the very first character
written to the page; with it, the function falls back cleanly and
returns success.
@tillkamppeter

Copy link
Copy Markdown
Member

Generally, your fix looks OK for me, but:

  • The tests on x86_64 and on arm64 are failing for all CUPS versions
  • Instead of adding a new test program, you can perhaps trigger this bug by adding an appropriate line to cupsfilters/test-filter-cases.txt, using option settings as described here.

….txt

Replace the standalone ASan-only regression test with a case in the
project's own test-filter-cases.txt harness. It was failing CI on
x86_64/arm64 because its link line hardcoded -liconv, which doesn't
exist as a separate library on glibc (iconv is part of libc there);
armv7/riscv64 only "passed" because they skip before reaching that
link step, since ASan can't run under their QEMU emulation.

Wires "texttotext" into testfilters.c's filter_mappings so a
FilterChain entry can drive cfFilterTextToText() directly, then adds
a case with PageWidth=1073741824/PageHeight=1 to trigger the same
overflow described in the bug report through that existing mechanism.

Verified locally (both x86_64 and arm64, via the real autotools build
and testfilters harness): reverting the texttotext.c fix makes
cfFilterChain report the texttotext step crashing on signal 6 for
this case ("free(): invalid next size"); with the fix, it falls back
to the default page size and the case passes.
@afonsojanu

Copy link
Copy Markdown
Contributor Author

Switched to the test-filter-cases.txt approach and dropped the standalone test program along with its Makefile.am wiring. Wired texttotext into testfilters.c's filter_mappings table so a FilterChain entry can drive cfFilterTextToText() directly, then added a line using PageWidth=1073741824 PageHeight=1 (same values as in the report) to hit the overflow through that existing mechanism.

On what was actually failing: the x86_64/arm64 legs were dying because the old standalone test's link line hardcoded -liconv, which isn't a separate library on glibc (iconv lives in libc there), so the linker just couldn't find it. armv7/riscv64 never hit that because ASan can't run under their QEMU emulation, so that test skipped (exit 77) before ever reaching the link step. Removing the standalone program removes that link line entirely, so it's not something the new approach can run into again.

Verified locally on both x86_64 and arm64 with the real autotools build: with the source fix reverted, cfFilterChain reports the texttotext step crashing on signal 6 for this case (glibc catches the corruption, "free(): invalid next size"), and with the fix in place it falls back to the default page size and the case passes cleanly.

@tillkamppeter
tillkamppeter merged commit 00d6ca4 into OpenPrinting:master Sep 14, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants