From 82a9e5b990e24b32444ac14dbd481e6baaa2483a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 31 Jul 2026 13:54:06 -0400 Subject: [PATCH] Backport PR #32062: Don't pre-allocate the Type1 /Subrs array from the declared count --- lib/matplotlib/_type1font.py | 64 +++++++++++++++++--------- lib/matplotlib/tests/test_type1font.py | 52 +++++++++++++++++++++ 2 files changed, 93 insertions(+), 23 deletions(-) diff --git a/lib/matplotlib/_type1font.py b/lib/matplotlib/_type1font.py index c7b73f9c0c7e..0b5403a3720a 100644 --- a/lib/matplotlib/_type1font.py +++ b/lib/matplotlib/_type1font.py @@ -612,30 +612,48 @@ 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')) - for _ in range(count): - next(t for t in tokens if t.is_keyword('dup')) - index_token = next(tokens) - if not index_token.is_number(): - raise RuntimeError( - "Token following dup in Subrs definition must be a " - f"number, was {index_token}" - ) - nbytes_token = next(tokens) - if not nbytes_token.is_number(): - raise RuntimeError( - "Second token following dup in Subrs definition must " - f"be a number, was {nbytes_token}" - ) - token = next(tokens) - if not token.is_keyword(self._abbr['RD']): - raise RuntimeError( - f"Token preceding subr must be {self._abbr['RD']}, " - f"was {token}" - ) - binary_token = tokens.send(1+nbytes_token.value()) - array[index_token.value()] = binary_token.value() + # 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. + entries = {} + try: + for _ in range(count): + next(t for t in tokens if t.is_keyword('dup')) + index_token = next(tokens) + if not index_token.is_number(): + raise RuntimeError( + "Token following dup in Subrs definition must be a " + f"number, was {index_token}" + ) + nbytes_token = next(tokens) + if not nbytes_token.is_number(): + raise RuntimeError( + "Second token following dup in Subrs definition must " + f"be a number, was {nbytes_token}" + ) + token = next(tokens) + if not token.is_keyword(self._abbr['RD']): + raise RuntimeError( + f"Token preceding subr must be {self._abbr['RD']}, " + f"was {token}" + ) + binary_token = tokens.send(1+nbytes_token.value()) + entries[index_token.value()] = binary_token.value() + except StopIteration: + raise RuntimeError( + "Malformed Type1 font file: Incomplete /Subrs" + ) from None + + # The indices must cover 0 to count-1 exactly. + if (len(entries) != count + or (count and (min(entries), max(entries)) != (0, count - 1))): + raise RuntimeError( + "Malformed Type1 font file: /Subrs indices do not cover " + f"0 to {count - 1}" + ) + array = [entries[index] for index in range(count)] return array, next(tokens).endpos() diff --git a/lib/matplotlib/tests/test_type1font.py b/lib/matplotlib/tests/test_type1font.py index b2f93ef28a26..067e9640e409 100644 --- a/lib/matplotlib/tests/test_type1font.py +++ b/lib/matplotlib/tests/test_type1font.py @@ -158,3 +158,55 @@ def test_encrypt_decrypt_roundtrip(): decrypted = t1f.Type1Font._decrypt(encrypted, 'eexec') assert encrypted != decrypted assert data == decrypted + + +def _write_pfa(path, private): + """Write a minimal font whose eexec-encrypted part is *private*.""" + plaintext = b'/FontName /X def\n/FontBBox [0 0 1 1] def\n' + private + enc = t1f.Type1Font._encrypt(plaintext, 'eexec').hex().encode() + path.write_bytes(b'%!PS-AdobeFont-1.0: X 001.000\neexec\n' + enc + b'\n' + + b'0' * 512 + b'\ncleartomark\n') + return str(path) + + +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 + path = _write_pfa(tmp_path / 'x.pfa', b'/Subrs 5000000 array\n') + + tracemalloc.start() + with pytest.raises(RuntimeError, match='Incomplete /Subrs'): + t1f.Type1Font(path) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + # The pre-allocation regressed to ~40 MB ([None] * count) before failing. + assert peak < 5 * 1024 * 1024 + + +def _write_subrs_pfa(path, indices): + """Write a font with a two-element /Subrs array declared at *indices*.""" + return _write_pfa(path, ( + b'/Subrs 2 array\n' + + b''.join(b'dup %d 5 RD \x00\x01\x02\x03\x04 NP\n' % index + for index in indices) + + b'ND\n' + b'/CharStrings 1 begin\n' + b'/.notdef 5 RD \x00\x01\x02\x03\x04 ND\n' + b'end\n' + )) + + +def test_Subrs_indices(tmp_path): + font = t1f.Type1Font(_write_subrs_pfa(tmp_path / 'x.pfa', (0, 1))) + assert len(font.prop['Subrs']) == 2 + + +@pytest.mark.parametrize('indices', [(0, 5), (0, 0), (-1, 1)]) +def test_Subrs_bad_indices(tmp_path, indices): + # The declared indices must cover 0 to count-1 exactly, so neither an index + # past the end nor a duplicate may reach the returned array. + with pytest.raises(RuntimeError, match='indices do not cover'): + t1f.Type1Font(_write_subrs_pfa(tmp_path / 'x.pfa', indices))