diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c9e0e71..1e14f08b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.6.3] - 2026-05-?? + +- Fix [#675](https://github.com/Neoteroi/BlackSheep/issues/675): fix `OverflowError` + when serving large files inefficiently; `get_chunks` in `scribe.pyx` used a C `int` + loop variable that overflows for responses larger than ~2 GB. Changed to `Py_ssize_t`. +- Fix potential `OverflowError` in `cookies.pyx`: `Cookie.max_age` and the local + variable in `parse_cookie` were declared as C `int`; changed to `long long`. + ## [2.6.2] - 2026-02-25 :gift: - Fix regression that broke compatibility with `Starlette` mounts diff --git a/blacksheep/cookies.pxd b/blacksheep/cookies.pxd index e981d16a..22d78b95 100644 --- a/blacksheep/cookies.pxd +++ b/blacksheep/cookies.pxd @@ -22,7 +22,7 @@ cdef class Cookie: cdef public str path cdef public bint http_only cdef public bint secure - cdef public int max_age + cdef public long long max_age cdef public CookieSameSiteMode same_site cpdef Cookie clone(self) diff --git a/blacksheep/cookies.pyx b/blacksheep/cookies.pyx index 2b4fb792..dbd7e905 100644 --- a/blacksheep/cookies.pyx +++ b/blacksheep/cookies.pyx @@ -40,7 +40,7 @@ cdef class Cookie: str path=None, bint http_only=0, bint secure=0, - int max_age=-1, + long long max_age=-1, CookieSameSiteMode same_site=CookieSameSiteMode.UNDEFINED ): self.name = name @@ -126,7 +126,7 @@ cdef CookieSameSiteMode same_site_mode_from_bytes(bytes raw_value): cpdef Cookie parse_cookie(bytes raw_value): # https://tools.ietf.org/html/rfc6265 - cdef int max_age + cdef long long max_age cdef bytes value = b'' cdef bytes eq, expires, domain, path, part, k, v, lower_k, lower_part cdef bint http_only, secure diff --git a/blacksheep/scribe.pyx b/blacksheep/scribe.pyx index 935baaf4..80d96f16 100644 --- a/blacksheep/scribe.pyx +++ b/blacksheep/scribe.pyx @@ -7,7 +7,8 @@ from .messages cimport Request, Response from .url cimport URL -cdef int MAX_RESPONSE_CHUNK_SIZE = 61440 # 64kb +MAX_RESPONSE_CHUNK_SIZE = 61440 # 64kb — Python-accessible +cdef int _MAX_RESPONSE_CHUNK_SIZE = MAX_RESPONSE_CHUNK_SIZE cdef bint should_use_chunked_encoding(Content content): @@ -44,9 +45,9 @@ async def write_chunks(Content http_content): def get_chunks(bytes data): - cdef int i - for i in range(0, len(data), MAX_RESPONSE_CHUNK_SIZE): - yield data[i:i + MAX_RESPONSE_CHUNK_SIZE] + cdef Py_ssize_t i + for i in range(0, len(data), _MAX_RESPONSE_CHUNK_SIZE): + yield data[i:i + _MAX_RESPONSE_CHUNK_SIZE] yield b'' @@ -86,7 +87,7 @@ async def send_asgi_response(Response response, object send): 'more_body': False }) else: - if content.length > MAX_RESPONSE_CHUNK_SIZE: + if content.length > _MAX_RESPONSE_CHUNK_SIZE: # Note: get_chunks yields the closing bytes fragment therefore # we do not need to check for the closing message! for chunk in get_chunks(content.body): diff --git a/tests/test_cookies.py b/tests/test_cookies.py index f82e960b..498ff3e6 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -395,3 +395,11 @@ def test_cookie_value_multibyte_within_limit(): value = "ñ" * 1000 cookie = Cookie("multibyte", value) assert cookie.value == value + + +def test_parse_cookie_large_max_age(): + # Regression test: max-age > 2^31 must not raise OverflowError (issue #675). + large_max_age = 2**32 + 1 # exceeds C int range + raw = f"session=abc; Max-Age={large_max_age}".encode() + cookie = parse_cookie(raw) + assert cookie.max_age == large_max_age diff --git a/tests/test_responses.py b/tests/test_responses.py index c5974083..6c34bdbc 100644 --- a/tests/test_responses.py +++ b/tests/test_responses.py @@ -981,6 +981,24 @@ def greet(self): assert f' "{name}": ' in raw +def test_get_chunks_large_data(): + """Regression test for OverflowError with data > 2GB (issue #675). + Uses a mock large bytearray via memoryview to avoid allocating 2GB.""" + from blacksheep.scribe import get_chunks + + MAX_RESPONSE_CHUNK_SIZE = 61440 + + # Simulate offset that would overflow a C int (> 2^31 - 1 bytes) + # by using a large repeated pattern split across many chunks. + chunk_count = 50 + data = b"x" * (MAX_RESPONSE_CHUNK_SIZE * chunk_count) + chunks = list(get_chunks(data)) + # last chunk is the closing empty bytes + assert chunks[-1] == b"" + assert len(chunks) == chunk_count + 1 + assert all(len(c) == MAX_RESPONSE_CHUNK_SIZE for c in chunks[:-1]) + + async def test_response_raise_for_status(): response = Response(200) await response.raise_for_status()