summaryrefslogtreecommitdiffstats
path: root/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229-2.patch
diff options
context:
space:
mode:
Diffstat (limited to 'meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229-2.patch')
-rw-r--r--meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229-2.patch255
1 files changed, 255 insertions, 0 deletions
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 @@
1From e124809ca5f17e608c09fc79423f9c357208a3c5 Mon Sep 17 00:00:00 2001
2From: Gyorgy Sarvari <skandigraun@gmail.com>
3Date: Sat, 3 Jan 2026 15:23:14 +0000
4Subject: [PATCH 2/2] Limit number of chunks before pausing reading (#11894)
5 (#11916)
6
7From: Sam Bull <git@sambull.org>
8
9(cherry picked from commit 1e4120e87daec963c67f956111e6bca44d7c3dea)
10
11Co-authored-by: J. Nick Koston <nick@koston.org>
12
13CVE: CVE-2025-69229
14Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/4ed97a4e46eaf61bd0f05063245f613469700229]
15Signed-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
21diff --git a/aiohttp/streams.py b/aiohttp/streams.py
22index 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
78diff --git a/tests/test_streams.py b/tests/test_streams.py
79index 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