Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 41 additions & 23 deletions lib/matplotlib/_type1font.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
52 changes: 52 additions & 0 deletions lib/matplotlib/tests/test_type1font.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Loading