Skip to content

Commit 14a2690

Browse files
committed
gh-160314: Raise for a damaged later stream in lzma and bz2
A damaged .xz or bzip2 stream that followed another stream was taken for trailing data and silently ignored. Leftover data that starts with the stream magic is now treated as a stream, as gzip does.
1 parent 3b3c692 commit 14a2690

8 files changed

Lines changed: 110 additions & 9 deletions

File tree

‎Doc/library/bz2.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ The :mod:`!bz2` module contains:
191191
readers or writers, just like its equivalent classes in :mod:`gzip` and
192192
:mod:`lzma` have always been.
193193

194+
.. versionchanged:: next
195+
A damaged bzip2 stream that follows another stream now raises
196+
:exc:`OSError` instead of being ignored as trailing data.
197+
194198

195199
Incremental (de)compression
196200
---------------------------
@@ -307,6 +311,10 @@ One-shot (de)compression
307311
.. versionchanged:: 3.3
308312
Support for multi-stream inputs was added.
309313

314+
.. versionchanged:: next
315+
A damaged bzip2 stream that follows another stream now raises
316+
:exc:`OSError` instead of being ignored as trailing data.
317+
310318

311319
Miscellaneous
312320
-------------

‎Doc/library/lzma.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ Reading and writing compressed files
140140
.. versionchanged:: 3.6
141141
Accepts a :term:`path-like object`.
142142

143+
.. versionchanged:: next
144+
A damaged ``.xz`` stream that follows another stream now raises
145+
:exc:`LZMAError` instead of being ignored as trailing data.
146+
143147

144148
Compressing and decompressing data in memory
145149
--------------------------------------------
@@ -300,6 +304,10 @@ Compressing and decompressing data in memory
300304
See :class:`LZMADecompressor` above for a description of the *format*,
301305
*memlimit* and *filters* arguments.
302306

307+
.. versionchanged:: next
308+
A damaged ``.xz`` stream that follows another stream now raises
309+
:exc:`LZMAError` instead of being ignored as trailing data.
310+
303311

304312
Miscellaneous
305313
-------------

‎Lib/bz2.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
# Value 2 no longer used
2525
_MODE_WRITE = 3
2626

27+
# Every bzip2 stream starts with one of these, "BZh" and the block size.
28+
_STREAM_MAGIC = tuple(b"BZh%d" % level for level in range(1, 10))
29+
2730

2831
class BZ2File(_streams.BaseStream):
2932

@@ -91,7 +94,8 @@ def __init__(self, filename, mode="r", *, compresslevel=9):
9194

9295
if self._mode == _MODE_READ:
9396
raw = _streams.DecompressReader(self._fp,
94-
BZ2Decompressor, trailing_error=OSError)
97+
BZ2Decompressor, trailing_error=OSError,
98+
stream_magic=_STREAM_MAGIC)
9599
self._buffer = io.BufferedReader(raw)
96100
else:
97101
self._pos = 0
@@ -342,10 +346,11 @@ def decompress(data):
342346
try:
343347
res = decomp.decompress(data)
344348
except OSError:
345-
if results:
346-
break # Leftover data is not a valid bzip2 stream; ignore it.
347-
else:
349+
if not results:
348350
raise # Error on the first iteration; bail out.
351+
if data.startswith(_STREAM_MAGIC):
352+
raise # Leftover data is a damaged bzip2 stream; bail out.
353+
break # Leftover data is not a valid bzip2 stream; ignore it.
349354
results.append(res)
350355
if not decomp.eof:
351356
raise ValueError("Compressed data ended before the "

‎Lib/compression/_common/_streams.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ class DecompressReader(io.RawIOBase):
3636
def readable(self):
3737
return True
3838

39-
def __init__(self, fp, decomp_factory, trailing_error=(), **decomp_args):
39+
def __init__(self, fp, decomp_factory, trailing_error=(), *,
40+
stream_magic=(), **decomp_args):
4041
self._fp = fp
4142
self._eof = False
4243
self._pos = 0 # Current offset in decompressed stream
@@ -56,6 +57,11 @@ def __init__(self, fp, decomp_factory, trailing_error=(), **decomp_args):
5657
# trailing data to ignore
5758
self._trailing_error = trailing_error
5859

60+
# Prefix (or tuple of prefixes) that every compressed stream starts
61+
# with, if the format has one. Data starting with it is a damaged
62+
# stream rather than trailing data, so its error is not ignored.
63+
self._stream_magic = stream_magic
64+
5965
def close(self):
6066
self._decompressor = None
6167
return super().close()
@@ -90,6 +96,9 @@ def read(self, size=-1):
9096
try:
9197
data = self._decompressor.decompress(rawblock, size)
9298
except self._trailing_error:
99+
if (self._stream_magic
100+
and rawblock.startswith(self._stream_magic)):
101+
raise # Damaged stream, not trailing data.
93102
# Trailing data isn't a valid compressed stream; ignore it.
94103
break
95104
else:

‎Lib/lzma.py‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@
3737
_MODE_WRITE = 3
3838

3939

40+
def _stream_magic(format):
41+
# Every stream of an .xz file starts with these bytes. The other
42+
# formats, including an .lzma stream read with FORMAT_AUTO, have
43+
# nothing that tells a damaged stream from trailing data.
44+
return b"\xfd7zXZ\x00" if format in (FORMAT_AUTO, FORMAT_XZ) else b""
45+
46+
4047
class LZMAFile(_streams.BaseStream):
4148

4249
"""A file object providing transparent LZMA (de)compression.
@@ -130,7 +137,8 @@ def __init__(self, filename=None, mode="r", *,
130137

131138
if self._mode == _MODE_READ:
132139
raw = _streams.DecompressReader(self._fp, LZMADecompressor,
133-
trailing_error=LZMAError, format=format, filters=filters)
140+
trailing_error=LZMAError, stream_magic=_stream_magic(format),
141+
format=format, filters=filters)
134142
self._buffer = io.BufferedReader(raw)
135143

136144
def close(self):
@@ -347,15 +355,17 @@ def decompress(data, format=FORMAT_AUTO, memlimit=None, filters=None):
347355
For incremental decompression, use an LZMADecompressor instead.
348356
"""
349357
results = []
358+
magic = _stream_magic(format)
350359
while True:
351360
decomp = LZMADecompressor(format, memlimit, filters)
352361
try:
353362
res = decomp.decompress(data)
354363
except LZMAError:
355-
if results:
356-
break # Leftover data is not a valid LZMA/XZ stream; ignore it.
357-
else:
364+
if not results:
358365
raise # Error on the first iteration; bail out.
366+
if magic and data.startswith(magic):
367+
raise # Leftover data is a damaged XZ stream; bail out.
368+
break # Leftover data is not a valid LZMA/XZ stream; ignore it.
359369
results.append(res)
360370
if not decomp.eof:
361371
raise LZMAError("Compressed data ended before the "

‎Lib/test/test_bz2.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,22 @@ def testReadMultiStreamTrailingJunk(self):
156156
with BZ2File(self.filename) as bz2f:
157157
self.assertEqual(bz2f.read(), self.TEXT * 5)
158158

159+
def testReadMultiStreamDamagedStream(self):
160+
# A damaged bzip2 stream following a valid one is not trailing junk,
161+
# whatever the size of the reads.
162+
for pos in 4, 100, -3:
163+
with self.subTest(pos=pos):
164+
damaged = bytearray(self.DATA)
165+
damaged[pos] ^= 1
166+
self.createTempFile(suffix=damaged)
167+
with BZ2File(self.filename) as bz2f:
168+
self.assertRaises(OSError, bz2f.read)
169+
with BZ2File(self.filename) as bz2f:
170+
self.assertEqual(bz2f.read(len(self.TEXT)), self.TEXT)
171+
with self.assertRaises(OSError):
172+
while bz2f.read(10):
173+
pass
174+
159175
def testRead0(self):
160176
self.createTempFile()
161177
with BZ2File(self.filename) as bz2f:
@@ -1100,6 +1116,18 @@ def testDecompressMultiStreamTrailingJunk(self):
11001116
text = bz2.decompress(self.DATA * 5 + self.BAD_DATA)
11011117
self.assertEqual(text, self.TEXT * 5)
11021118

1119+
def testDecompressMultiStreamDamagedStream(self):
1120+
# A damaged bzip2 stream following a valid one is not trailing junk.
1121+
for pos in 4, 100, -3:
1122+
with self.subTest(pos=pos):
1123+
damaged = bytearray(self.DATA)
1124+
damaged[pos] ^= 1
1125+
self.assertRaises(OSError, bz2.decompress,
1126+
self.DATA + damaged)
1127+
# Not a bzip2 header: the block size digit must be 1 to 9.
1128+
text = bz2.decompress(self.DATA + b"BZh0" + self.BAD_DATA)
1129+
self.assertEqual(text, self.TEXT)
1130+
11031131

11041132
class OpenTest(BaseTest):
11051133
"Test the open function."

‎Lib/test/test_lzma.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,16 @@ def test_decompress_multistream_trailing_junk(self):
524524
ddata = lzma.decompress(COMPRESSED_XZ * 3 + COMPRESSED_BOGUS)
525525
self.assertEqual(ddata, INPUT * 3)
526526

527+
def test_decompress_multistream_damaged_stream(self):
528+
# A damaged .xz stream following a valid one is not trailing junk.
529+
for pos in 6, 100, -20, -1:
530+
damaged = bytearray(COMPRESSED_XZ)
531+
damaged[pos] ^= 1
532+
for format in lzma.FORMAT_AUTO, lzma.FORMAT_XZ:
533+
with self.subTest(pos=pos, format=format):
534+
self.assertRaises(LZMAError, lzma.decompress,
535+
COMPRESSED_XZ + damaged, format)
536+
527537

528538
class TempFile:
529539
"""Context manager - creates a file, and deletes it on __exit__."""
@@ -885,6 +895,25 @@ def test_read_multistream_trailing_junk(self):
885895
with LZMAFile(BytesIO(COMPRESSED_XZ * 5 + COMPRESSED_BOGUS)) as f:
886896
self.assertEqual(f.read(), INPUT * 5)
887897

898+
def test_read_multistream_damaged_stream(self):
899+
# A damaged .xz stream following a valid one is not trailing junk,
900+
# whatever the size of the reads.
901+
for pos in 6, 100, -20, -1:
902+
damaged = bytearray(COMPRESSED_XZ)
903+
damaged[pos] ^= 1
904+
cdata = COMPRESSED_XZ + damaged
905+
for format in lzma.FORMAT_AUTO, lzma.FORMAT_XZ:
906+
with self.subTest(pos=pos, format=format):
907+
with LZMAFile(BytesIO(cdata), format=format) as f:
908+
self.assertRaises(LZMAError, f.read)
909+
with LZMAFile(BytesIO(cdata), format=format) as f:
910+
self.assertEqual(f.read(len(INPUT)), INPUT)
911+
with self.assertRaises(LZMAError):
912+
while f.read(10):
913+
pass
914+
with LZMAFile(BytesIO(cdata), format=format) as f:
915+
self.assertRaises(LZMAError, f.seek, 0, 2)
916+
888917
def test_read_from_file(self):
889918
with TempFile(TESTFN, COMPRESSED_XZ):
890919
with LZMAFile(TESTFN) as f:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Raise an error in :class:`lzma.LZMAFile`, :func:`lzma.decompress`,
2+
:class:`bz2.BZ2File` and :func:`bz2.decompress` for a damaged ``.xz`` or bzip2
3+
stream that follows another stream, instead of silently ignoring it and
4+
everything after it as trailing data.

0 commit comments

Comments
 (0)