Skip to content

Commit bd9d844

Browse files
committed
Keep FDICT zlib headers out of the invalid-input test and pin them
Fuzzing the zlib tests with Atheris produced a zlib header with the FDICT flag set within seconds, and zlib._ZlibDecompressor turns that into SystemError ("error return without exception set") because the Z_NEED_DICT branch returns NULL without an exception. That is the same defect as gh-160001, reached without any dictionary at all. Random invalid input hits it often enough to make test_invalid_input flaky for the zlib wrapper backend, and in a debug build the missing exception is a fatal C assertion rather than a failure. The invalid-input check now asks the class whether to skip a blob, and the zlib wrapper companion skips valid headers with FDICT set. The carve-out is pinned by test_fdict_header, which expects zlib.error as zlib.decompressobj raises for the same input, and which like test_zlib_wrapper can only run in a release build.
1 parent e03f55a commit bd9d844

1 file changed

Lines changed: 43 additions & 2 deletions

File tree

‎Lib/test/test_compression_property.py‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,13 +451,24 @@ def test_chunked_compress_round_trip(self, data, sizes):
451451
sizes=[], max_lengths=[])
452452
def test_invalid_input(self, garbage, data, index, byte, sizes,
453453
max_lengths):
454+
self._check_invalid_input(garbage, data, index, byte, sizes,
455+
max_lengths)
456+
457+
def _skip_invalid_blob(self, blob):
458+
# Hook for a known bug: a mixin may rule out inputs that crash.
459+
return False
460+
461+
def _check_invalid_input(self, garbage, data, index, byte, sizes,
462+
max_lengths):
454463
stream = self.backend.compress(data)
455464
index %= len(stream) + 1
456465
if index == len(stream):
457466
corrupted = stream[:byte % (len(stream) + 1)]
458467
else:
459468
corrupted = stream[:index] + bytes([byte]) + stream[index + 1:]
460469
for blob in (garbage, corrupted):
470+
if self._skip_invalid_blob(blob):
471+
continue
461472
with self.subTest(blob=blob):
462473
decomp = self.backend.new_decompressor()
463474
try:
@@ -519,6 +530,36 @@ def test_needs_input_means_drained(self, data, sizes, max_lengths):
519530
self._check_needs_input_means_drained(data, sizes, max_lengths)
520531

521532

533+
def _zlib_fdict_header(blob):
534+
# A valid zlib header whose FDICT flag is set.
535+
return (len(blob) >= 2 and blob[0] & 0x0f == 8 and blob[0] >> 4 <= 7
536+
and int.from_bytes(blob[:2]) % 31 == 0 and bool(blob[1] & 0x20))
537+
538+
539+
class _ZlibNeedsDictBug:
540+
# zlib._ZlibDecompressor returns NULL without an exception when inflate
541+
# asks for a preset dictionary (gh-160001). Any zlib-wrapped input whose
542+
# header has FDICT set gets there, so random invalid input would hit it,
543+
# and in a debug build the missing exception is a fatal C assertion.
544+
# test_invalid_input leaves those inputs out; test_fdict_header pins the
545+
# bug and can only run in a release build.
546+
def _skip_invalid_blob(self, blob):
547+
return _zlib_fdict_header(blob)
548+
549+
@unittest.skipIf(support.Py_DEBUG, "gh-160001 aborts debug builds")
550+
@unittest.expectedFailure
551+
@hypothesis.given(data=st.binary(max_size=64))
552+
@hypothesis.example(data=b"")
553+
@hypothesis.example(data=b"\x00\x00\x00\x01\x03\x00")
554+
def test_fdict_header(self, data):
555+
# zlib.decompressobj raises zlib.error for the same input.
556+
stream = b"\x78\xbb" + data
557+
with self.assertRaises(zlib.error):
558+
zlib.decompressobj().decompress(stream)
559+
with self.assertRaises(zlib.error):
560+
zlib._ZlibDecompressor().decompress(stream)
561+
562+
522563
# liblzma reports an error when two consecutive calls can make no progress,
523564
# which lzma.LZMADecompressor lets through as LZMAError("Insufficient buffer
524565
# space") for two max_length=0 calls in a row with nothing buffered
@@ -608,8 +649,8 @@ class LZMADecompressorTests(_LZMAMaxLengthZeroBug, _DecompressorTests,
608649

609650

610651
@support.requires_zlib()
611-
class ZlibDecompressorTests(_ZlibNeedsInputBug, _DecompressorTests,
612-
unittest.TestCase):
652+
class ZlibDecompressorTests(_ZlibNeedsInputBug, _ZlibNeedsDictBug,
653+
_DecompressorTests, unittest.TestCase):
613654
backend = _zlib_backend(15)
614655

615656

0 commit comments

Comments
 (0)