From 13083e021ea2891449b9dc77574dc181f1d2814f Mon Sep 17 00:00:00 2001 From: wjiang Date: Tue, 4 Nov 2025 15:59:01 +0800 Subject: [PATCH] fix CVE-2025-6176 (cherry picked from commit f63e330c855ee1a9d34869a715c7d4e1e86781ae) --- ...th-to-Python-streaming-decompression.patch | 281 ++++++++++++++++++ add-size-limit-to-buffer.patch | 98 ++++++ brotli.spec | 8 +- 3 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 add-max_length-to-Python-streaming-decompression.patch create mode 100644 add-size-limit-to-buffer.patch diff --git a/add-max_length-to-Python-streaming-decompression.patch b/add-max_length-to-Python-streaming-decompression.patch new file mode 100644 index 0000000..d4c6264 --- /dev/null +++ b/add-max_length-to-Python-streaming-decompression.patch @@ -0,0 +1,281 @@ +From eb3a31e2d356d5a633de995afe7fe60e590a26d8 Mon Sep 17 00:00:00 2001 +From: Robert Obryk +Date: Wed, 18 Sep 2024 15:25:06 +0200 +Subject: [PATCH] add max_length to Python streaming decompression + +--- + python/_brotli.c | 192 ++++++++++++++++-------- + 1 files changed, 133 insertions(+), 59 deletions(-) + +diff --git a/python/_brotli.c b/python/_brotli.c +index 75c54c489..f86b04f93 100644 +--- a/python/_brotli.c ++++ b/python/_brotli.c +@@ -606,57 +606,6 @@ static PyTypeObject brotli_CompressorType = { + brotli_Compressor_new, /* tp_new */ + }; + +-static PyObject* decompress_stream(BrotliDecoderState* dec, +- uint8_t* input, size_t input_length) { +- BrotliDecoderResult result; +- +- size_t available_in = input_length; +- const uint8_t* next_in = input; +- +- size_t available_out; +- uint8_t* next_out; +- BlocksOutputBuffer buffer = {.list=NULL}; +- PyObject *ret; +- +- if (BlocksOutputBuffer_InitAndGrow(&buffer, PY_SSIZE_T_MAX, &available_out, &next_out) < 0) { +- goto error; +- } +- +- while (1) { +- Py_BEGIN_ALLOW_THREADS +- result = BrotliDecoderDecompressStream(dec, +- &available_in, &next_in, +- &available_out, &next_out, NULL); +- Py_END_ALLOW_THREADS +- +- if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { +- if (available_out == 0) { +- if (BlocksOutputBuffer_Grow(&buffer, &available_out, &next_out) < 0) { +- goto error; +- } +- } +- continue; +- } +- +- break; +- } +- +- if (result == BROTLI_DECODER_RESULT_ERROR || available_in != 0) { +- goto error; +- } +- +- ret = BlocksOutputBuffer_Finish(&buffer, available_out); +- if (ret != NULL) { +- goto finally; +- } +- +-error: +- BlocksOutputBuffer_OnError(&buffer); +- ret = NULL; +-finally: +- return ret; +-} +- + PyDoc_STRVAR(brotli_Decompressor_doc, + "An object to decompress a byte string.\n" + "\n" +@@ -669,10 +618,14 @@ PyDoc_STRVAR(brotli_Decompressor_doc, + typedef struct { + PyObject_HEAD + BrotliDecoderState* dec; ++ uint8_t* unconsumed_data; ++ size_t unconsumed_data_length; + } brotli_Decompressor; + + static void brotli_Decompressor_dealloc(brotli_Decompressor* self) { + BrotliDecoderDestroyInstance(self->dec); ++ if (self->unconsumed_data) ++ free(self->unconsumed_data); + #if PY_MAJOR_VERSION >= 3 + Py_TYPE(self)->tp_free((PyObject*)self); + #else +@@ -688,6 +641,9 @@ static PyObject* brotli_Decompressor_new(PyTypeObject *type, PyObject *args, PyO + self->dec = BrotliDecoderCreateInstance(0, 0, 0); + } + ++ self->unconsumed_data = NULL; ++ self->unconsumed_data_length = 0; ++ + return (PyObject *)self; + } + +@@ -706,6 +662,79 @@ static int brotli_Decompressor_init(brotli_Decompressor *self, PyObject *args, P + return 0; + } + ++static PyObject* decompress_stream(brotli_Decompressor* self, ++ uint8_t* input, size_t input_length, Py_ssize_t output_buffer_limit) { ++ BrotliDecoderResult result; ++ ++ size_t available_in = input_length; ++ const uint8_t* next_in = input; ++ ++ size_t available_out; ++ uint8_t* next_out; ++ uint8_t* new_tail; ++ BlocksOutputBuffer buffer = {.list=NULL}; ++ PyObject *ret; ++ ++ if (BlocksOutputBuffer_InitAndGrow(&buffer, output_buffer_limit, &available_out, &next_out) < 0) { ++ goto error; ++ } ++ ++ while (1) { ++ Py_BEGIN_ALLOW_THREADS ++ result = BrotliDecoderDecompressStream(self->dec, ++ &available_in, &next_in, ++ &available_out, &next_out, NULL); ++ Py_END_ALLOW_THREADS ++ ++ if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { ++ if (available_out == 0) { ++ if (buffer.allocated == PY_SSIZE_T_MAX) { ++ PyErr_SetString(PyExc_MemoryError, unable_allocate_msg); ++ goto error; ++ } ++ if (buffer.allocated == output_buffer_limit) { ++ // We've reached the output length limit. ++ break; ++ } ++ if (BlocksOutputBuffer_Grow(&buffer, &available_out, &next_out) < 0) { ++ goto error; ++ } ++ } ++ continue; ++ } ++ ++ if (result == BROTLI_DECODER_RESULT_ERROR || available_in != 0) { ++ available_in = 0; ++ goto error; ++ } ++ ++ break; ++ } ++ ++ ret = BlocksOutputBuffer_Finish(&buffer, available_out); ++ if (ret != NULL) { ++ goto finally; ++ } ++ ++error: ++ BlocksOutputBuffer_OnError(&buffer); ++ ret = NULL; ++ ++finally: ++ new_tail = available_in > 0 ? malloc(available_in) : NULL; ++ if (available_in > 0) { ++ memcpy(new_tail, next_in, available_in); ++ } ++ if (self->unconsumed_data) { ++ free(self->unconsumed_data); ++ } ++ self->unconsumed_data = new_tail; ++ self->unconsumed_data_length = available_in; ++ ++ return ret; ++} ++ ++ + PyDoc_STRVAR(brotli_Decompressor_process_doc, + "Process \"string\" for decompression, returning a string that contains \n" + "decompressed output data. This data should be concatenated to the output \n" +@@ -713,28 +742,38 @@ PyDoc_STRVAR(brotli_Decompressor_process_doc, + "Some or all of the input may be kept in internal buffers for later \n" + "processing, and the decompressed output data may be empty until enough input \n" + "has been accumulated.\n" ++"If output_buffer_limit is set, no more than output_buffer_limit bytes will be\n" ++"returned. If the limit is reached, further calls to process (potentially with\n" ++"empty input) will continue to yield more data. If, after returning a string of\n" ++"the length equal to limit, can_accept_more_data() returns False, process()\n" ++"must only be called with empty input until can_accept_more_data() once again\n" ++"returns True.\n" + "\n" + "Signature:\n" +-" decompress(string)\n" ++" decompress(string, output_buffer_limit=int)\n" + "\n" + "Args:\n" + " string (bytes): The input data\n" +-"\n" +-"Returns:\n" ++"\n""Returns:\n" + " The decompressed output data (bytes)\n" + "\n" + "Raises:\n" + " brotli.error: If decompression fails\n"); + +-static PyObject* brotli_Decompressor_process(brotli_Decompressor *self, PyObject *args) { ++static PyObject* brotli_Decompressor_process(brotli_Decompressor *self, PyObject *args, PyObject* keywds) { + PyObject* ret; + Py_buffer input; + int ok; ++ Py_ssize_t output_buffer_limit = PY_SSIZE_T_MAX; ++ uint8_t* data; ++ size_t data_length; ++ ++ static char* kwlist[] = { "", "output_buffer_limit", NULL }; + + #if PY_MAJOR_VERSION >= 3 +- ok = PyArg_ParseTuple(args, "y*:process", &input); ++ ok = PyArg_ParseTupleAndKeywords(args, keywds, "y*|n:process", kwlist, &input, &output_buffer_limit); + #else +- ok = PyArg_ParseTuple(args, "s*:process", &input); ++ ok = PyArg_ParseTupleAndKeywords(args, keywds, "s*|n:process", kwlist, &input, &output_buffer_limit); + #endif + + if (!ok) { +@@ -745,7 +784,20 @@ static PyObject* brotli_Decompressor_process(brotli_Decompressor *self, PyObject + goto error; + } + +- ret = decompress_stream(self->dec, (uint8_t*) input.buf, input.len); ++ if (self->unconsumed_data_length > 0) { ++ if (input.len > 0) { ++ PyErr_SetString(BrotliError, "brotli: decoder process called with data when 'can_accept_more_data()' is False"); ++ ret = NULL; ++ goto finally; ++ } ++ data = self->unconsumed_data; ++ data_length = self->unconsumed_data_length; ++ } else { ++ data = (uint8_t*)input.buf; ++ data_length = input.len; ++ } ++ ++ ret = decompress_stream(self, data, data_length, output_buffer_limit); + if (ret != NULL) { + goto finally; + } +@@ -787,13 +839,35 @@ static PyObject* brotli_Decompressor_is_finished(brotli_Decompressor *self) { + } + } + ++PyDoc_STRVAR(brotli_Decompressor_can_accept_more_data_doc, ++"Checks if the decoder instance can accept more compressed data. If the decompress()\n" ++"method on this instance of decompressor was never called with max_length,\n" ++"this method will always return True.\n" ++"\n" ++"Signature:" ++" can_accept_more_data()\n" ++"\n" ++"Returns:\n" ++" True if the decoder is ready to accept more compressed data via decompress()\n" ++" False if the decoder needs to output some data via decompress(b'') before\n" ++" being provided any more compressed data\n"); ++ ++static PyObject* brotli_Decompressor_can_accept_more_data(brotli_Decompressor* self) { ++ if (self->unconsumed_data_length > 0) { ++ Py_RETURN_FALSE; ++ } else { ++ Py_RETURN_TRUE; ++ } ++} ++ + static PyMemberDef brotli_Decompressor_members[] = { + {NULL} /* Sentinel */ + }; + + static PyMethodDef brotli_Decompressor_methods[] = { +- {"process", (PyCFunction)brotli_Decompressor_process, METH_VARARGS, brotli_Decompressor_process_doc}, ++ {"process", (PyCFunction)brotli_Decompressor_process, METH_VARARGS | METH_KEYWORDS, brotli_Decompressor_process_doc}, + {"is_finished", (PyCFunction)brotli_Decompressor_is_finished, METH_NOARGS, brotli_Decompressor_is_finished_doc}, ++ {"can_accept_more_data", (PyCFunction)brotli_Decompressor_can_accept_more_data, METH_NOARGS, brotli_Decompressor_can_accept_more_data_doc}, + {NULL} /* Sentinel */ + }; + + diff --git a/add-size-limit-to-buffer.patch b/add-size-limit-to-buffer.patch new file mode 100644 index 0000000..ed04015 --- /dev/null +++ b/add-size-limit-to-buffer.patch @@ -0,0 +1,98 @@ +From 28ce91caf605ac5481e9ca69131a28e1087574b7 Mon Sep 17 00:00:00 2001 +From: Robert Obryk +Date: Tue, 17 Sep 2024 16:50:39 +0200 +Subject: [PATCH] add size limit to buffer + +--- + python/_brotli.c | 32 +++++++++++++++++++++++--------- + 1 file changed, 23 insertions(+), 9 deletions(-) + +diff --git a/python/_brotli.c b/python/_brotli.c +index c6a0da03d..75c54c489 100644 +--- a/python/_brotli.c ++++ b/python/_brotli.c +@@ -23,6 +23,7 @@ typedef struct { + PyObject *list; + /* Number of whole allocated size. */ + Py_ssize_t allocated; ++ Py_ssize_t size_limit; + } BlocksOutputBuffer; + + static const char unable_allocate_msg[] = "Unable to allocate output buffer."; +@@ -69,11 +70,17 @@ static const Py_ssize_t BUFFER_BLOCK_SIZE[] = + Return -1 on failure + */ + static inline int +-BlocksOutputBuffer_InitAndGrow(BlocksOutputBuffer *buffer, ++BlocksOutputBuffer_InitAndGrow(BlocksOutputBuffer *buffer, Py_ssize_t size_limit, + size_t *avail_out, uint8_t **next_out) + { + PyObject *b; +- const Py_ssize_t block_size = BUFFER_BLOCK_SIZE[0]; ++ Py_ssize_t block_size = BUFFER_BLOCK_SIZE[0]; ++ ++ assert(size_limit > 0); ++ ++ if (size_limit < block_size) { ++ block_size = size_limit; ++ } + + // Ensure .list was set to NULL, for BlocksOutputBuffer_OnError(). + assert(buffer->list == NULL); +@@ -94,6 +101,7 @@ BlocksOutputBuffer_InitAndGrow(BlocksOutputBuffer *buffer, + + // Set variables + buffer->allocated = block_size; ++ buffer->size_limit = size_limit; + + *avail_out = (size_t) block_size; + *next_out = (uint8_t*) PyBytes_AS_STRING(b); +@@ -122,10 +130,16 @@ BlocksOutputBuffer_Grow(BlocksOutputBuffer *buffer, + block_size = BUFFER_BLOCK_SIZE[Py_ARRAY_LENGTH(BUFFER_BLOCK_SIZE) - 1]; + } + +- // Check buffer->allocated overflow +- if (block_size > PY_SSIZE_T_MAX - buffer->allocated) { +- PyErr_SetString(PyExc_MemoryError, unable_allocate_msg); +- return -1; ++ if (block_size > buffer->size_limit - buffer->allocated) { ++ block_size = buffer->size_limit - buffer->allocated; ++ } ++ ++ if (block_size == 0) { ++ // We are at the size_limit (either the provided one, in which case we ++ // shouldn't have been called, or the implicit PY_SSIZE_T_MAX one, in ++ // which case we wouldn't be able to concatenate the blocks at the end). ++ PyErr_SetString(PyExc_MemoryError, "too long"); ++ return -1; + } + + // Create the block +@@ -291,7 +305,7 @@ static PyObject* compress_stream(BrotliEncoderState* enc, BrotliEncoderOperation + BlocksOutputBuffer buffer = {.list=NULL}; + PyObject *ret; + +- if (BlocksOutputBuffer_InitAndGrow(&buffer, &available_out, &next_out) < 0) { ++ if (BlocksOutputBuffer_InitAndGrow(&buffer, PY_SSIZE_T_MAX, &available_out, &next_out) < 0) { + goto error; + } + +@@ -604,7 +618,7 @@ static PyObject* decompress_stream(BrotliDecoderState* dec, + BlocksOutputBuffer buffer = {.list=NULL}; + PyObject *ret; + +- if (BlocksOutputBuffer_InitAndGrow(&buffer, &available_out, &next_out) < 0) { ++ if (BlocksOutputBuffer_InitAndGrow(&buffer, PY_SSIZE_T_MAX, &available_out, &next_out) < 0) { + goto error; + } + +@@ -877,7 +891,7 @@ static PyObject* brotli_decompress(PyObject *self, PyObject *args, PyObject *key + next_in = (uint8_t*) input.buf; + available_in = input.len; + +- if (BlocksOutputBuffer_InitAndGrow(&buffer, &available_out, &next_out) < 0) { ++ if (BlocksOutputBuffer_InitAndGrow(&buffer, PY_SSIZE_T_MAX, &available_out, &next_out) < 0) { + goto error; + } + + diff --git a/brotli.spec b/brotli.spec index c0c99af..01c5051 100644 --- a/brotli.spec +++ b/brotli.spec @@ -1,12 +1,15 @@ Name: brotli Version: 1.1.0 -Release: 1 +Release: 2 Summary: Lossless compression algorithm License: MIT URL: https://github.com/google/brotli Source0: https://github.com/google/brotli/archive/v%{version}.tar.gz +Patch6000: add-size-limit-to-buffer.patch +Patch6001: add-max_length-to-Python-streaming-decompression.patch + BuildRequires: python3-devel gcc-c++ gcc cmake @@ -89,6 +92,9 @@ popd %{_mandir}/man3/* %changelog +* Tue Nov 04 2025 wangjiang - 1.1.0-2 +- fix CVE-2025-6176 + * Thu Nov 09 2023 lvcongqing - 1.1.0-1 - upgrade version to 1.1.0 -- Gitee