From cded0da7c27aa9e79c68d59c812083c3a8314638 Mon Sep 17 00:00:00 2001 From: Vladimir Saraikin Date: Fri, 10 Jul 2026 19:15:24 +0200 Subject: [PATCH] Don't pre-allocate the Subrs array from the declared count _parse_subrs allocated [None] * count from the count declared in the font before reading the body, so a malformed font declaring a huge count could force a large allocation before being rejected. Accumulate entries into a dict and build the list after the body parses, matching _parse_charstrings. --- lib/matplotlib/_type1font.py | 13 +++++++++++-- lib/matplotlib/tests/test_type1font.py | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/_type1font.py b/lib/matplotlib/_type1font.py index c7b73f9c0c7e..614f51d67ca2 100644 --- a/lib/matplotlib/_type1font.py +++ b/lib/matplotlib/_type1font.py @@ -612,8 +612,13 @@ def _parse_subrs(self, tokens, _data): f"Token following /Subrs must be a number, was {count_token}" ) count = count_token.value() - array = [None] * count next(t for t in tokens if t.is_keyword('array')) + # Accumulate the parsed subrs into a dict and only allocate the result + # list once the body has been read. Allocating ``[None] * count`` up + # front lets a malformed font declare a huge count in a few bytes and + # force a large allocation before it is rejected; _parse_charstrings + # already avoids this by accumulating into a dict. + entries = {} for _ in range(count): next(t for t in tokens if t.is_keyword('dup')) index_token = next(tokens) @@ -635,7 +640,11 @@ def _parse_subrs(self, tokens, _data): f"was {token}" ) binary_token = tokens.send(1+nbytes_token.value()) - array[index_token.value()] = binary_token.value() + entries[index_token.value()] = binary_token.value() + + array = [None] * count + for index, value in entries.items(): + array[index] = value return array, next(tokens).endpos() diff --git a/lib/matplotlib/tests/test_type1font.py b/lib/matplotlib/tests/test_type1font.py index b2f93ef28a26..7ba9ed53574d 100644 --- a/lib/matplotlib/tests/test_type1font.py +++ b/lib/matplotlib/tests/test_type1font.py @@ -158,3 +158,27 @@ def test_encrypt_decrypt_roundtrip(): decrypted = t1f.Type1Font._decrypt(encrypted, 'eexec') assert encrypted != decrypted assert data == decrypted + + +def test_Subrs_no_preallocation(tmp_path): + # Regression test for #31962: a font declaring a huge /Subrs count must not + # cause a large allocation sized from that (untrusted) count before the + # body is parsed. Here the body is empty, so parsing must fail without + # first allocating a count-sized array. + import tracemalloc + count = 5_000_000 + plaintext = (b'/FontName /X def\n/FontBBox [0 0 1 1] def\n' + + f'/Subrs {count} array\n'.encode()) + enc = t1f.Type1Font._encrypt(plaintext, 'eexec').hex().encode() + pfa = (b'%!PS-AdobeFont-1.0: X 001.000\neexec\n' + enc + b'\n' + + b'0' * 512 + b'\ncleartomark\n') + path = tmp_path / 'x.pfa' + path.write_bytes(pfa) + + tracemalloc.start() + with pytest.raises(StopIteration): + t1f.Type1Font(str(path)) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + # The pre-allocation regressed to ~40 MB ([None] * count) before failing. + assert peak < 5 * 1024 * 1024