diff --git a/cassandra/cython_lz4.pyx b/cassandra/cython_lz4.pyx index 12bb8703ee..c632822cff 100644 --- a/cassandra/cython_lz4.pyx +++ b/cassandra/cython_lz4.pyx @@ -52,11 +52,14 @@ cdef extern from *: uint32_t ntohl(uint32_t netlong) nogil # CQL native protocol v4 frames have a 32-bit body length, so the -# theoretical maximum is ~2 GiB. We use 256 MiB as a practical upper -# bound (matching the server's default frame size limit) to avoid -# accidentally allocating multi-GiB buffers on corrupt headers. +# theoretical maximum uncompressed size is INT32_MAX (~2 GiB). The +# previous 256 MiB cap was the server's *default* frame-size limit, not +# the protocol/LZ4 API limit, so it rejected valid frames on clusters +# configured with larger frames (see issue #1000). We validate against +# INT32_MAX so legitimate oversized frames still decompress while +# negative or >2 GiB values from corrupt headers are still rejected. cdef enum: - MAX_DECOMPRESSED_LENGTH = 268435456 # 256 MiB + MAX_DECOMPRESSED_LENGTH = INT32_MAX # ~2 GiB # LZ4_MAX_INPUT_SIZE from lz4.h — the LZ4 C API uses C int (32-bit # signed) for sizes, so we must reject Python bytes objects that diff --git a/tests/unit/cython/test_cython_lz4.py b/tests/unit/cython/test_cython_lz4.py index 7d0ab0488b..6ef1f99c6b 100644 --- a/tests/unit/cython/test_cython_lz4.py +++ b/tests/unit/cython/test_cython_lz4.py @@ -131,11 +131,40 @@ def test_decompress_corrupt_payload(self): lz4_decompress(bad_frame) def test_decompress_oversized_header(self): - """Header claiming > 256 MiB should raise ValueError.""" - # 0x10000001 = 256 MiB + 1 - huge_header = struct.pack('>I', 0x10000001) + b"\x00" * 10 + """Header claiming > INT32_MAX should raise ValueError.""" + # 0x80000000 = INT32_MAX + 1 (high bit set, would overflow int) + too_big = struct.pack('>I', 0x80000000) + b"\x00" * 10 with self.assertRaises(ValueError): - lz4_decompress(huge_header) + lz4_decompress(too_big) + # 0xFFFFFFFF = UINT32_MAX, also too large for signed int + max_u32 = struct.pack('>I', 0xFFFFFFFF) + b"\x00" * 10 + with self.assertRaises(ValueError): + lz4_decompress(max_u32) + + def test_decompress_accepts_int32_max_header(self): + """Header claiming exactly INT32_MAX should be accepted (not rejected). + + The native protocol permits uncompressed frame sizes up to the + signed 32-bit limit (~2 GiB). Operators configure ``frame_size`` + above the 256 MiB server default on production clusters, so the + Cython codec must not reject those frames (issue #1000). + """ + # INT32_MAX = 0x7FFFFFFF. The header is well-formed but the + # payload is missing, so decompression must fail at the LZ4 + # stage (RuntimeError) -- never at the size-validation stage. + header_only = struct.pack('>I', 0x7FFFFFFF) + with self.assertRaises(RuntimeError): + lz4_decompress(header_only) + + def test_decompress_accepts_512mb_header(self): + """A 512 MiB declared size must pass validation (issue #1000). + + Validates the boundary behaviour introduced for clusters that + configure ``frame_size`` above the previous 256 MiB safety cap. + """ + header_only = struct.pack('>I', 512 * 1024 * 1024) + with self.assertRaises(RuntimeError): + lz4_decompress(header_only) def test_round_trip_all_zeros(self): """All-zero payloads compress extremely well; verify correctness."""