Files
poky/meta/recipes-devtools/python/python3/CVE-2026-9669.patch
Benjamin Robin (Schneider Electric) 0c2fa146c5 python3: fix CVE-2026-9669
bz2.BZ2Decompressor objects could be reused after a decompression error.
If an application caught the resulting OSError and retried with the same
decompressor, crafted input could cause the decompressor to resume from an
invalid internal state and perform out-of-bounds writes to a stack buffer.
This could crash the process when processing untrusted data.

This CVE has a CVSS 4.0 score of 8.2. The patch (5755d0f08394) is
referenced in the CVEList database.

(From OE-Core rev: 226831c16d13133e89d3405b5e2298bb6571bed6)

Signed-off-by: Benjamin Robin (Schneider Electric) <benjamin.robin@bootlin.com>
Signed-off-by: Yoann Congal <yoann.congal@smile.fr>
Signed-off-by: Paul Barker <paul@pbarker.dev>
2026-07-22 21:08:54 +01:00

97 lines
3.4 KiB
Diff

From 5b412e1f7bdb3e0667b2bc8b216ad216d59d8373 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <stan@python.org>
Date: Mon, 8 Jun 2026 11:55:32 +0200
Subject: [PATCH] gh-150599: Prevent bz2 decompressor reuse after errors
(GH-150600)
CVE: CVE-2026-9669
Upstream-Status: Backport [https://github.com/python/cpython/commit/5755d0f083949ff3c5bf3a37e673e24e306b036e]
Signed-off-by: Benjamin Robin <benjamin.robin@bootlin.com>
---
Lib/test/test_bz2.py | 15 +++++++++++++++
Modules/_bz2module.c | 18 +++++++++++++++---
2 files changed, 30 insertions(+), 3 deletions(-)
diff --git a/Lib/test/test_bz2.py b/Lib/test/test_bz2.py
index cb730a1a46e2..dcbf6a298264 100644
--- a/Lib/test/test_bz2.py
+++ b/Lib/test/test_bz2.py
@@ -958,6 +958,21 @@ def test_failure(self):
# Previously, a second call could crash due to internal inconsistency
self.assertRaises(Exception, bzd.decompress, self.BAD_DATA * 30)
+ def test_decompress_after_data_error(self):
+ data = bytes.fromhex(
+ "425a6839314159265359000000000000007fffff000000000000000000000000"
+ "00000000000000000000000000000000000000e0370000000000000000000000"
+ "000000000000000000000000000000000000000000000000000083f3"
+ )
+ bzd = BZ2Decompressor()
+ with self.assertRaisesRegex(OSError, "Invalid data stream"):
+ bzd.decompress(data)
+ # Previously, a second call could crash due to internal inconsistency
+ self.assertFalse(bzd.needs_input)
+ self.assertFalse(bzd.eof)
+ with self.assertRaisesRegex(ValueError, "previous error"):
+ bzd.decompress(b'\x00' * 18)
+
@support.refcount_test
def test_refleaks_in___init__(self):
gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
diff --git a/Modules/_bz2module.c b/Modules/_bz2module.c
index 97bd44b4ac96..0b0916142f57 100644
--- a/Modules/_bz2module.c
+++ b/Modules/_bz2module.c
@@ -114,6 +114,7 @@ typedef struct {
typedef struct {
PyObject_HEAD
bz_stream bzs;
+ int bzerror;
char eof; /* T_BOOL expects a char */
PyObject *unused_data;
char needs_input;
@@ -453,8 +454,11 @@ decompress_buf(BZ2Decompressor *d, Py_ssize_t max_length)
d->bzs_avail_in_real += bzs->avail_in;
- if (catch_bz2_error(bzret))
+ if (catch_bz2_error(bzret)) {
+ d->bzerror = bzret;
+ d->needs_input = 0;
goto error;
+ }
if (bzret == BZ_STREAM_END) {
d->eof = 1;
break;
@@ -621,10 +625,17 @@ _bz2_BZ2Decompressor_decompress_impl(BZ2Decompressor *self, Py_buffer *data,
PyObject *result = NULL;
ACQUIRE_LOCK(self);
- if (self->eof)
+ if (self->eof) {
PyErr_SetString(PyExc_EOFError, "End of stream already reached");
- else
+ }
+ else if (self->bzerror) {
+ // Re-entering BZ2_bzDecompress() after an error can write out of bounds.
+ PyErr_SetString(PyExc_ValueError,
+ "Decompressor is unusable after a previous error");
+ }
+ else {
result = decompress(self, data->buf, data->len, max_length);
+ }
RELEASE_LOCK(self);
return result;
}
@@ -658,6 +669,7 @@ _bz2_BZ2Decompressor_impl(PyTypeObject *type)
return NULL;
}
+ self->bzerror = 0;
self->needs_input = 1;
self->bzs_avail_in_real = 0;
self->input_buffer = NULL;
--
2.54.0