Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion Lib/test/test_bz2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering we need to hit an error path with a MemoryError, I'm not sure we really need a test. Sometimes we do add tests, sometimes not. I don't think there is a need to add a test. The test also don't assert that we hit the goto so I don't think we need it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, while it is neat that we have _testcapi.set_nomemory (I didn't realize that), it is a difficult thing to use in a deterministic manner to show that the specific case in desire of covering is hit. usually not worth it.

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
Expand Down
31 changes: 30 additions & 1 deletion Lib/test/test_lzma.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):

Expand Down
31 changes: 30 additions & 1 deletion Lib/test/test_zlib.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions Modules/_bz2module.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions Modules/_lzmamodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions Modules/zlibmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1669,6 +1669,7 @@ decompress(ZlibDecompressor *self, uint8_t *data,
return result;

error:
self->zst.next_in = NULL;
Py_XDECREF(result);
return NULL;
}
Expand Down
Loading