summaryrefslogtreecommitdiffstats
path: root/meta-python
diff options
context:
space:
mode:
authorGyorgy Sarvari <skandigraun@gmail.com>2026-02-05 07:59:36 +0100
committerAnuj Mittal <anuj.mittal@oss.qualcomm.com>2026-02-19 08:20:16 +0530
commit891e25f9bfc768d32dd2143adf19cd910840885f (patch)
treeb25be4644c18e5e3de22d9241aef6b5edb94bfbe /meta-python
parentd28d0a23615f4d91d125fdcd247a71245087dab3 (diff)
downloadmeta-openembedded-891e25f9bfc768d32dd2143adf19cd910840885f.tar.gz
python3-cbor2: patch CVE-2025-68131
Details: https://nvd.nist.gov/vuln/detail/CVE-2025-68131 The NVD report mentions a PR as the solution, however in the discussion of that PR it turned out that this is incorrect, and another patch is the solution. That patch was picked. Ptests passed successfully. Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com> Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
Diffstat (limited to 'meta-python')
-rw-r--r--meta-python/recipes-devtools/python/python3-cbor2/CVE-2025-68131.patch515
-rw-r--r--meta-python/recipes-devtools/python/python3-cbor2_5.7.1.bb1
2 files changed, 516 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-cbor2/CVE-2025-68131.patch b/meta-python/recipes-devtools/python/python3-cbor2/CVE-2025-68131.patch
new file mode 100644
index 0000000000..bf6819eebe
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-cbor2/CVE-2025-68131.patch
@@ -0,0 +1,515 @@
1From 60b74e9842e83318efccf0f4eed6a94a07ac5677 Mon Sep 17 00:00:00 2001
2From: Andreas Eriksen <andreer@vespa.ai>
3Date: Thu, 18 Dec 2025 16:48:26 +0100
4Subject: [PATCH] Merge commit from fork
5
6* track depth of recursive encode/decode, clear shared refs on start
7
8* test that shared refs are cleared on start
9
10* add fix-shared-state-reset to version history
11
12* clear shared state _after_ encode/decode
13
14* use PY_SSIZE_T_MAX to clear shareables list
15
16* use context manager for python decoder depth tracking
17
18* use context manager for python encoder depth tracking
19
20CVE: CVE-2025-68131
21Upstream-Status: Backport [https://github.com/agronholm/cbor2/commit/f1d701cd2c411ee40bb1fe383afe7f365f35abf0]
22Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
23---
24 cbor2/_decoder.py | 38 +++++++++++++++++-----
25 cbor2/_encoder.py | 44 +++++++++++++++++++++-----
26 source/decoder.c | 28 ++++++++++++++++-
27 source/decoder.h | 1 +
28 source/encoder.c | 23 ++++++++++++--
29 source/encoder.h | 1 +
30 tests/test_decoder.py | 62 ++++++++++++++++++++++++++++++++++++
31 tests/test_encoder.py | 70 +++++++++++++++++++++++++++++++++++++++++
32 8 files changed, 250 insertions(+), 17 deletions(-)
33
34diff --git a/cbor2/_decoder.py b/cbor2/_decoder.py
35index 42a9740..b552492 100644
36--- a/cbor2/_decoder.py
37+++ b/cbor2/_decoder.py
38@@ -5,6 +5,7 @@ import struct
39 import sys
40 from codecs import getincrementaldecoder
41 from collections.abc import Callable, Mapping, Sequence
42+from contextlib import contextmanager
43 from datetime import date, datetime, timedelta, timezone
44 from io import BytesIO
45 from typing import IO, TYPE_CHECKING, Any, TypeVar, cast, overload
46@@ -59,6 +60,7 @@ class CBORDecoder:
47 "_immutable",
48 "_str_errors",
49 "_stringref_namespace",
50+ "_decode_depth",
51 )
52
53 _fp: IO[bytes]
54@@ -100,6 +102,7 @@ class CBORDecoder:
55 self._shareables: list[object] = []
56 self._stringref_namespace: list[str | bytes] | None = None
57 self._immutable = False
58+ self._decode_depth = 0
59
60 @property
61 def immutable(self) -> bool:
62@@ -225,13 +228,33 @@ class CBORDecoder:
63 if unshared:
64 self._share_index = old_index
65
66+ @contextmanager
67+ def _decoding_context(self):
68+ """
69+ Context manager for tracking decode depth and clearing shared state.
70+
71+ Shared state is cleared at the end of each top-level decode to prevent
72+ shared references from leaking between independent decode operations.
73+ Nested calls (from hooks) must preserve the state.
74+ """
75+ self._decode_depth += 1
76+ try:
77+ yield
78+ finally:
79+ self._decode_depth -= 1
80+ assert self._decode_depth >= 0
81+ if self._decode_depth == 0:
82+ self._shareables.clear()
83+ self._share_index = None
84+
85 def decode(self) -> object:
86 """
87 Decode the next value from the stream.
88
89 :raises CBORDecodeError: if there is any problem decoding the stream
90 """
91- return self._decode()
92+ with self._decoding_context():
93+ return self._decode()
94
95 def decode_from_bytes(self, buf: bytes) -> object:
96 """
97@@ -242,12 +265,13 @@ class CBORDecoder:
98 object needs to be decoded separately from the rest but while still
99 taking advantage of the shared value registry.
100 """
101- with BytesIO(buf) as fp:
102- old_fp = self.fp
103- self.fp = fp
104- retval = self._decode()
105- self.fp = old_fp
106- return retval
107+ with self._decoding_context():
108+ with BytesIO(buf) as fp:
109+ old_fp = self.fp
110+ self.fp = fp
111+ retval = self._decode()
112+ self.fp = old_fp
113+ return retval
114
115 @overload
116 def _decode_length(self, subtype: int) -> int: ...
117diff --git a/cbor2/_encoder.py b/cbor2/_encoder.py
118index fe65763..5b9609c 100644
119--- a/cbor2/_encoder.py
120+++ b/cbor2/_encoder.py
121@@ -124,6 +124,7 @@ class CBOREncoder:
122 "string_namespacing",
123 "_string_references",
124 "indefinite_containers",
125+ "_encode_depth",
126 )
127
128 _fp: IO[bytes]
129@@ -188,6 +189,7 @@ class CBOREncoder:
130 int, tuple[object, int | None]
131 ] = {} # indexes used for value sharing
132 self._string_references: dict[str | bytes, int] = {} # indexes used for string references
133+ self._encode_depth = 0
134 self._encoders = default_encoders.copy()
135 if canonical:
136 self._encoders.update(canonical_encoders)
137@@ -303,6 +305,24 @@ class CBOREncoder:
138 """
139 self._fp_write(data)
140
141+ @contextmanager
142+ def _encoding_context(self):
143+ """
144+ Context manager for tracking encode depth and clearing shared state.
145+
146+ Shared state is cleared at the end of each top-level encode to prevent
147+ shared references from leaking between independent encode operations.
148+ Nested calls (from hooks) must preserve the state.
149+ """
150+ self._encode_depth += 1
151+ try:
152+ yield
153+ finally:
154+ self._encode_depth -= 1
155+ if self._encode_depth == 0:
156+ self._shared_containers.clear()
157+ self._string_references.clear()
158+
159 def encode(self, obj: Any) -> None:
160 """
161 Encode the given object using CBOR.
162@@ -310,6 +330,16 @@ class CBOREncoder:
163 :param obj:
164 the object to encode
165 """
166+ with self._encoding_context():
167+ self._encode_value(obj)
168+
169+ def _encode_value(self, obj: Any) -> None:
170+ """
171+ Internal fast path for encoding - used by built-in encoders.
172+
173+ External code should use encode() instead, which properly manages
174+ shared state between independent encode operations.
175+ """
176 obj_type = obj.__class__
177 encoder = self._encoders.get(obj_type) or self._find_encoder(obj_type) or self._default
178 if not encoder:
179@@ -459,7 +489,7 @@ class CBOREncoder:
180 def encode_array(self, value: Sequence[Any]) -> None:
181 self.encode_length(4, len(value) if not self.indefinite_containers else None)
182 for item in value:
183- self.encode(item)
184+ self._encode_value(item)
185
186 if self.indefinite_containers:
187 self.encode_break()
188@@ -468,8 +498,8 @@ class CBOREncoder:
189 def encode_map(self, value: Mapping[Any, Any]) -> None:
190 self.encode_length(5, len(value) if not self.indefinite_containers else None)
191 for key, val in value.items():
192- self.encode(key)
193- self.encode(val)
194+ self._encode_value(key)
195+ self._encode_value(val)
196
197 if self.indefinite_containers:
198 self.encode_break()
199@@ -494,10 +524,10 @@ class CBOREncoder:
200 # String referencing requires that the order encoded is
201 # the same as the order emitted so string references are
202 # generated after an order is determined
203- self.encode(realkey)
204+ self._encode_value(realkey)
205 else:
206 self._fp_write(sortkey[1])
207- self.encode(value)
208+ self._encode_value(value)
209
210 if self.indefinite_containers:
211 self.encode_break()
212@@ -511,7 +541,7 @@ class CBOREncoder:
213 self._string_references = {}
214
215 self.encode_length(6, value.tag)
216- self.encode(value.value)
217+ self._encode_value(value.value)
218
219 self.string_referencing = old_string_referencing
220 self._string_references = old_string_references
221@@ -574,7 +604,7 @@ class CBOREncoder:
222 def encode_stringref(self, value: str | bytes) -> None:
223 # Semantic tag 25
224 if not self._stringref(value):
225- self.encode(value)
226+ self._encode_value(value)
227
228 def encode_rational(self, value: Fraction) -> None:
229 # Semantic tag 30
230diff --git a/source/decoder.c b/source/decoder.c
231index 8b6b842..b0bdb9a 100644
232--- a/source/decoder.c
233+++ b/source/decoder.c
234@@ -143,6 +143,7 @@ CBORDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
235 self->str_errors = PyBytes_FromString("strict");
236 self->immutable = false;
237 self->shared_index = -1;
238+ self->decode_depth = 0;
239 }
240 return (PyObject *) self;
241 error:
242@@ -2083,11 +2084,30 @@ decode(CBORDecoderObject *self, DecodeOptions options)
243 }
244
245
246+// Reset shared state at the end of each top-level decode to prevent
247+// shared references from leaking between independent decode operations.
248+// Nested calls (from hooks) must preserve the state.
249+static inline void
250+clear_shareable_state(CBORDecoderObject *self)
251+{
252+ PyList_SetSlice(self->shareables, 0, PY_SSIZE_T_MAX, NULL);
253+ self->shared_index = -1;
254+}
255+
256+
257 // CBORDecoder.decode(self) -> obj
258 PyObject *
259 CBORDecoder_decode(CBORDecoderObject *self)
260 {
261- return decode(self, DECODE_NORMAL);
262+ PyObject *ret;
263+ self->decode_depth++;
264+ ret = decode(self, DECODE_NORMAL);
265+ self->decode_depth--;
266+ assert(self->decode_depth >= 0);
267+ if (self->decode_depth == 0) {
268+ clear_shareable_state(self);
269+ }
270+ return ret;
271 }
272
273
274@@ -2100,6 +2120,7 @@ CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data)
275 if (!_CBOR2_BytesIO && _CBOR2_init_BytesIO() == -1)
276 return NULL;
277
278+ self->decode_depth++;
279 save_read = self->read;
280 buf = PyObject_CallFunctionObjArgs(_CBOR2_BytesIO, data, NULL);
281 if (buf) {
282@@ -2111,6 +2132,11 @@ CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data)
283 Py_DECREF(buf);
284 }
285 self->read = save_read;
286+ self->decode_depth--;
287+ assert(self->decode_depth >= 0);
288+ if (self->decode_depth == 0) {
289+ clear_shareable_state(self);
290+ }
291 return ret;
292 }
293
294diff --git a/source/decoder.h b/source/decoder.h
295index 6bb6d52..a2f1bcb 100644
296--- a/source/decoder.h
297+++ b/source/decoder.h
298@@ -13,6 +13,7 @@ typedef struct {
299 PyObject *str_errors;
300 bool immutable;
301 Py_ssize_t shared_index;
302+ Py_ssize_t decode_depth;
303 } CBORDecoderObject;
304
305 extern PyTypeObject CBORDecoderType;
306diff --git a/source/encoder.c b/source/encoder.c
307index 4dc3c6b..e87670d 100644
308--- a/source/encoder.c
309+++ b/source/encoder.c
310@@ -114,6 +114,7 @@ CBOREncoder_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
311 self->string_referencing = false;
312 self->string_namespacing = false;
313 self->indefinite_containers = false;
314+ self->encode_depth = 0;
315 }
316 return (PyObject *) self;
317 }
318@@ -2132,17 +2133,35 @@ encode(CBOREncoderObject *self, PyObject *value)
319 }
320
321
322+// Reset shared state at the end of each top-level encode to prevent
323+// shared references from leaking between independent encode operations.
324+// Nested calls (from hooks or recursive encoding) must preserve the state.
325+static inline void
326+clear_shared_state(CBOREncoderObject *self)
327+{
328+ PyDict_Clear(self->shared);
329+ PyDict_Clear(self->string_references);
330+}
331+
332+
333 // CBOREncoder.encode(self, value)
334 PyObject *
335 CBOREncoder_encode(CBOREncoderObject *self, PyObject *value)
336 {
337 PyObject *ret;
338
339- // TODO reset shared dict?
340- if (Py_EnterRecursiveCall(" in CBOREncoder.encode"))
341+ self->encode_depth++;
342+ if (Py_EnterRecursiveCall(" in CBOREncoder.encode")) {
343+ self->encode_depth--;
344 return NULL;
345+ }
346 ret = encode(self, value);
347 Py_LeaveRecursiveCall();
348+ self->encode_depth--;
349+ assert(self->encode_depth >= 0);
350+ if (self->encode_depth == 0) {
351+ clear_shared_state(self);
352+ }
353 return ret;
354 }
355
356diff --git a/source/encoder.h b/source/encoder.h
357index abc6560..915f1f2 100644
358--- a/source/encoder.h
359+++ b/source/encoder.h
360@@ -25,6 +25,7 @@ typedef struct {
361 bool string_referencing;
362 bool string_namespacing;
363 bool indefinite_containers;
364+ Py_ssize_t encode_depth;
365 } CBOREncoderObject;
366
367 extern PyTypeObject CBOREncoderType;
368diff --git a/tests/test_decoder.py b/tests/test_decoder.py
369index 0f4af4d..c8b47d5 100644
370--- a/tests/test_decoder.py
371+++ b/tests/test_decoder.py
372@@ -1022,3 +1022,65 @@ def test_oversized_read(impl, payload: bytes, tmp_path: Path) -> None:
373 dummy_path.write_bytes(payload)
374 with dummy_path.open("rb") as f:
375 impl.load(f)
376+
377+
378+class TestDecoderReuse:
379+ """
380+ Tests for correct behavior when reusing CBORDecoder instances.
381+ """
382+
383+ def test_decoder_reuse_resets_shared_refs(self, impl):
384+ """
385+ Shared references should be scoped to a single decode operation,
386+ not persist across multiple decodes on the same decoder instance.
387+ """
388+ # Message with shareable tag (28)
389+ msg1 = impl.dumps(impl.CBORTag(28, "first_value"))
390+
391+ # Message with sharedref tag (29) referencing index 0
392+ msg2 = impl.dumps(impl.CBORTag(29, 0))
393+
394+ # Reuse decoder across messages
395+ decoder = impl.CBORDecoder(BytesIO(msg1))
396+ result1 = decoder.decode()
397+ assert result1 == "first_value"
398+
399+ # Second decode should fail - sharedref(0) doesn't exist in this context
400+ decoder.fp = BytesIO(msg2)
401+ with pytest.raises(impl.CBORDecodeValueError, match="shared reference"):
402+ decoder.decode()
403+
404+ def test_decode_from_bytes_resets_shared_refs(self, impl):
405+ """
406+ decode_from_bytes should also reset shared references between calls.
407+ """
408+ msg1 = impl.dumps(impl.CBORTag(28, "value"))
409+ msg2 = impl.dumps(impl.CBORTag(29, 0))
410+
411+ decoder = impl.CBORDecoder(BytesIO(b""))
412+ decoder.decode_from_bytes(msg1)
413+
414+ with pytest.raises(impl.CBORDecodeValueError, match="shared reference"):
415+ decoder.decode_from_bytes(msg2)
416+
417+ def test_shared_refs_within_single_decode(self, impl):
418+ """
419+ Shared references must work correctly within a single decode operation.
420+
421+ Note: This tests non-cyclic sibling references [shareable(x), sharedref(0)],
422+ which is a different pattern from test_cyclic_array/test_cyclic_map that
423+ test self-referencing structures like shareable([sharedref(0)]).
424+ """
425+ # [shareable("hello"), sharedref(0)] -> ["hello", "hello"]
426+ data = unhexlify(
427+ "82" # array(2)
428+ "d81c" # tag(28) shareable
429+ "65" # text(5)
430+ "68656c6c6f" # "hello"
431+ "d81d" # tag(29) sharedref
432+ "00" # unsigned(0)
433+ )
434+
435+ result = impl.loads(data)
436+ assert result == ["hello", "hello"]
437+ assert result[0] is result[1] # Same object reference
438diff --git a/tests/test_encoder.py b/tests/test_encoder.py
439index cbb4295..e6adc08 100644
440--- a/tests/test_encoder.py
441+++ b/tests/test_encoder.py
442@@ -717,3 +717,73 @@ def test_indefinite_containers(impl):
443 expected = b"\xbf\xff"
444 assert impl.dumps({}, indefinite_containers=True) == expected
445 assert impl.dumps({}, indefinite_containers=True, canonical=True) == expected
446+
447+
448+class TestEncoderReuse:
449+ """
450+ Tests for correct behavior when reusing CBOREncoder instances.
451+ """
452+
453+ def test_encoder_reuse_resets_shared_containers(self, impl):
454+ """
455+ Shared container tracking should be scoped to a single encode operation,
456+ not persist across multiple encodes on the same encoder instance.
457+ """
458+ fp = BytesIO()
459+ encoder = impl.CBOREncoder(fp, value_sharing=True)
460+ shared_obj = ["hello"]
461+
462+ # First encode: object is tracked in shared containers
463+ encoder.encode([shared_obj, shared_obj])
464+
465+ # Second encode on new fp: should produce valid standalone CBOR
466+ # (not a sharedref pointing to stale first-encode data)
467+ encoder.fp = BytesIO()
468+ encoder.encode(shared_obj)
469+ second_output = encoder.fp.getvalue()
470+
471+ # The second output must be decodable on its own
472+ result = impl.loads(second_output)
473+ assert result == ["hello"]
474+
475+ def test_encode_to_bytes_resets_shared_containers(self, impl):
476+ """
477+ encode_to_bytes should also reset shared container tracking between calls.
478+ """
479+ fp = BytesIO()
480+ encoder = impl.CBOREncoder(fp, value_sharing=True)
481+ shared_obj = ["hello"]
482+
483+ # First encode
484+ encoder.encode_to_bytes([shared_obj, shared_obj])
485+
486+ # Second encode should produce valid standalone CBOR
487+ result_bytes = encoder.encode_to_bytes(shared_obj)
488+ result = impl.loads(result_bytes)
489+ assert result == ["hello"]
490+
491+ def test_encoder_hook_does_not_reset_state(self, impl):
492+ """
493+ When a custom encoder hook calls encode(), the shared container
494+ tracking should be preserved (not reset mid-operation).
495+ """
496+
497+ class Custom:
498+ def __init__(self, value):
499+ self.value = value
500+
501+ def custom_encoder(encoder, obj):
502+ # Hook encodes the wrapped value
503+ encoder.encode(obj.value)
504+
505+ # Encode a Custom wrapping a list
506+ data = impl.dumps(Custom(["a", "b"]), default=custom_encoder)
507+
508+ # Verify the output decodes correctly
509+ result = impl.loads(data)
510+ assert result == ["a", "b"]
511+
512+ # Test nested Custom objects - hook should work recursively
513+ data2 = impl.dumps(Custom(Custom(["x"])), default=custom_encoder)
514+ result2 = impl.loads(data2)
515+ assert result2 == ["x"]
diff --git a/meta-python/recipes-devtools/python/python3-cbor2_5.7.1.bb b/meta-python/recipes-devtools/python/python3-cbor2_5.7.1.bb
index e124207cbf..5aa5ba599f 100644
--- a/meta-python/recipes-devtools/python/python3-cbor2_5.7.1.bb
+++ b/meta-python/recipes-devtools/python/python3-cbor2_5.7.1.bb
@@ -3,6 +3,7 @@ LICENSE = "MIT"
3LIC_FILES_CHKSUM = "file://LICENSE.txt;md5=a79e64179819c7ce293372c059f1dbd8" 3LIC_FILES_CHKSUM = "file://LICENSE.txt;md5=a79e64179819c7ce293372c059f1dbd8"
4DEPENDS += "python3-setuptools-scm-native" 4DEPENDS += "python3-setuptools-scm-native"
5 5
6SRC_URI += "file://CVE-2025-68131.patch"
6SRC_URI[sha256sum] = "7a405a1d7c8230ee9acf240aad48ae947ef584e8af05f169f3c1bde8f01f8b71" 7SRC_URI[sha256sum] = "7a405a1d7c8230ee9acf240aad48ae947ef584e8af05f169f3c1bde8f01f8b71"
7 8
8inherit pypi python_setuptools_build_meta ptest-python-pytest 9inherit pypi python_setuptools_build_meta ptest-python-pytest