Skip to content

Commit 5755d0f

Browse files
gh-150599: Prevent bz2 decompressor reuse after errors (#150600)
1 parent e5ced1f commit 5755d0f

3 files changed

Lines changed: 33 additions & 3 deletions

File tree

Lib/test/test_bz2.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,21 @@ def test_failure(self):
10321032
# Previously, a second call could crash due to internal inconsistency
10331033
self.assertRaises(Exception, bzd.decompress, self.BAD_DATA * 30)
10341034

1035+
def test_decompress_after_data_error(self):
1036+
data = bytes.fromhex(
1037+
"425a6839314159265359000000000000007fffff000000000000000000000000"
1038+
"00000000000000000000000000000000000000e0370000000000000000000000"
1039+
"000000000000000000000000000000000000000000000000000083f3"
1040+
)
1041+
bzd = BZ2Decompressor()
1042+
with self.assertRaisesRegex(OSError, "Invalid data stream"):
1043+
bzd.decompress(data)
1044+
# Previously, a second call could crash due to internal inconsistency
1045+
self.assertFalse(bzd.needs_input)
1046+
self.assertFalse(bzd.eof)
1047+
with self.assertRaisesRegex(ValueError, "previous error"):
1048+
bzd.decompress(b'\x00' * 18)
1049+
10351050
@support.refcount_test
10361051
def test_refleaks_in___init__(self):
10371052
gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a possible stack buffer overflow in :mod:`bz2` when a
2+
:class:`bz2.BZ2Decompressor` is reused after a decompression error.
3+
The decompressor now becomes unusable after libbz2 reports an error.

Modules/_bz2module.c

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ typedef struct {
108108
typedef struct {
109109
PyObject_HEAD
110110
bz_stream bzs;
111+
int bzerror;
111112
char eof; /* Py_T_BOOL expects a char */
112113
PyObject *unused_data;
113114
char needs_input;
@@ -435,8 +436,11 @@ decompress_buf(BZ2Decompressor *d, Py_ssize_t max_length)
435436

436437
d->bzs_avail_in_real += bzs->avail_in;
437438

438-
if (catch_bz2_error(bzret))
439+
if (catch_bz2_error(bzret)) {
440+
d->bzerror = bzret;
441+
FT_ATOMIC_STORE_CHAR_RELAXED(d->needs_input, 0);
439442
goto error;
443+
}
440444
if (bzret == BZ_STREAM_END) {
441445
FT_ATOMIC_STORE_CHAR_RELAXED(d->eof, 1);
442446
break;
@@ -607,10 +611,17 @@ _bz2_BZ2Decompressor_decompress_impl(BZ2Decompressor *self, Py_buffer *data,
607611
PyObject *result = NULL;
608612

609613
PyMutex_Lock(&self->mutex);
610-
if (self->eof)
614+
if (self->eof) {
611615
PyErr_SetString(PyExc_EOFError, "End of stream already reached");
612-
else
616+
}
617+
else if (self->bzerror) {
618+
// Re-entering BZ2_bzDecompress() after an error can write out of bounds.
619+
PyErr_SetString(PyExc_ValueError,
620+
"Decompressor is unusable after a previous error");
621+
}
622+
else {
613623
result = decompress(self, data->buf, data->len, max_length);
624+
}
614625
PyMutex_Unlock(&self->mutex);
615626
return result;
616627
}
@@ -638,6 +649,7 @@ _bz2_BZ2Decompressor_impl(PyTypeObject *type)
638649
}
639650

640651
self->mutex = (PyMutex){0};
652+
self->bzerror = 0;
641653
self->needs_input = 1;
642654
self->bzs_avail_in_real = 0;
643655
self->input_buffer = NULL;

0 commit comments

Comments
 (0)