From b9bcc953f5744a61fd9e5dcd484594149c05bd5f Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Fri, 10 Apr 2026 15:31:30 +0100 Subject: [PATCH 1/4] Fix dangling input pointer after `MemoryError` in _lzma/_bz2/_ZlibDecompressor.decompress --- Modules/_bz2module.c | 1 + Modules/_lzmamodule.c | 1 + Modules/zlibmodule.c | 1 + 3 files changed, 3 insertions(+) diff --git a/Modules/_bz2module.c b/Modules/_bz2module.c index 7b8cbf3ed96184..84d7a41ebc24c5 100644 --- a/Modules/_bz2module.c +++ b/Modules/_bz2module.c @@ -569,6 +569,7 @@ decompress(BZ2Decompressor *d, char *data, size_t len, Py_ssize_t max_length) return result; error: + bzs->next_in = NULL; Py_XDECREF(result); return NULL; } diff --git a/Modules/_lzmamodule.c b/Modules/_lzmamodule.c index 3c391675d7b93e..00ee68dcea2d0d 100644 --- a/Modules/_lzmamodule.c +++ b/Modules/_lzmamodule.c @@ -1100,6 +1100,7 @@ decompress(Decompressor *d, uint8_t *data, size_t len, Py_ssize_t max_length) return result; error: + lzs->next_in = NULL; Py_XDECREF(result); return NULL; } diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c index f67434ecdc908c..9c5820fbe97a6b 100644 --- a/Modules/zlibmodule.c +++ b/Modules/zlibmodule.c @@ -1669,6 +1669,7 @@ decompress(ZlibDecompressor *self, uint8_t *data, return result; error: + self->zst.next_in = NULL; Py_XDECREF(result); return NULL; } From ef544fabb7f8018360d127733f688d335a08b1be Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Fri, 10 Apr 2026 16:28:45 +0100 Subject: [PATCH 2/4] tests + news --- Lib/test/test_bz2.py | 31 ++++++++++++++++++- Lib/test/test_lzma.py | 31 ++++++++++++++++++- Lib/test/test_zlib.py | 31 ++++++++++++++++++- ...-04-10-16-28-21.gh-issue-111111.kfzm0G.rst | 5 +++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-111111.kfzm0G.rst diff --git a/Lib/test/test_bz2.py b/Lib/test/test_bz2.py index d8e3b671ec229f..c6fbcc95832d7e 100644 --- a/Lib/test/test_bz2.py +++ b/Lib/test/test_bz2.py @@ -13,7 +13,7 @@ import shutil import subprocess import threading -from test.support import import_helper +from test.support import import_helper, script_helper from test.support import threading_helper from test.support.os_helper import unlink, FakePath from compression._common import _streams @@ -936,6 +936,35 @@ def testPickle(self): with self.assertRaises(TypeError): pickle.dumps(BZ2Decompressor(), proto) + def test_decompressor_reuse_after_tail_copy_memory_error(self): + import_helper.import_module("_testcapi") + code = """if 1: + import _testcapi + import bz2 + + data = bz2.compress(b"S" * (1 << 20)) + for skip in range(1, 500): + d = bz2.BZ2Decompressor() + try: + _testcapi.set_nomemory(skip, skip + 1) + try: + d.decompress(data, max_length=0) + except MemoryError: + pass + else: + continue + finally: + _testcapi.remove_mem_hooks() + if d.needs_input: + continue + try: + d.decompress(b"B" * 64, max_length=0) + except Exception: + pass + break + """ + script_helper.assert_python_ok("-c", code) + def testDecompressorChunksMaxsize(self): bzd = BZ2Decompressor() max_length = 100 diff --git a/Lib/test/test_lzma.py b/Lib/test/test_lzma.py index e93c3c37354e27..a857a592a80f3d 100644 --- a/Lib/test/test_lzma.py +++ b/Lib/test/test_lzma.py @@ -8,7 +8,7 @@ import unittest from compression._common import _streams -from test.support import _4G, bigmemtest +from test.support import _4G, bigmemtest, script_helper from test.support.import_helper import import_module from test.support.os_helper import ( TESTFN, unlink, FakePath @@ -383,6 +383,35 @@ def test_uninitialized_LZMADecompressor_crash(self): self.assertEqual(LZMADecompressor.__new__(LZMADecompressor). decompress(bytes()), b'') + def test_decompressor_reuse_after_tail_copy_memory_error(self): + import_module("_testcapi") + code = """if 1: + import _testcapi + import lzma + + data = lzma.compress(b"S" * (1 << 20)) + for skip in range(1, 500): + d = lzma.LZMADecompressor() + try: + _testcapi.set_nomemory(skip, skip + 1) + try: + d.decompress(data, max_length=0) + except MemoryError: + pass + else: + continue + finally: + _testcapi.remove_mem_hooks() + if d.needs_input: + continue + try: + d.decompress(b"B" * 64, max_length=0) + except Exception: + pass + break + """ + script_helper.assert_python_ok("-c", code) + class CompressDecompressFunctionTestCase(unittest.TestCase): diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py index ed9d85408159b2..47e734d2ac363f 100644 --- a/Lib/test/test_zlib.py +++ b/Lib/test/test_zlib.py @@ -1,6 +1,6 @@ import unittest from test import support -from test.support import import_helper +from test.support import import_helper, script_helper import binascii import copy import pickle @@ -1207,6 +1207,35 @@ def test_failure(self): # Previously, a second call could crash due to internal inconsistency self.assertRaises(Exception, zlibd.decompress, self.BAD_DATA * 30) + def test_decompressor_reuse_after_tail_copy_memory_error(self): + import_helper.import_module("_testcapi") + code = """if 1: + import _testcapi + import zlib + + data = zlib.compress(b"S" * (1 << 20)) + for skip in range(1, 500): + d = zlib._ZlibDecompressor() + try: + _testcapi.set_nomemory(skip, skip + 1) + try: + d.decompress(data, max_length=0) + except MemoryError: + pass + else: + continue + finally: + _testcapi.remove_mem_hooks() + if d.needs_input: + continue + try: + d.decompress(b"B" * 64, max_length=0) + except Exception: + pass + break + """ + script_helper.assert_python_ok("-c", code) + @support.refcount_test def test_refleaks_in___init__(self): gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount') diff --git a/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-111111.kfzm0G.rst b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-111111.kfzm0G.rst new file mode 100644 index 00000000000000..9502189ab199c1 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-111111.kfzm0G.rst @@ -0,0 +1,5 @@ +Fix a dangling input pointer in :class:`lzma.LZMADecompressor`, +:class:`bz2.BZ2Decompressor`, and internal :class:`!zlib._ZlibDecompressor` +when memory allocation fails with :exc:`MemoryError`, which could let a +subsequent :meth:`!decompress` call read or write through a stale pointer to +the already-released caller buffer. From a07683d9a55e04df76419c207381a5a7e022d7cb Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sat, 11 Apr 2026 18:35:56 +0100 Subject: [PATCH 3/4] Add issue num to news --- ....kfzm0G.rst => 2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Security/{2026-04-10-16-28-21.gh-issue-111111.kfzm0G.rst => 2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst} (100%) diff --git a/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-111111.kfzm0G.rst b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst similarity index 100% rename from Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-111111.kfzm0G.rst rename to Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst From 93adea30deb7a109e3a4079b1e3be9180574cf77 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 12 Apr 2026 09:08:31 +0100 Subject: [PATCH 4/4] Remove test --- Lib/test/test_bz2.py | 31 +------------------------------ Lib/test/test_lzma.py | 31 +------------------------------ Lib/test/test_zlib.py | 31 +------------------------------ 3 files changed, 3 insertions(+), 90 deletions(-) diff --git a/Lib/test/test_bz2.py b/Lib/test/test_bz2.py index c6fbcc95832d7e..d8e3b671ec229f 100644 --- a/Lib/test/test_bz2.py +++ b/Lib/test/test_bz2.py @@ -13,7 +13,7 @@ import shutil import subprocess import threading -from test.support import import_helper, script_helper +from test.support import import_helper from test.support import threading_helper from test.support.os_helper import unlink, FakePath from compression._common import _streams @@ -936,35 +936,6 @@ def testPickle(self): with self.assertRaises(TypeError): pickle.dumps(BZ2Decompressor(), proto) - def test_decompressor_reuse_after_tail_copy_memory_error(self): - import_helper.import_module("_testcapi") - code = """if 1: - import _testcapi - import bz2 - - data = bz2.compress(b"S" * (1 << 20)) - for skip in range(1, 500): - d = bz2.BZ2Decompressor() - try: - _testcapi.set_nomemory(skip, skip + 1) - try: - d.decompress(data, max_length=0) - except MemoryError: - pass - else: - continue - finally: - _testcapi.remove_mem_hooks() - if d.needs_input: - continue - try: - d.decompress(b"B" * 64, max_length=0) - except Exception: - pass - break - """ - script_helper.assert_python_ok("-c", code) - def testDecompressorChunksMaxsize(self): bzd = BZ2Decompressor() max_length = 100 diff --git a/Lib/test/test_lzma.py b/Lib/test/test_lzma.py index a857a592a80f3d..e93c3c37354e27 100644 --- a/Lib/test/test_lzma.py +++ b/Lib/test/test_lzma.py @@ -8,7 +8,7 @@ import unittest from compression._common import _streams -from test.support import _4G, bigmemtest, script_helper +from test.support import _4G, bigmemtest from test.support.import_helper import import_module from test.support.os_helper import ( TESTFN, unlink, FakePath @@ -383,35 +383,6 @@ def test_uninitialized_LZMADecompressor_crash(self): self.assertEqual(LZMADecompressor.__new__(LZMADecompressor). decompress(bytes()), b'') - def test_decompressor_reuse_after_tail_copy_memory_error(self): - import_module("_testcapi") - code = """if 1: - import _testcapi - import lzma - - data = lzma.compress(b"S" * (1 << 20)) - for skip in range(1, 500): - d = lzma.LZMADecompressor() - try: - _testcapi.set_nomemory(skip, skip + 1) - try: - d.decompress(data, max_length=0) - except MemoryError: - pass - else: - continue - finally: - _testcapi.remove_mem_hooks() - if d.needs_input: - continue - try: - d.decompress(b"B" * 64, max_length=0) - except Exception: - pass - break - """ - script_helper.assert_python_ok("-c", code) - class CompressDecompressFunctionTestCase(unittest.TestCase): diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py index 47e734d2ac363f..ed9d85408159b2 100644 --- a/Lib/test/test_zlib.py +++ b/Lib/test/test_zlib.py @@ -1,6 +1,6 @@ import unittest from test import support -from test.support import import_helper, script_helper +from test.support import import_helper import binascii import copy import pickle @@ -1207,35 +1207,6 @@ def test_failure(self): # Previously, a second call could crash due to internal inconsistency self.assertRaises(Exception, zlibd.decompress, self.BAD_DATA * 30) - def test_decompressor_reuse_after_tail_copy_memory_error(self): - import_helper.import_module("_testcapi") - code = """if 1: - import _testcapi - import zlib - - data = zlib.compress(b"S" * (1 << 20)) - for skip in range(1, 500): - d = zlib._ZlibDecompressor() - try: - _testcapi.set_nomemory(skip, skip + 1) - try: - d.decompress(data, max_length=0) - except MemoryError: - pass - else: - continue - finally: - _testcapi.remove_mem_hooks() - if d.needs_input: - continue - try: - d.decompress(b"B" * 64, max_length=0) - except Exception: - pass - break - """ - script_helper.assert_python_ok("-c", code) - @support.refcount_test def test_refleaks_in___init__(self): gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')