summaryrefslogtreecommitdiffstats
path: root/meta-python
diff options
context:
space:
mode:
authorHitendra Prajapati <hprajapati@mvista.com>2026-03-31 11:45:06 +0530
committerAnuj Mittal <anuj.mittal@oss.qualcomm.com>2026-04-03 15:00:47 +0530
commit4810cd8c5bbc0b4349a78eac85a6a882bc0b03a2 (patch)
tree772d54d051e1374ce4d971d0673a17a39bcf91e2 /meta-python
parentb13ae5a8eb286f8f4e53c290d5b09e15c303289c (diff)
downloadmeta-openembedded-4810cd8c5bbc0b4349a78eac85a6a882bc0b03a2.tar.gz
python3-cbor2: patch CVE-2026-26209
Backport the patch[1] which fixes this vulnerability as mentioned in the comment[3]. Details: https://nvd.nist.gov/vuln/detail/CVE-2026-26209 [1] https://github.com/agronholm/cbor2/commit/e61a5f365ba610d5907a0ae1bc72769bba34294b [2] https://github.com/agronholm/cbor2/commit/fb4ee1612a8a1ac0dbd8cf2f2f6f931a4e06d824 (pre patch) [3] https://github.com/agronholm/cbor2/pull/275 Dropped changes to the changelog from the original commit. Signed-off-by: Hitendra Prajapati <hprajapati@mvista.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-2026-26209-pre1.patch469
-rw-r--r--meta-python/recipes-devtools/python/python3-cbor2/CVE-2026-26209.patch415
-rw-r--r--meta-python/recipes-devtools/python/python3-cbor2_5.6.4.bb2
3 files changed, 886 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-cbor2/CVE-2026-26209-pre1.patch b/meta-python/recipes-devtools/python/python3-cbor2/CVE-2026-26209-pre1.patch
new file mode 100644
index 0000000000..3ff275c247
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-cbor2/CVE-2026-26209-pre1.patch
@@ -0,0 +1,469 @@
1From fb4ee1612a8a1ac0dbd8cf2f2f6f931a4e06d824 Mon Sep 17 00:00:00 2001
2From: Andreas Eriksen <andreer@vespa.ai>
3Date: Mon, 29 Dec 2025 14:01:52 +0100
4Subject: [PATCH] Added a read-ahead buffer to the C decoder (#268)
5
6CVE: CVE-2026-26209
7Upstream-Status: Backport [https://github.com/agronholm/cbor2/commit/fb4ee1612a8a1ac0dbd8cf2f2f6f931a4e06d824]
8Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
9---
10 source/decoder.c | 225 +++++++++++++++++++++++++++++++++---------
11 source/decoder.h | 9 ++
12 tests/test_decoder.py | 85 +++++++++++++++-
13 3 files changed, 274 insertions(+), 45 deletions(-)
14
15diff --git a/source/decoder.c b/source/decoder.c
16index 4f7ee5d..9cd1596 100644
17--- a/source/decoder.c
18+++ b/source/decoder.c
19@@ -42,6 +42,7 @@ enum DecodeOption {
20 typedef uint8_t DecodeOptions;
21
22 static int _CBORDecoder_set_fp(CBORDecoderObject *, PyObject *, void *);
23+static int _CBORDecoder_set_fp_with_read_size(CBORDecoderObject *, PyObject *, Py_ssize_t);
24 static int _CBORDecoder_set_tag_hook(CBORDecoderObject *, PyObject *, void *);
25 static int _CBORDecoder_set_object_hook(CBORDecoderObject *, PyObject *, void *);
26 static int _CBORDecoder_set_str_errors(CBORDecoderObject *, PyObject *, void *);
27@@ -101,6 +102,13 @@ CBORDecoder_clear(CBORDecoderObject *self)
28 Py_CLEAR(self->shareables);
29 Py_CLEAR(self->stringref_namespace);
30 Py_CLEAR(self->str_errors);
31+ if (self->readahead) {
32+ PyMem_Free(self->readahead);
33+ self->readahead = NULL;
34+ self->readahead_size = 0;
35+ }
36+ self->read_pos = 0;
37+ self->read_len = 0;
38 return 0;
39 }
40
41@@ -143,6 +151,10 @@ CBORDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
42 self->immutable = false;
43 self->shared_index = -1;
44 self->decode_depth = 0;
45+ self->readahead = NULL;
46+ self->readahead_size = 0;
47+ self->read_pos = 0;
48+ self->read_len = 0;
49 }
50 return (PyObject *) self;
51 error:
52@@ -152,21 +164,27 @@ error:
53
54
55 // CBORDecoder.__init__(self, fp=None, tag_hook=None, object_hook=None,
56-// str_errors='strict')
57+// str_errors='strict', read_size=4096)
58 int
59 CBORDecoder_init(CBORDecoderObject *self, PyObject *args, PyObject *kwargs)
60 {
61 static char *keywords[] = {
62- "fp", "tag_hook", "object_hook", "str_errors", NULL
63+ "fp", "tag_hook", "object_hook", "str_errors", "read_size", NULL
64 };
65 PyObject *fp = NULL, *tag_hook = NULL, *object_hook = NULL,
66 *str_errors = NULL;
67+ Py_ssize_t read_size = CBOR2_DEFAULT_READ_SIZE;
68
69- if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OOO", keywords,
70- &fp, &tag_hook, &object_hook, &str_errors))
71+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OOOn", keywords,
72+ &fp, &tag_hook, &object_hook, &str_errors, &read_size))
73 return -1;
74
75- if (_CBORDecoder_set_fp(self, fp, NULL) == -1)
76+ if (read_size < 1) {
77+ PyErr_SetString(PyExc_ValueError, "read_size must be at least 1");
78+ return -1;
79+ }
80+
81+ if (_CBORDecoder_set_fp_with_read_size(self, fp, read_size) == -1)
82 return -1;
83 if (tag_hook && _CBORDecoder_set_tag_hook(self, tag_hook, NULL) == -1)
84 return -1;
85@@ -197,11 +215,12 @@ _CBORDecoder_get_fp(CBORDecoderObject *self, void *closure)
86 }
87
88
89-// CBORDecoder._set_fp(self, value)
90+// Internal: set fp with configurable read size
91 static int
92-_CBORDecoder_set_fp(CBORDecoderObject *self, PyObject *value, void *closure)
93+_CBORDecoder_set_fp_with_read_size(CBORDecoderObject *self, PyObject *value, Py_ssize_t read_size)
94 {
95 PyObject *tmp, *read;
96+ char *new_buffer = NULL;
97
98 if (!value) {
99 PyErr_SetString(PyExc_AttributeError, "cannot delete fp attribute");
100@@ -214,13 +233,43 @@ _CBORDecoder_set_fp(CBORDecoderObject *self, PyObject *value, void *closure)
101 return -1;
102 }
103
104+ if (self->readahead == NULL || self->readahead_size != read_size) {
105+ new_buffer = (char *)PyMem_Malloc(read_size);
106+ if (!new_buffer) {
107+ Py_DECREF(read);
108+ PyErr_NoMemory();
109+ return -1;
110+ }
111+ }
112+
113 // See notes in encoder.c / _CBOREncoder_set_fp
114 tmp = self->read;
115 self->read = read;
116 Py_DECREF(tmp);
117+
118+ self->read_pos = 0;
119+ self->read_len = 0;
120+
121+ // Replace buffer (size changed or was NULL)
122+ if (new_buffer) {
123+ PyMem_Free(self->readahead);
124+ self->readahead = new_buffer;
125+ self->readahead_size = read_size;
126+ }
127+
128 return 0;
129 }
130
131+// CBORDecoder._set_fp(self, value) - property setter uses default read size
132+static int
133+_CBORDecoder_set_fp(CBORDecoderObject *self, PyObject *value, void *closure)
134+{
135+ // Use existing readahead_size if already allocated, otherwise use default
136+ Py_ssize_t read_size = (self->readahead_size > 0) ?
137+ self->readahead_size : CBOR2_DEFAULT_READ_SIZE;
138+ return _CBORDecoder_set_fp_with_read_size(self, value, read_size);
139+}
140+
141
142 // CBORDecoder._get_tag_hook(self)
143 static PyObject *
144@@ -376,45 +425,93 @@ raise_from(PyObject *new_exc_type, const char *message) {
145 }
146 }
147
148-static PyObject *
149-fp_read_object(CBORDecoderObject *self, const Py_ssize_t size)
150+// Read directly into caller's buffer (bypassing readahead buffer)
151+static Py_ssize_t
152+fp_read_bytes(CBORDecoderObject *self, char *buf, Py_ssize_t size)
153 {
154- PyObject *ret = NULL;
155- PyObject *obj, *size_obj;
156- size_obj = PyLong_FromSsize_t(size);
157- if (size_obj) {
158- obj = PyObject_CallFunctionObjArgs(self->read, size_obj, NULL);
159- Py_DECREF(size_obj);
160- if (obj) {
161- assert(PyBytes_CheckExact(obj));
162- if (PyBytes_GET_SIZE(obj) == (Py_ssize_t) size) {
163- ret = obj;
164+ PyObject *size_obj = PyLong_FromSsize_t(size);
165+ if (!size_obj)
166+ return -1;
167+
168+ PyObject *obj = PyObject_CallFunctionObjArgs(self->read, size_obj, NULL);
169+ Py_DECREF(size_obj);
170+ if (!obj)
171+ return -1;
172+
173+ assert(PyBytes_CheckExact(obj));
174+ Py_ssize_t bytes_read = PyBytes_GET_SIZE(obj);
175+ if (bytes_read > 0)
176+ memcpy(buf, PyBytes_AS_STRING(obj), bytes_read);
177+
178+ Py_DECREF(obj);
179+ return bytes_read;
180+}
181+
182+// Read into caller's buffer using the readahead buffer
183+static int
184+fp_read(CBORDecoderObject *self, char *buf, const Py_ssize_t size)
185+{
186+ Py_ssize_t available, to_copy, remaining, total_copied;
187+
188+ remaining = size;
189+ total_copied = 0;
190+
191+ while (remaining > 0) {
192+ available = self->read_len - self->read_pos;
193+
194+ if (available > 0) {
195+ // Copy from buffer
196+ to_copy = (available < remaining) ? available : remaining;
197+ memcpy(buf + total_copied, self->readahead + self->read_pos, to_copy);
198+ self->read_pos += to_copy;
199+ total_copied += to_copy;
200+ remaining -= to_copy;
201+ } else {
202+ Py_ssize_t bytes_read;
203+
204+ if (remaining >= self->readahead_size) {
205+ // Large remaining: read directly into destination, bypass buffer
206+ bytes_read = fp_read_bytes(self, buf + total_copied, remaining);
207+ if (bytes_read > 0) {
208+ total_copied += bytes_read;
209+ remaining -= bytes_read;
210+ }
211 } else {
212- PyErr_Format(
213- _CBOR2_CBORDecodeEOF,
214- "premature end of stream (expected to read %zd bytes, "
215- "got %zd instead)", size, PyBytes_GET_SIZE(obj));
216- Py_DECREF(obj);
217+ // Small remaining: refill buffer
218+ self->read_pos = 0;
219+ self->read_len = 0;
220+ bytes_read = fp_read_bytes(self, self->readahead, self->readahead_size);
221+ if (bytes_read > 0)
222+ self->read_len = bytes_read;
223+ }
224+
225+ if (bytes_read <= 0) {
226+ if (bytes_read == 0)
227+ PyErr_Format(
228+ _CBOR2_CBORDecodeEOF,
229+ "premature end of stream (expected to read %zd bytes, "
230+ "got %zd instead)", size, total_copied);
231+ return -1;
232 }
233 }
234 }
235- return ret;
236-}
237
238+ return 0;
239+}
240
241-static int
242-fp_read(CBORDecoderObject *self, char *buf, const Py_ssize_t size)
243+// Read and return as PyBytes object
244+static PyObject *
245+fp_read_object(CBORDecoderObject *self, const Py_ssize_t size)
246 {
247- int ret = -1;
248- PyObject *obj = fp_read_object(self, size);
249- if (obj) {
250- char *data = PyBytes_AS_STRING(obj);
251- if (data) {
252- memcpy(buf, data, size);
253- ret = 0;
254- }
255- Py_DECREF(obj);
256+ PyObject *ret = PyBytes_FromStringAndSize(NULL, size);
257+ if (!ret)
258+ return NULL;
259+
260+ if (fp_read(self, PyBytes_AS_STRING(ret), size) == -1) {
261+ Py_DECREF(ret);
262+ return NULL;
263 }
264+
265 return ret;
266 }
267
268@@ -2091,23 +2188,55 @@ static PyObject *
269 CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data)
270 {
271 PyObject *save_read, *buf, *ret = NULL;
272+ bool is_nested = (self->decode_depth > 0);
273+ Py_ssize_t save_read_pos = 0, save_read_len = 0;
274+ char *save_buffer = NULL;
275
276 if (!_CBOR2_BytesIO && _CBOR2_init_BytesIO() == -1)
277 return NULL;
278
279+ buf = PyObject_CallFunctionObjArgs(_CBOR2_BytesIO, data, NULL);
280+ if (!buf)
281+ return NULL;
282+
283 self->decode_depth++;
284 save_read = self->read;
285- buf = PyObject_CallFunctionObjArgs(_CBOR2_BytesIO, data, NULL);
286- if (buf) {
287- self->read = PyObject_GetAttr(buf, _CBOR2_str_read);
288- if (self->read) {
289- ret = decode(self, DECODE_NORMAL);
290- Py_DECREF(self->read);
291+ Py_INCREF(save_read); // Keep alive while we use a different read method
292+ save_read_pos = self->read_pos;
293+ save_read_len = self->read_len;
294+
295+ // Save buffer pointer if nested
296+ if (is_nested) {
297+ save_buffer = self->readahead;
298+ self->readahead = NULL; // Prevent setter from freeing saved buffer
299+ }
300+
301+ // Set up BytesIO decoder - setter handles buffer allocation
302+ if (_CBORDecoder_set_fp_with_read_size(self, buf, self->readahead_size) == -1) {
303+ if (is_nested) {
304+ PyMem_Free(self->readahead);
305+ self->readahead = save_buffer;
306 }
307+ Py_DECREF(save_read);
308 Py_DECREF(buf);
309+ self->decode_depth--;
310+ return NULL;
311 }
312- self->read = save_read;
313+
314+ ret = decode(self, DECODE_NORMAL);
315+
316+ Py_XDECREF(self->read); // Decrement BytesIO read method
317+ self->read = save_read; // Restore saved read (already has correct refcount)
318+ Py_DECREF(buf);
319 self->decode_depth--;
320+
321+ if (is_nested) {
322+ PyMem_Free(self->readahead);
323+ self->readahead = save_buffer;
324+ }
325+ self->read_pos = save_read_pos;
326+ self->read_len = save_read_len;
327+
328 assert(self->decode_depth >= 0);
329 if (self->decode_depth == 0) {
330 clear_shareable_state(self);
331@@ -2257,6 +2386,14 @@ PyDoc_STRVAR(CBORDecoder__doc__,
332 " dictionary. This callback is invoked for each deserialized\n"
333 " :class:`dict` object. The return value is substituted for the dict\n"
334 " in the deserialized output.\n"
335+":param read_size:\n"
336+" the size of the read buffer (default 4096). The decoder reads from\n"
337+" the stream in chunks of this size for performance. This means the\n"
338+" stream position may advance beyond the bytes actually decoded. For\n"
339+" large values (bytestrings, text strings), reads may be larger than\n"
340+" ``read_size``. Code that needs to read from the stream after\n"
341+" decoding should use :meth:`decode_from_bytes` instead, or set\n"
342+" ``read_size=1`` to disable buffering (at a performance cost).\n"
343 "\n"
344 ".. _CBOR: https://cbor.io/\n"
345 );
346diff --git a/source/decoder.h b/source/decoder.h
347index a2f1bcb..a2f4bf1 100644
348--- a/source/decoder.h
349+++ b/source/decoder.h
350@@ -3,6 +3,9 @@
351 #include <stdbool.h>
352 #include <stdint.h>
353
354+// Default readahead buffer size for streaming reads
355+#define CBOR2_DEFAULT_READ_SIZE 4096
356+
357 typedef struct {
358 PyObject_HEAD
359 PyObject *read; // cached read() method of fp
360@@ -14,6 +17,12 @@ typedef struct {
361 bool immutable;
362 Py_ssize_t shared_index;
363 Py_ssize_t decode_depth;
364+
365+ // Readahead buffer for streaming
366+ char *readahead; // allocated buffer
367+ Py_ssize_t readahead_size; // size of allocated buffer
368+ Py_ssize_t read_pos; // current position in buffer
369+ Py_ssize_t read_len; // valid bytes in buffer
370 } CBORDecoderObject;
371
372 extern PyTypeObject CBORDecoderType;
373diff --git a/tests/test_decoder.py b/tests/test_decoder.py
374index 3b4455a..9bf5a10 100644
375--- a/tests/test_decoder.py
376+++ b/tests/test_decoder.py
377@@ -1043,4 +1043,87 @@ class TestDecoderReuse:
378
379 result = impl.loads(data)
380 assert result == ["hello", "hello"]
381- assert result[0] is result[1] # Same object reference
382\ No newline at end of file
383+ assert result[0] is result[1] # Same object reference
384+
385+
386+def test_decode_from_bytes_in_hook_preserves_buffer(impl):
387+ """Test that calling decode_from_bytes from a hook preserves stream buffer state.
388+ This is a documented use case from docs/customizing.rst where hooks decode
389+ embedded CBOR data. Before the fix, the stream's readahead buffer would be
390+ corrupted, causing subsequent reads to fail or return wrong data.
391+ """
392+
393+ def tag_hook(decoder, tag):
394+ if tag.tag == 999:
395+ # Decode embedded CBOR (documented pattern)
396+ return decoder.decode_from_bytes(tag.value)
397+ return tag
398+
399+ # Test data: array with [tag(999, embedded_cbor), "after_hook", "final"]
400+ # embedded_cbor encodes: [1, 2, 3]
401+ data = unhexlify(
402+ "83" # array(3)
403+ "d903e7" # tag(999)
404+ "44" # bytes(4)
405+ "83010203" # embedded: array [1, 2, 3]
406+ "6a" # text(10)
407+ "61667465725f686f6f6b" # "after_hook"
408+ "65" # text(5)
409+ "66696e616c" # "final"
410+ )
411+
412+ # Decode from stream (not bytes) to use readahead buffer
413+ stream = BytesIO(data)
414+ decoder = impl.CBORDecoder(stream, tag_hook=tag_hook)
415+ result = decoder.decode()
416+
417+ # Verify all values decoded correctly
418+ assert result == [[1, 2, 3], "after_hook", "final"]
419+
420+ # First element should be the decoded embedded CBOR
421+ assert result[0] == [1, 2, 3]
422+ # Second element should be "after_hook" (not corrupted)
423+ assert result[1] == "after_hook"
424+ # Third element should be "final"
425+ assert result[2] == "final"
426+
427+
428+def test_decode_from_bytes_deeply_nested_in_hook(impl):
429+ """Test deeply nested decode_from_bytes calls preserve buffer state.
430+ This tests tag(999, tag(888, tag(777, [1,2,3]))) where each tag value
431+ is embedded CBOR that triggers the hook recursively.
432+ Before the fix, even a single level would corrupt the buffer. With multiple
433+ levels, the buffer would be completely corrupted, mixing data from different
434+ BytesIO objects and the original stream.
435+ """
436+
437+ def tag_hook(decoder, tag):
438+ if tag.tag in [999, 888, 777]:
439+ # Recursively decode embedded CBOR
440+ return decoder.decode_from_bytes(tag.value)
441+ return tag
442+
443+ # Test data: [tag(999, tag(888, tag(777, [1,2,3]))), "after", "final"]
444+ # Each tag contains embedded CBOR
445+ data = unhexlify(
446+ "83" # array(3)
447+ "d903e7" # tag(999)
448+ "4c" # bytes(12)
449+ "d9037848d903094483010203" # embedded: tag(888, tag(777, [1,2,3]))
450+ "65" # text(5)
451+ "6166746572" # "after"
452+ "65" # text(5)
453+ "66696e616c" # "final"
454+ )
455+
456+ # Decode from stream to use readahead buffer
457+ stream = BytesIO(data)
458+ decoder = impl.CBORDecoder(stream, tag_hook=tag_hook)
459+ result = decoder.decode()
460+
461+ # With the fix: all three levels of nesting work correctly
462+ # Without the fix: buffer corruption at each level, test fails
463+ assert result == [[1, 2, 3], "after", "final"]
464+ assert result[0] == [1, 2, 3]
465+ assert result[1] == "after"
466+ assert result[2] == "final"
467--
4682.50.1
469
diff --git a/meta-python/recipes-devtools/python/python3-cbor2/CVE-2026-26209.patch b/meta-python/recipes-devtools/python/python3-cbor2/CVE-2026-26209.patch
new file mode 100644
index 0000000000..1a9c5a3995
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-cbor2/CVE-2026-26209.patch
@@ -0,0 +1,415 @@
1From e61a5f365ba610d5907a0ae1bc72769bba34294b Mon Sep 17 00:00:00 2001
2From: Andreas Eriksen <andreer@vespa.ai>
3Date: Sat, 28 Feb 2026 22:21:06 +0100
4Subject: [PATCH] Set default read_size to 1 for backwards compatibility (#275)
5MIME-Version: 1.0
6Content-Type: text/plain; charset=UTF-8
7Content-Transfer-Encoding: 8bit
8
9The buffered reads introduced in 5.8.0 could cause issues when code needs to access the stream position after decoding. This changes the default back to 1 (matching 5.7.1 behavior) while allowing users to opt-in to faster decoding by passing read_size=4096.
10
11Implementation details:
12- Use function pointer dispatch to eliminate runtime checks for read_size=1
13- Skip buffer allocation entirely for unbuffered path
14- Add read_size parameter to load() and loads() for API completeness
15
16CVE: CVE-2026-26209
17Upstream-Status: Backport [https://github.com/agronholm/cbor2/commit/e61a5f365ba610d5907a0ae1bc72769bba34294b]
18Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
19---
20 cbor2/_decoder.py | 33 ++++++++++++++++--
21 docs/usage.rst | 11 ++++++
22 source/decoder.c | 78 +++++++++++++++++++++++++++++--------------
23 source/decoder.h | 16 +++++++--
24 tests/test_decoder.py | 15 +++++++++
25 5 files changed, 123 insertions(+), 30 deletions(-)
26
27diff --git a/cbor2/_decoder.py b/cbor2/_decoder.py
28index 4aeadcf..5a1f65b 100644
29--- a/cbor2/_decoder.py
30+++ b/cbor2/_decoder.py
31@@ -72,6 +72,7 @@ class CBORDecoder:
32 tag_hook: Callable[[CBORDecoder, CBORTag], Any] | None = None,
33 object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
34 str_errors: Literal["strict", "error", "replace"] = "strict",
35+ read_size: int = 1,
36 ):
37 """
38 :param fp:
39@@ -90,6 +91,13 @@ class CBORDecoder:
40 :param str_errors:
41 determines how to handle unicode decoding errors (see the `Error Handlers`_
42 section in the standard library documentation for details)
43+ :param read_size:
44+ the minimum number of bytes to read at a time.
45+ Setting this to a higher value like 4096 improves performance,
46+ but is likely to read past the end of the CBOR value, advancing the stream
47+ position beyond the decoded data. This only matters if you need to reuse the
48+ stream after decoding.
49+ Ignored in the pure Python implementation, but included for API compatibility.
50
51 .. _Error Handlers: https://docs.python.org/3/library/codecs.html#error-handlers
52
53@@ -813,6 +821,7 @@ def loads(
54 tag_hook: Callable[[CBORDecoder, CBORTag], Any] | None = None,
55 object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
56 str_errors: Literal["strict", "error", "replace"] = "strict",
57+ read_size: int = 1,
58 ) -> Any:
59 """
60 Deserialize an object from a bytestring.
61@@ -831,6 +840,10 @@ def loads(
62 :param str_errors:
63 determines how to handle unicode decoding errors (see the `Error Handlers`_
64 section in the standard library documentation for details)
65+ :param read_size:
66+ the minimum number of bytes to read at a time.
67+ Setting this to a higher value like 4096 improves performance.
68+ Ignored in the pure Python implementation, but included for API compatibility.
69 :return:
70 the deserialized object
71
72@@ -839,7 +852,11 @@ def loads(
73 """
74 with BytesIO(s) as fp:
75 return CBORDecoder(
76- fp, tag_hook=tag_hook, object_hook=object_hook, str_errors=str_errors
77+ fp,
78+ tag_hook=tag_hook,
79+ object_hook=object_hook,
80+ str_errors=str_errors,
81+ read_size=read_size,
82 ).decode()
83
84
85@@ -848,6 +865,7 @@ def load(
86 tag_hook: Callable[[CBORDecoder, CBORTag], Any] | None = None,
87 object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
88 str_errors: Literal["strict", "error", "replace"] = "strict",
89+ read_size: int = 1,
90 ) -> Any:
91 """
92 Deserialize an object from an open file.
93@@ -866,6 +884,13 @@ def load(
94 :param str_errors:
95 determines how to handle unicode decoding errors (see the `Error Handlers`_
96 section in the standard library documentation for details)
97+ :param read_size:
98+ the minimum number of bytes to read at a time.
99+ Setting this to a higher value like 4096 improves performance,
100+ but is likely to read past the end of the CBOR value, advancing the stream
101+ position beyond the decoded data. This only matters if you need to reuse the
102+ stream after decoding.
103+ Ignored in the pure Python implementation, but included for API compatibility.
104 :return:
105 the deserialized object
106
107@@ -873,5 +898,9 @@ def load(
108
109 """
110 return CBORDecoder(
111- fp, tag_hook=tag_hook, object_hook=object_hook, str_errors=str_errors
112+ fp,
113+ tag_hook=tag_hook,
114+ object_hook=object_hook,
115+ str_errors=str_errors,
116+ read_size=read_size,
117 ).decode()
118diff --git a/docs/usage.rst b/docs/usage.rst
119index 797db59..6f53174 100644
120--- a/docs/usage.rst
121+++ b/docs/usage.rst
122@@ -74,6 +74,17 @@ instead encodes a reference to the nth sufficiently long string already encoded.
123 .. warning:: Support for string referencing is rare in other CBOR implementations, so think carefully
124 whether you want to enable it.
125
126+Performance tuning
127+------------------
128+
129+By default, the decoder only reads the exact amount of bytes it needs. But this can negatively
130+impact the performance due to the potentially large number of individual read operations.
131+To make it faster, you can pass a different ``read_size`` parameter (say, 4096), to :func:`load`,
132+:func:`loads` or :class:`CBORDecoder`.
133+
134+.. warning:: If the input stream contains data other than the CBOR stream, that data (or parts of)
135+ may be lost.
136+
137 Tag support
138 -----------
139
140diff --git a/source/decoder.c b/source/decoder.c
141index 9cd1596..f8adc93 100644
142--- a/source/decoder.c
143+++ b/source/decoder.c
144@@ -47,6 +47,10 @@ static int _CBORDecoder_set_tag_hook(CBORDecoderObject *, PyObject *, void *);
145 static int _CBORDecoder_set_object_hook(CBORDecoderObject *, PyObject *, void *);
146 static int _CBORDecoder_set_str_errors(CBORDecoderObject *, PyObject *, void *);
147
148+// Forward declarations for read dispatch functions
149+static int fp_read_unbuffered(CBORDecoderObject *, char *, Py_ssize_t);
150+static int fp_read_buffered(CBORDecoderObject *, char *, Py_ssize_t);
151+
152 static PyObject * decode(CBORDecoderObject *, DecodeOptions);
153 static PyObject * decode_bytestring(CBORDecoderObject *, uint8_t);
154 static PyObject * decode_string(CBORDecoderObject *, uint8_t);
155@@ -155,6 +159,7 @@ CBORDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
156 self->readahead_size = 0;
157 self->read_pos = 0;
158 self->read_len = 0;
159+ self->fp_read = fp_read_unbuffered; // default, will be set properly in init
160 }
161 return (PyObject *) self;
162 error:
163@@ -164,7 +169,7 @@ error:
164
165
166 // CBORDecoder.__init__(self, fp=None, tag_hook=None, object_hook=None,
167-// str_errors='strict', read_size=4096)
168+// str_errors='strict', read_size=1)
169 int
170 CBORDecoder_init(CBORDecoderObject *self, PyObject *args, PyObject *kwargs)
171 {
172@@ -233,7 +238,8 @@ _CBORDecoder_set_fp_with_read_size(CBORDecoderObject *self, PyObject *value, Py_
173 return -1;
174 }
175
176- if (self->readahead == NULL || self->readahead_size != read_size) {
177+ // Skip buffer allocation for read_size=1 (direct read path doesn't use buffer)
178+ if (read_size > 1 && (self->readahead == NULL || self->readahead_size != read_size)) {
179 new_buffer = (char *)PyMem_Malloc(read_size);
180 if (!new_buffer) {
181 Py_DECREF(read);
182@@ -254,8 +260,15 @@ _CBORDecoder_set_fp_with_read_size(CBORDecoderObject *self, PyObject *value, Py_
183 if (new_buffer) {
184 PyMem_Free(self->readahead);
185 self->readahead = new_buffer;
186- self->readahead_size = read_size;
187+ } else if (read_size == 1 && self->readahead != NULL) {
188+ // Free existing buffer when switching to direct read path (read_size=1)
189+ PyMem_Free(self->readahead);
190+ self->readahead = NULL;
191 }
192+ self->readahead_size = read_size;
193+
194+ // Set read dispatch function - eliminates runtime check on every read
195+ self->fp_read = (read_size == 1) ? fp_read_unbuffered : fp_read_buffered;
196
197 return 0;
198 }
199@@ -447,9 +460,25 @@ fp_read_bytes(CBORDecoderObject *self, char *buf, Py_ssize_t size)
200 return bytes_read;
201 }
202
203-// Read into caller's buffer using the readahead buffer
204+// Unbuffered read - used when read_size=1 (backwards compatible mode)
205+// This matches the 5.7.1 behavior with no runtime overhead
206+static int
207+fp_read_unbuffered(CBORDecoderObject *self, char *buf, Py_ssize_t size)
208+{
209+ Py_ssize_t bytes_read = fp_read_bytes(self, buf, size);
210+ if (bytes_read == size)
211+ return 0;
212+ if (bytes_read >= 0)
213+ PyErr_Format(
214+ _CBOR2_CBORDecodeEOF,
215+ "premature end of stream (expected to read %zd bytes, "
216+ "got %zd instead)", size, bytes_read);
217+ return -1;
218+}
219+
220+// Buffered read - used when read_size > 1 for improved performance
221 static int
222-fp_read(CBORDecoderObject *self, char *buf, const Py_ssize_t size)
223+fp_read_buffered(CBORDecoderObject *self, char *buf, Py_ssize_t size)
224 {
225 Py_ssize_t available, to_copy, remaining, total_copied;
226
227@@ -507,7 +536,7 @@ fp_read_object(CBORDecoderObject *self, const Py_ssize_t size)
228 if (!ret)
229 return NULL;
230
231- if (fp_read(self, PyBytes_AS_STRING(ret), size) == -1) {
232+ if (self->fp_read(self, PyBytes_AS_STRING(ret), size) == -1) {
233 Py_DECREF(ret);
234 return NULL;
235 }
236@@ -528,7 +557,7 @@ CBORDecoder_read(CBORDecoderObject *self, PyObject *length)
237 return NULL;
238 ret = PyBytes_FromStringAndSize(NULL, len);
239 if (ret) {
240- if (fp_read(self, PyBytes_AS_STRING(ret), len) == -1) {
241+ if (self->fp_read(self, PyBytes_AS_STRING(ret), len) == -1) {
242 Py_DECREF(ret);
243 ret = NULL;
244 }
245@@ -576,19 +605,19 @@ decode_length(CBORDecoderObject *self, uint8_t subtype,
246 if (subtype < 24) {
247 *length = subtype;
248 } else if (subtype == 24) {
249- if (fp_read(self, value.u8.buf, sizeof(uint8_t)) == -1)
250+ if (self->fp_read(self, value.u8.buf, sizeof(uint8_t)) == -1)
251 return -1;
252 *length = value.u8.value;
253 } else if (subtype == 25) {
254- if (fp_read(self, value.u16.buf, sizeof(uint16_t)) == -1)
255+ if (self->fp_read(self, value.u16.buf, sizeof(uint16_t)) == -1)
256 return -1;
257 *length = be16toh(value.u16.value);
258 } else if (subtype == 26) {
259- if (fp_read(self, value.u32.buf, sizeof(uint32_t)) == -1)
260+ if (self->fp_read(self, value.u32.buf, sizeof(uint32_t)) == -1)
261 return -1;
262 *length = be32toh(value.u32.value);
263 } else {
264- if (fp_read(self, value.u64.buf, sizeof(uint64_t)) == -1)
265+ if (self->fp_read(self, value.u64.buf, sizeof(uint64_t)) == -1)
266 return -1;
267 *length = be64toh(value.u64.value);
268 }
269@@ -752,7 +781,7 @@ decode_indefinite_bytestrings(CBORDecoderObject *self)
270 list = PyList_New(0);
271 if (list) {
272 while (1) {
273- if (fp_read(self, &lead.byte, 1) == -1)
274+ if (self->fp_read(self, &lead.byte, 1) == -1)
275 break;
276 if (lead.major == 2 && lead.subtype != 31) {
277 ret = decode_bytestring(self, lead.subtype);
278@@ -959,7 +988,7 @@ decode_indefinite_strings(CBORDecoderObject *self)
279 list = PyList_New(0);
280 if (list) {
281 while (1) {
282- if (fp_read(self, &lead.byte, 1) == -1)
283+ if (self->fp_read(self, &lead.byte, 1) == -1)
284 break;
285 if (lead.major == 3 && lead.subtype != 31) {
286 ret = decode_string(self, lead.subtype);
287@@ -2040,7 +2069,7 @@ CBORDecoder_decode_simple_value(CBORDecoderObject *self)
288 PyObject *tag, *ret = NULL;
289 uint8_t buf;
290
291- if (fp_read(self, (char*)&buf, sizeof(uint8_t)) == 0) {
292+ if (self->fp_read(self, (char*)&buf, sizeof(uint8_t)) == 0) {
293 tag = PyStructSequence_New(&CBORSimpleValueType);
294 if (tag) {
295 PyStructSequence_SET_ITEM(tag, 0, PyLong_FromLong(buf));
296@@ -2066,7 +2095,7 @@ CBORDecoder_decode_float16(CBORDecoderObject *self)
297 char buf[sizeof(uint16_t)];
298 } u;
299
300- if (fp_read(self, u.buf, sizeof(uint16_t)) == 0)
301+ if (self->fp_read(self, u.buf, sizeof(uint16_t)) == 0)
302 ret = PyFloat_FromDouble(unpack_float16(u.i));
303 set_shareable(self, ret);
304 return ret;
305@@ -2084,7 +2113,7 @@ CBORDecoder_decode_float32(CBORDecoderObject *self)
306 char buf[sizeof(float)];
307 } u;
308
309- if (fp_read(self, u.buf, sizeof(float)) == 0) {
310+ if (self->fp_read(self, u.buf, sizeof(float)) == 0) {
311 u.i = be32toh(u.i);
312 ret = PyFloat_FromDouble(u.f);
313 }
314@@ -2104,7 +2133,7 @@ CBORDecoder_decode_float64(CBORDecoderObject *self)
315 char buf[sizeof(double)];
316 } u;
317
318- if (fp_read(self, u.buf, sizeof(double)) == 0) {
319+ if (self->fp_read(self, u.buf, sizeof(double)) == 0) {
320 u.i = be64toh(u.i);
321 ret = PyFloat_FromDouble(u.f);
322 }
323@@ -2133,7 +2162,7 @@ decode(CBORDecoderObject *self, DecodeOptions options)
324 if (Py_EnterRecursiveCall(" in CBORDecoder.decode"))
325 return NULL;
326
327- if (fp_read(self, &lead.byte, 1) == 0) {
328+ if (self->fp_read(self, &lead.byte, 1) == 0) {
329 switch (lead.major) {
330 case 0: ret = decode_uint(self, lead.subtype); break;
331 case 1: ret = decode_negint(self, lead.subtype); break;
332@@ -2387,13 +2416,12 @@ PyDoc_STRVAR(CBORDecoder__doc__,
333 " :class:`dict` object. The return value is substituted for the dict\n"
334 " in the deserialized output.\n"
335 ":param read_size:\n"
336-" the size of the read buffer (default 4096). The decoder reads from\n"
337-" the stream in chunks of this size for performance. This means the\n"
338-" stream position may advance beyond the bytes actually decoded. For\n"
339-" large values (bytestrings, text strings), reads may be larger than\n"
340-" ``read_size``. Code that needs to read from the stream after\n"
341-" decoding should use :meth:`decode_from_bytes` instead, or set\n"
342-" ``read_size=1`` to disable buffering (at a performance cost).\n"
343+" the minimum number of bytes to read at a time.\n"
344+" Setting this to a higher value like 4096 improves performance,\n"
345+" but is likely to read past the end of the CBOR value, advancing the stream\n"
346+" position beyond the decoded data. This only matters if you need to reuse the\n"
347+" stream after decoding.\n"
348+" Ignored in the pure Python implementation, but included for API compatibility.\n"
349 "\n"
350 ".. _CBOR: https://cbor.io/\n"
351 );
352diff --git a/source/decoder.h b/source/decoder.h
353index a2f4bf1..3efff8b 100644
354--- a/source/decoder.h
355+++ b/source/decoder.h
356@@ -3,10 +3,17 @@
357 #include <stdbool.h>
358 #include <stdint.h>
359
360-// Default readahead buffer size for streaming reads
361-#define CBOR2_DEFAULT_READ_SIZE 4096
362+// Default readahead buffer size for streaming reads.
363+// Set to 1 for backwards compatibility (no buffering).
364+#define CBOR2_DEFAULT_READ_SIZE 1
365
366-typedef struct {
367+// Forward declaration for function pointer typedef
368+struct CBORDecoderObject_;
369+
370+// Function pointer type for read dispatch (eliminates runtime check)
371+typedef int (*fp_read_fn)(struct CBORDecoderObject_ *, char *, Py_ssize_t);
372+
373+typedef struct CBORDecoderObject_ {
374 PyObject_HEAD
375 PyObject *read; // cached read() method of fp
376 PyObject *tag_hook;
377@@ -23,6 +30,9 @@ typedef struct {
378 Py_ssize_t readahead_size; // size of allocated buffer
379 Py_ssize_t read_pos; // current position in buffer
380 Py_ssize_t read_len; // valid bytes in buffer
381+
382+ // Read dispatch - points to unbuffered or buffered implementation
383+ fp_read_fn fp_read;
384 } CBORDecoderObject;
385
386 extern PyTypeObject CBORDecoderType;
387diff --git a/tests/test_decoder.py b/tests/test_decoder.py
388index 9bf5a10..c5d1a9c 100644
389--- a/tests/test_decoder.py
390+++ b/tests/test_decoder.py
391@@ -123,6 +123,21 @@ def test_load(impl):
392 assert impl.load(fp=stream) == 1
393
394
395+def test_stream_position_after_decode(impl):
396+ """Test that stream position is exactly at end of decoded CBOR value."""
397+ # CBOR: integer 1 (1 byte: 0x01) followed by extra data
398+ cbor_data = b"\x01"
399+ extra_data = b"extra"
400+ with BytesIO(cbor_data + extra_data) as stream:
401+ decoder = impl.CBORDecoder(stream)
402+ result = decoder.decode()
403+ assert result == 1
404+ # Stream position should be exactly at end of CBOR data
405+ assert stream.tell() == len(cbor_data)
406+ # Should be able to read the extra data
407+ assert stream.read() == extra_data
408+
409+
410 @pytest.mark.parametrize(
411 "payload, expected",
412 [
413--
4142.50.1
415
diff --git a/meta-python/recipes-devtools/python/python3-cbor2_5.6.4.bb b/meta-python/recipes-devtools/python/python3-cbor2_5.6.4.bb
index 69e5daba2a..90688ced20 100644
--- a/meta-python/recipes-devtools/python/python3-cbor2_5.6.4.bb
+++ b/meta-python/recipes-devtools/python/python3-cbor2_5.6.4.bb
@@ -14,6 +14,8 @@ SRC_URI += " \
14 file://run-ptest \ 14 file://run-ptest \
15 file://CVE-2025-64076.patch \ 15 file://CVE-2025-64076.patch \
16 file://CVE-2025-68131.patch \ 16 file://CVE-2025-68131.patch \
17 file://CVE-2026-26209-pre1.patch \
18 file://CVE-2026-26209.patch \
17" 19"
18 20
19RDEPENDS:${PN}-ptest += " \ 21RDEPENDS:${PN}-ptest += " \