Skip to content

[Bug]: Type1Font allocates /Subrs array from declared count before validating the body #31962

Description

@Str1ckl4nd

Bug summary

The Type1 font parser trusts the declared /Subrs count and allocates a Python list of that size before validating that the following body actually contains that many subroutine definitions. A compact malformed Type1 font can therefore cause avoidable large allocation before parsing fails.

Code for reproduction

import pathlib
import resource
import tempfile
import time

from matplotlib._type1font import Type1Font


def main():
    subrs_count = 5_000_000

    plaintext = (
        b"/FontName /LargeSubrsCount def\n"
        b"/FontBBox [0 0 1 1] def\n"
        + f"/Subrs {subrs_count} array\n".encode("ascii")
    )

    encrypted_hex = Type1Font._encrypt(plaintext, "eexec").hex().encode("ascii")
    pfa = (
        b"%!PS-AdobeFont-1.0: LargeSubrsCount 001.000\n"
        b"eexec\n"
        + encrypted_hex
        + b"\n"
        + (b"0" * 512)
        + b"\ncleartomark\n"
    )

    font_path = pathlib.Path(tempfile.mkdtemp()) / "large-subrs-count.pfa"
    font_path.write_bytes(pfa)

    rss_before = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
    start = time.time()
    try:
        Type1Font(str(font_path))
    except StopIteration:
        parse_result = "body_rejected_after_allocation"
    except MemoryError:
        parse_result = "memory_error"
    else:
        raise RuntimeError("malformed font unexpectedly parsed")

    elapsed = time.time() - start
    rss_after = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss

    print(f"subrs_count={subrs_count}")
    print(f"parse_result={parse_result}")
    print(f"elapsed_seconds={elapsed:.3f}")
    print(f"rss_before_kb={rss_before}")
    print(f"rss_after_kb={rss_after}")
    print(f"rss_delta_kb={rss_after - rss_before}")


if __name__ == "__main__":
    main()

Actual outcome

In a source-checkout verification run, a compact generated PFA declaring /Subrs 5000000 array caused the parser to allocate based on the declared count before the malformed body was rejected:

subrs_count=5000000
parse_result=body_rejected_after_allocation
elapsed_seconds=0.009
rss_before_kb=45168
rss_after_kb=84208
rss_delta_kb=39040

Expected outcome

Matplotlib should validate the /Subrs body structure before allocating a count-sized list, or enforce a reasonable maximum /Subrs count before allocation.

Additional information

The relevant parser path is lib/matplotlib/_type1font.py:

def _parse_subrs(self, tokens, _data):
    count_token = next(tokens)
    ...
    count = count_token.value()
    array = [None] * count
    next(t for t in tokens if t.is_keyword('array'))

The allocation happens immediately after reading the count token. The parser only confirms the array keyword and the following dup entries after the list has already been allocated.

This report is intended as a robustness bug, not as a security vulnerability report. It requires processing an untrusted Type1 PFA/PFB font.

Operating system

Linux

Matplotlib Version

main branch source checkout; reproduced on commit 5a33910eb7827d8a6188ff42bc788951c015ff48; same parser shape was still present on current main commit 996f4e877f6a7a50a68e00879183b8db8761461d.

Matplotlib Backend

Not backend-specific

Python version

Python 3.12

Jupyter version

Not applicable

Installation

git checkout

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions