diff options
Diffstat (limited to 'meta-python')
3 files changed, 368 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229-1.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229-1.patch new file mode 100644 index 0000000000..70feb03258 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229-1.patch | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | From 9e03b5732805f3cf3c5c249761e2fb8ace2223d3 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Gyorgy Sarvari <skandigraun@gmail.com> | ||
| 3 | Date: Sat, 3 Jan 2026 03:57:17 +0000 | ||
| 4 | Subject: [PATCH 1/2] Use collections.deque for chunk splits (#11892) (#11912) | ||
| 5 | |||
| 6 | From: Sam Bull <git@sambull.org> | ||
| 7 | |||
| 8 | (cherry picked from commit 271532ea355c65480c8ecc14137dfbb72aec8f6f) | ||
| 9 | |||
| 10 | --------- | ||
| 11 | |||
| 12 | Co-authored-by: Finder <nakamurajames123@gmail.com> | ||
| 13 | |||
| 14 | CVE: CVE-2025-69229 | ||
| 15 | Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/dc3170b56904bdf814228fae70a5501a42a6c712] | ||
| 16 | Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com> | ||
| 17 | --- | ||
| 18 | aiohttp/streams.py | 8 ++++---- | ||
| 19 | tests/test_http_parser.py | 14 +++++++++----- | ||
| 20 | 2 files changed, 13 insertions(+), 9 deletions(-) | ||
| 21 | |||
| 22 | diff --git a/aiohttp/streams.py b/aiohttp/streams.py | ||
| 23 | index 7a3f64d..108257e 100644 | ||
| 24 | --- a/aiohttp/streams.py | ||
| 25 | +++ b/aiohttp/streams.py | ||
| 26 | @@ -148,7 +148,7 @@ class StreamReader(AsyncStreamReaderMixin): | ||
| 27 | self._loop = loop | ||
| 28 | self._size = 0 | ||
| 29 | self._cursor = 0 | ||
| 30 | - self._http_chunk_splits: Optional[List[int]] = None | ||
| 31 | + self._http_chunk_splits: Optional[Deque[int]] = None | ||
| 32 | self._buffer: Deque[bytes] = collections.deque() | ||
| 33 | self._buffer_offset = 0 | ||
| 34 | self._eof = False | ||
| 35 | @@ -295,7 +295,7 @@ class StreamReader(AsyncStreamReaderMixin): | ||
| 36 | raise RuntimeError( | ||
| 37 | "Called begin_http_chunk_receiving when some data was already fed" | ||
| 38 | ) | ||
| 39 | - self._http_chunk_splits = [] | ||
| 40 | + self._http_chunk_splits = collections.deque() | ||
| 41 | |||
| 42 | def end_http_chunk_receiving(self) -> None: | ||
| 43 | if self._http_chunk_splits is None: | ||
| 44 | @@ -454,7 +454,7 @@ class StreamReader(AsyncStreamReaderMixin): | ||
| 45 | raise self._exception | ||
| 46 | |||
| 47 | while self._http_chunk_splits: | ||
| 48 | - pos = self._http_chunk_splits.pop(0) | ||
| 49 | + pos = self._http_chunk_splits.popleft() | ||
| 50 | if pos == self._cursor: | ||
| 51 | return (b"", True) | ||
| 52 | if pos > self._cursor: | ||
| 53 | @@ -527,7 +527,7 @@ class StreamReader(AsyncStreamReaderMixin): | ||
| 54 | chunk_splits = self._http_chunk_splits | ||
| 55 | # Prevent memory leak: drop useless chunk splits | ||
| 56 | while chunk_splits and chunk_splits[0] < self._cursor: | ||
| 57 | - chunk_splits.pop(0) | ||
| 58 | + chunk_splits.popleft() | ||
| 59 | |||
| 60 | if self._size < self._low_water and self._protocol._reading_paused: | ||
| 61 | self._protocol.resume_reading() | ||
| 62 | diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py | ||
| 63 | index d4c1768..b9d917f 100644 | ||
| 64 | --- a/tests/test_http_parser.py | ||
| 65 | +++ b/tests/test_http_parser.py | ||
| 66 | @@ -1223,7 +1223,8 @@ def test_http_request_chunked_payload(parser) -> None: | ||
| 67 | parser.feed_data(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n") | ||
| 68 | |||
| 69 | assert b"dataline" == b"".join(d for d in payload._buffer) | ||
| 70 | - assert [4, 8] == payload._http_chunk_splits | ||
| 71 | + assert payload._http_chunk_splits is not None | ||
| 72 | + assert [4, 8] == list(payload._http_chunk_splits) | ||
| 73 | assert payload.is_eof() | ||
| 74 | |||
| 75 | |||
| 76 | @@ -1238,7 +1239,8 @@ def test_http_request_chunked_payload_and_next_message(parser) -> None: | ||
| 77 | ) | ||
| 78 | |||
| 79 | assert b"dataline" == b"".join(d for d in payload._buffer) | ||
| 80 | - assert [4, 8] == payload._http_chunk_splits | ||
| 81 | + assert payload._http_chunk_splits is not None | ||
| 82 | + assert [4, 8] == list(payload._http_chunk_splits) | ||
| 83 | assert payload.is_eof() | ||
| 84 | |||
| 85 | assert len(messages) == 1 | ||
| 86 | @@ -1262,12 +1264,13 @@ def test_http_request_chunked_payload_chunks(parser) -> None: | ||
| 87 | parser.feed_data(b"test: test\r\n") | ||
| 88 | |||
| 89 | assert b"dataline" == b"".join(d for d in payload._buffer) | ||
| 90 | - assert [4, 8] == payload._http_chunk_splits | ||
| 91 | + assert payload._http_chunk_splits is not None | ||
| 92 | + assert [4, 8] == list(payload._http_chunk_splits) | ||
| 93 | assert not payload.is_eof() | ||
| 94 | |||
| 95 | parser.feed_data(b"\r\n") | ||
| 96 | assert b"dataline" == b"".join(d for d in payload._buffer) | ||
| 97 | - assert [4, 8] == payload._http_chunk_splits | ||
| 98 | + assert [4, 8] == list(payload._http_chunk_splits) | ||
| 99 | assert payload.is_eof() | ||
| 100 | |||
| 101 | |||
| 102 | @@ -1278,7 +1281,8 @@ def test_parse_chunked_payload_chunk_extension(parser) -> None: | ||
| 103 | parser.feed_data(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\ntest: test\r\n\r\n") | ||
| 104 | |||
| 105 | assert b"dataline" == b"".join(d for d in payload._buffer) | ||
| 106 | - assert [4, 8] == payload._http_chunk_splits | ||
| 107 | + assert payload._http_chunk_splits is not None | ||
| 108 | + assert [4, 8] == list(payload._http_chunk_splits) | ||
| 109 | assert payload.is_eof() | ||
| 110 | |||
| 111 | |||
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229-2.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229-2.patch new file mode 100644 index 0000000000..e67832f09e --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229-2.patch | |||
| @@ -0,0 +1,255 @@ | |||
| 1 | From e124809ca5f17e608c09fc79423f9c357208a3c5 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Gyorgy Sarvari <skandigraun@gmail.com> | ||
| 3 | Date: Sat, 3 Jan 2026 15:23:14 +0000 | ||
| 4 | Subject: [PATCH 2/2] Limit number of chunks before pausing reading (#11894) | ||
| 5 | (#11916) | ||
| 6 | |||
| 7 | From: Sam Bull <git@sambull.org> | ||
| 8 | |||
| 9 | (cherry picked from commit 1e4120e87daec963c67f956111e6bca44d7c3dea) | ||
| 10 | |||
| 11 | Co-authored-by: J. Nick Koston <nick@koston.org> | ||
| 12 | |||
| 13 | CVE: CVE-2025-69229 | ||
| 14 | Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/4ed97a4e46eaf61bd0f05063245f613469700229] | ||
| 15 | Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com> | ||
| 16 | --- | ||
| 17 | aiohttp/streams.py | 25 ++++++- | ||
| 18 | tests/test_streams.py | 170 ++++++++++++++++++++++++++++++++++++++++++ | ||
| 19 | 2 files changed, 194 insertions(+), 1 deletion(-) | ||
| 20 | |||
| 21 | diff --git a/aiohttp/streams.py b/aiohttp/streams.py | ||
| 22 | index 108257e..9329534 100644 | ||
| 23 | --- a/aiohttp/streams.py | ||
| 24 | +++ b/aiohttp/streams.py | ||
| 25 | @@ -116,6 +116,8 @@ class StreamReader(AsyncStreamReaderMixin): | ||
| 26 | "_protocol", | ||
| 27 | "_low_water", | ||
| 28 | "_high_water", | ||
| 29 | + "_low_water_chunks", | ||
| 30 | + "_high_water_chunks", | ||
| 31 | "_loop", | ||
| 32 | "_size", | ||
| 33 | "_cursor", | ||
| 34 | @@ -145,6 +147,11 @@ class StreamReader(AsyncStreamReaderMixin): | ||
| 35 | self._high_water = limit * 2 | ||
| 36 | if loop is None: | ||
| 37 | loop = asyncio.get_event_loop() | ||
| 38 | + # Ensure high_water_chunks >= 3 so it's always > low_water_chunks. | ||
| 39 | + self._high_water_chunks = max(3, limit // 4) | ||
| 40 | + # Use max(2, ...) because there's always at least 1 chunk split remaining | ||
| 41 | + # (the current position), so we need low_water >= 2 to allow resume. | ||
| 42 | + self._low_water_chunks = max(2, self._high_water_chunks // 2) | ||
| 43 | self._loop = loop | ||
| 44 | self._size = 0 | ||
| 45 | self._cursor = 0 | ||
| 46 | @@ -321,6 +328,15 @@ class StreamReader(AsyncStreamReaderMixin): | ||
| 47 | |||
| 48 | self._http_chunk_splits.append(self.total_bytes) | ||
| 49 | |||
| 50 | + # If we get too many small chunks before self._high_water is reached, then any | ||
| 51 | + # .read() call becomes computationally expensive, and could block the event loop | ||
| 52 | + # for too long, hence an additional self._high_water_chunks here. | ||
| 53 | + if ( | ||
| 54 | + len(self._http_chunk_splits) > self._high_water_chunks | ||
| 55 | + and not self._protocol._reading_paused | ||
| 56 | + ): | ||
| 57 | + self._protocol.pause_reading() | ||
| 58 | + | ||
| 59 | # wake up readchunk when end of http chunk received | ||
| 60 | waiter = self._waiter | ||
| 61 | if waiter is not None: | ||
| 62 | @@ -529,7 +545,14 @@ class StreamReader(AsyncStreamReaderMixin): | ||
| 63 | while chunk_splits and chunk_splits[0] < self._cursor: | ||
| 64 | chunk_splits.popleft() | ||
| 65 | |||
| 66 | - if self._size < self._low_water and self._protocol._reading_paused: | ||
| 67 | + if ( | ||
| 68 | + self._protocol._reading_paused | ||
| 69 | + and self._size < self._low_water | ||
| 70 | + and ( | ||
| 71 | + self._http_chunk_splits is None | ||
| 72 | + or len(self._http_chunk_splits) < self._low_water_chunks | ||
| 73 | + ) | ||
| 74 | + ): | ||
| 75 | self._protocol.resume_reading() | ||
| 76 | return data | ||
| 77 | |||
| 78 | diff --git a/tests/test_streams.py b/tests/test_streams.py | ||
| 79 | index 1b65f77..c5bc671 100644 | ||
| 80 | --- a/tests/test_streams.py | ||
| 81 | +++ b/tests/test_streams.py | ||
| 82 | @@ -1552,3 +1552,173 @@ async def test_stream_reader_iter_chunks_chunked_encoding(protocol) -> None: | ||
| 83 | |||
| 84 | def test_isinstance_check() -> None: | ||
| 85 | assert isinstance(streams.EMPTY_PAYLOAD, streams.StreamReader) | ||
| 86 | + | ||
| 87 | + | ||
| 88 | +async def test_stream_reader_pause_on_high_water_chunks( | ||
| 89 | + protocol: mock.Mock, | ||
| 90 | +) -> None: | ||
| 91 | + """Test that reading is paused when chunk count exceeds high water mark.""" | ||
| 92 | + loop = asyncio.get_event_loop() | ||
| 93 | + # Use small limit so high_water_chunks is small: limit // 4 = 10 | ||
| 94 | + stream = streams.StreamReader(protocol, limit=40, loop=loop) | ||
| 95 | + | ||
| 96 | + assert stream._high_water_chunks == 10 | ||
| 97 | + assert stream._low_water_chunks == 5 | ||
| 98 | + | ||
| 99 | + # Feed chunks until we exceed high_water_chunks | ||
| 100 | + for i in range(12): | ||
| 101 | + stream.begin_http_chunk_receiving() | ||
| 102 | + stream.feed_data(b"x") # 1 byte per chunk | ||
| 103 | + stream.end_http_chunk_receiving() | ||
| 104 | + | ||
| 105 | + # pause_reading should have been called when chunk count exceeded 10 | ||
| 106 | + protocol.pause_reading.assert_called() | ||
| 107 | + | ||
| 108 | + | ||
| 109 | +async def test_stream_reader_resume_on_low_water_chunks( | ||
| 110 | + protocol: mock.Mock, | ||
| 111 | +) -> None: | ||
| 112 | + """Test that reading resumes when chunk count drops below low water mark.""" | ||
| 113 | + loop = asyncio.get_event_loop() | ||
| 114 | + # Use small limit so high_water_chunks is small: limit // 4 = 10 | ||
| 115 | + stream = streams.StreamReader(protocol, limit=40, loop=loop) | ||
| 116 | + | ||
| 117 | + assert stream._high_water_chunks == 10 | ||
| 118 | + assert stream._low_water_chunks == 5 | ||
| 119 | + | ||
| 120 | + # Feed chunks until we exceed high_water_chunks | ||
| 121 | + for i in range(12): | ||
| 122 | + stream.begin_http_chunk_receiving() | ||
| 123 | + stream.feed_data(b"x") # 1 byte per chunk | ||
| 124 | + stream.end_http_chunk_receiving() | ||
| 125 | + | ||
| 126 | + # Simulate that reading was paused | ||
| 127 | + protocol._reading_paused = True | ||
| 128 | + protocol.pause_reading.reset_mock() | ||
| 129 | + | ||
| 130 | + # Read data to reduce both size and chunk count | ||
| 131 | + # Reading will consume chunks and reduce _http_chunk_splits | ||
| 132 | + data = await stream.read(10) | ||
| 133 | + assert data == b"xxxxxxxxxx" | ||
| 134 | + | ||
| 135 | + # resume_reading should have been called when both size and chunk count | ||
| 136 | + # dropped below their respective low water marks | ||
| 137 | + protocol.resume_reading.assert_called() | ||
| 138 | + | ||
| 139 | + | ||
| 140 | +async def test_stream_reader_no_resume_when_chunks_still_high( | ||
| 141 | + protocol: mock.Mock, | ||
| 142 | +) -> None: | ||
| 143 | + """Test that reading doesn't resume if chunk count is still above low water.""" | ||
| 144 | + loop = asyncio.get_event_loop() | ||
| 145 | + # Use small limit so high_water_chunks is small: limit // 4 = 10 | ||
| 146 | + stream = streams.StreamReader(protocol, limit=40, loop=loop) | ||
| 147 | + | ||
| 148 | + # Feed many chunks | ||
| 149 | + for i in range(12): | ||
| 150 | + stream.begin_http_chunk_receiving() | ||
| 151 | + stream.feed_data(b"x") | ||
| 152 | + stream.end_http_chunk_receiving() | ||
| 153 | + | ||
| 154 | + # Simulate that reading was paused | ||
| 155 | + protocol._reading_paused = True | ||
| 156 | + | ||
| 157 | + # Read only a few bytes - chunk count will still be high | ||
| 158 | + data = await stream.read(2) | ||
| 159 | + assert data == b"xx" | ||
| 160 | + | ||
| 161 | + # resume_reading should NOT be called because chunk count is still >= low_water_chunks | ||
| 162 | + protocol.resume_reading.assert_not_called() | ||
| 163 | + | ||
| 164 | + | ||
| 165 | +async def test_stream_reader_read_non_chunked_response( | ||
| 166 | + protocol: mock.Mock, | ||
| 167 | +) -> None: | ||
| 168 | + """Test that non-chunked responses work correctly (no chunk tracking).""" | ||
| 169 | + loop = asyncio.get_event_loop() | ||
| 170 | + stream = streams.StreamReader(protocol, limit=40, loop=loop) | ||
| 171 | + | ||
| 172 | + # Non-chunked: just feed data without begin/end_http_chunk_receiving | ||
| 173 | + stream.feed_data(b"Hello World") | ||
| 174 | + | ||
| 175 | + # _http_chunk_splits should be None for non-chunked responses | ||
| 176 | + assert stream._http_chunk_splits is None | ||
| 177 | + | ||
| 178 | + # Reading should work without issues | ||
| 179 | + data = await stream.read(5) | ||
| 180 | + assert data == b"Hello" | ||
| 181 | + | ||
| 182 | + data = await stream.read(6) | ||
| 183 | + assert data == b" World" | ||
| 184 | + | ||
| 185 | + | ||
| 186 | +async def test_stream_reader_resume_non_chunked_when_paused( | ||
| 187 | + protocol: mock.Mock, | ||
| 188 | +) -> None: | ||
| 189 | + """Test that resume works for non-chunked responses when paused due to size.""" | ||
| 190 | + loop = asyncio.get_event_loop() | ||
| 191 | + # Small limit so we can trigger pause via size | ||
| 192 | + stream = streams.StreamReader(protocol, limit=10, loop=loop) | ||
| 193 | + | ||
| 194 | + # Feed data that exceeds high_water (limit * 2 = 20) | ||
| 195 | + stream.feed_data(b"x" * 25) | ||
| 196 | + | ||
| 197 | + # Simulate that reading was paused due to size | ||
| 198 | + protocol._reading_paused = True | ||
| 199 | + protocol.pause_reading.assert_called() | ||
| 200 | + | ||
| 201 | + # Read enough to drop below low_water (limit = 10) | ||
| 202 | + data = await stream.read(20) | ||
| 203 | + assert data == b"x" * 20 | ||
| 204 | + | ||
| 205 | + # resume_reading should be called (size is now 5 < low_water 10) | ||
| 206 | + protocol.resume_reading.assert_called() | ||
| 207 | + | ||
| 208 | + | ||
| 209 | +@pytest.mark.parametrize("limit", [1, 2, 4]) | ||
| 210 | +async def test_stream_reader_small_limit_resumes_reading( | ||
| 211 | + protocol: mock.Mock, | ||
| 212 | + limit: int, | ||
| 213 | +) -> None: | ||
| 214 | + """Test that small limits still allow resume_reading to be called. | ||
| 215 | + | ||
| 216 | + Even with very small limits, high_water_chunks should be at least 3 | ||
| 217 | + and low_water_chunks should be at least 2, with high > low to ensure | ||
| 218 | + proper flow control. | ||
| 219 | + """ | ||
| 220 | + loop = asyncio.get_event_loop() | ||
| 221 | + stream = streams.StreamReader(protocol, limit=limit, loop=loop) | ||
| 222 | + | ||
| 223 | + # Verify minimum thresholds are enforced and high > low | ||
| 224 | + assert stream._high_water_chunks >= 3 | ||
| 225 | + assert stream._low_water_chunks >= 2 | ||
| 226 | + assert stream._high_water_chunks > stream._low_water_chunks | ||
| 227 | + | ||
| 228 | + # Set up pause/resume side effects | ||
| 229 | + def pause_reading() -> None: | ||
| 230 | + protocol._reading_paused = True | ||
| 231 | + | ||
| 232 | + protocol.pause_reading.side_effect = pause_reading | ||
| 233 | + | ||
| 234 | + def resume_reading() -> None: | ||
| 235 | + protocol._reading_paused = False | ||
| 236 | + | ||
| 237 | + protocol.resume_reading.side_effect = resume_reading | ||
| 238 | + | ||
| 239 | + # Feed 4 chunks (triggers pause at > high_water_chunks which is >= 3) | ||
| 240 | + for char in b"abcd": | ||
| 241 | + stream.begin_http_chunk_receiving() | ||
| 242 | + stream.feed_data(bytes([char])) | ||
| 243 | + stream.end_http_chunk_receiving() | ||
| 244 | + | ||
| 245 | + # Reading should now be paused | ||
| 246 | + assert protocol._reading_paused is True | ||
| 247 | + assert protocol.pause_reading.called | ||
| 248 | + | ||
| 249 | + # Read all data - should resume (chunk count drops below low_water_chunks) | ||
| 250 | + data = stream.read_nowait() | ||
| 251 | + assert data == b"abcd" | ||
| 252 | + assert stream._size == 0 | ||
| 253 | + | ||
| 254 | + protocol.resume_reading.assert_called() | ||
| 255 | + assert protocol._reading_paused is False | ||
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.12.15.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.12.15.bb index 55ff57d05c..84dd369753 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.12.15.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.12.15.bb | |||
| @@ -9,6 +9,8 @@ SRC_URI += "file://CVE-2025-69224.patch \ | |||
| 9 | file://CVE-2025-69226.patch \ | 9 | file://CVE-2025-69226.patch \ |
| 10 | file://CVE-2025-69227.patch \ | 10 | file://CVE-2025-69227.patch \ |
| 11 | file://CVE-2025-69228.patch \ | 11 | file://CVE-2025-69228.patch \ |
| 12 | file://CVE-2025-69229-1.patch \ | ||
| 13 | file://CVE-2025-69229-2.patch \ | ||
| 12 | " | 14 | " |
| 13 | SRC_URI[sha256sum] = "4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2" | 15 | SRC_URI[sha256sum] = "4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2" |
| 14 | 16 | ||
