Skip to content

Commit 82a9e5b

Browse files
tacaswellmeeseeksmachine
authored andcommitted
Backport PR matplotlib#32062: Don't pre-allocate the Type1 /Subrs array from the declared count
1 parent a2fcca5 commit 82a9e5b

2 files changed

Lines changed: 93 additions & 23 deletions

File tree

lib/matplotlib/_type1font.py

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -612,30 +612,48 @@ def _parse_subrs(self, tokens, _data):
612612
f"Token following /Subrs must be a number, was {count_token}"
613613
)
614614
count = count_token.value()
615-
array = [None] * count
616615
next(t for t in tokens if t.is_keyword('array'))
617-
for _ in range(count):
618-
next(t for t in tokens if t.is_keyword('dup'))
619-
index_token = next(tokens)
620-
if not index_token.is_number():
621-
raise RuntimeError(
622-
"Token following dup in Subrs definition must be a "
623-
f"number, was {index_token}"
624-
)
625-
nbytes_token = next(tokens)
626-
if not nbytes_token.is_number():
627-
raise RuntimeError(
628-
"Second token following dup in Subrs definition must "
629-
f"be a number, was {nbytes_token}"
630-
)
631-
token = next(tokens)
632-
if not token.is_keyword(self._abbr['RD']):
633-
raise RuntimeError(
634-
f"Token preceding subr must be {self._abbr['RD']}, "
635-
f"was {token}"
636-
)
637-
binary_token = tokens.send(1+nbytes_token.value())
638-
array[index_token.value()] = binary_token.value()
616+
# Accumulate the parsed subrs into a dict and only allocate the result
617+
# list once the body has been read. Allocating ``[None] * count`` up
618+
# front lets a malformed font declare a huge count in a few bytes and
619+
# force a large allocation before it is rejected.
620+
entries = {}
621+
try:
622+
for _ in range(count):
623+
next(t for t in tokens if t.is_keyword('dup'))
624+
index_token = next(tokens)
625+
if not index_token.is_number():
626+
raise RuntimeError(
627+
"Token following dup in Subrs definition must be a "
628+
f"number, was {index_token}"
629+
)
630+
nbytes_token = next(tokens)
631+
if not nbytes_token.is_number():
632+
raise RuntimeError(
633+
"Second token following dup in Subrs definition must "
634+
f"be a number, was {nbytes_token}"
635+
)
636+
token = next(tokens)
637+
if not token.is_keyword(self._abbr['RD']):
638+
raise RuntimeError(
639+
f"Token preceding subr must be {self._abbr['RD']}, "
640+
f"was {token}"
641+
)
642+
binary_token = tokens.send(1+nbytes_token.value())
643+
entries[index_token.value()] = binary_token.value()
644+
except StopIteration:
645+
raise RuntimeError(
646+
"Malformed Type1 font file: Incomplete /Subrs"
647+
) from None
648+
649+
# The indices must cover 0 to count-1 exactly.
650+
if (len(entries) != count
651+
or (count and (min(entries), max(entries)) != (0, count - 1))):
652+
raise RuntimeError(
653+
"Malformed Type1 font file: /Subrs indices do not cover "
654+
f"0 to {count - 1}"
655+
)
656+
array = [entries[index] for index in range(count)]
639657

640658
return array, next(tokens).endpos()
641659

lib/matplotlib/tests/test_type1font.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,55 @@ def test_encrypt_decrypt_roundtrip():
158158
decrypted = t1f.Type1Font._decrypt(encrypted, 'eexec')
159159
assert encrypted != decrypted
160160
assert data == decrypted
161+
162+
163+
def _write_pfa(path, private):
164+
"""Write a minimal font whose eexec-encrypted part is *private*."""
165+
plaintext = b'/FontName /X def\n/FontBBox [0 0 1 1] def\n' + private
166+
enc = t1f.Type1Font._encrypt(plaintext, 'eexec').hex().encode()
167+
path.write_bytes(b'%!PS-AdobeFont-1.0: X 001.000\neexec\n' + enc + b'\n'
168+
+ b'0' * 512 + b'\ncleartomark\n')
169+
return str(path)
170+
171+
172+
def test_Subrs_no_preallocation(tmp_path):
173+
# Regression test for #31962: a font declaring a huge /Subrs count must not
174+
# cause a large allocation sized from that (untrusted) count before the
175+
# body is parsed. Here the body is empty, so parsing must fail without
176+
# first allocating a count-sized array.
177+
import tracemalloc
178+
path = _write_pfa(tmp_path / 'x.pfa', b'/Subrs 5000000 array\n')
179+
180+
tracemalloc.start()
181+
with pytest.raises(RuntimeError, match='Incomplete /Subrs'):
182+
t1f.Type1Font(path)
183+
_, peak = tracemalloc.get_traced_memory()
184+
tracemalloc.stop()
185+
# The pre-allocation regressed to ~40 MB ([None] * count) before failing.
186+
assert peak < 5 * 1024 * 1024
187+
188+
189+
def _write_subrs_pfa(path, indices):
190+
"""Write a font with a two-element /Subrs array declared at *indices*."""
191+
return _write_pfa(path, (
192+
b'/Subrs 2 array\n'
193+
+ b''.join(b'dup %d 5 RD \x00\x01\x02\x03\x04 NP\n' % index
194+
for index in indices)
195+
+ b'ND\n'
196+
b'/CharStrings 1 begin\n'
197+
b'/.notdef 5 RD \x00\x01\x02\x03\x04 ND\n'
198+
b'end\n'
199+
))
200+
201+
202+
def test_Subrs_indices(tmp_path):
203+
font = t1f.Type1Font(_write_subrs_pfa(tmp_path / 'x.pfa', (0, 1)))
204+
assert len(font.prop['Subrs']) == 2
205+
206+
207+
@pytest.mark.parametrize('indices', [(0, 5), (0, 0), (-1, 1)])
208+
def test_Subrs_bad_indices(tmp_path, indices):
209+
# The declared indices must cover 0 to count-1 exactly, so neither an index
210+
# past the end nor a duplicate may reach the returned array.
211+
with pytest.raises(RuntimeError, match='indices do not cover'):
212+
t1f.Type1Font(_write_subrs_pfa(tmp_path / 'x.pfa', indices))

0 commit comments

Comments
 (0)