mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-09-09 06:40:17 +00:00
Scarthgap already contains 4810cd8c5b [3], which backports
fb4ee161 and e61a5f36 for read-ahead/read-size handling. The
associated submission [4] labels that work as CVE-2026-26209, but
it does not add the max_depth protection required for uncontrolled
recursion [2].
Complete the existing backport with the 5.9.0 max-depth chain:
- bcb6cea4: add the C decoder depth limit [1]
- 94e0d212: add the security-essential pure-Python depth limit
- 53521e7c: apply the required type correction
- a7ac10d5: raise the default depth limit to 400
- d903d62c: synchronize the C function signature default
The 5.9.0 upgrade description [5] also identifies max_depth as the
CVE fix. Full upstream commit links are recorded in the embedded
patch headers.
[1] https://github.com/agronholm/cbor2/commit/bcb6cea4edde1d00ff4f0eece883dea951f66e1b
[2] https://github.com/advisories/GHSA-3c37-wwvx-h642
[3] https://git.openembedded.org/meta-openembedded/commit/?id=4810cd8c5bbc0b4349a78eac85a6a882bc0b03a2
[4] https://www.mail-archive.com/openembedded-devel%40lists.openembedded.org/msg105607.html
[5] https://www.mail-archive.com/openembedded-devel%40lists.openembedded.org/msg105418.html
Signed-off-by: Devansh Patel <devanshp@cisco.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
436 lines
15 KiB
Diff
436 lines
15 KiB
Diff
From abb6fef29ad304fb95e08ff151d52548543816b5 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= <alex.gronholm@nextday.fi>
|
|
Date: Tue, 3 Mar 2026 01:11:34 +0200
|
|
Subject: [PATCH] Added missing Python counterpart for max_depth
|
|
|
|
CVE: CVE-2026-26209
|
|
Upstream-Status: Backport [https://github.com/agronholm/cbor2/commit/94e0d2125fbfb183606afa9ef07754a8dba50748]
|
|
|
|
Backport Changes:
|
|
- Omitted docs/versionhistory.rst after it failed to cherry-pick because
|
|
Scarthgap 5.6.4 lacks the later release sections.
|
|
- Preserved the existing CVE-2025-68131 top-level shared-state cleanup
|
|
while routing recursive decoding through the depth-checked path.
|
|
- Omitted decode_complex() because CBOR tag 43000 is absent from 5.6.4.
|
|
- Omitted the upstream Generator import and _decoding_context() return
|
|
annotation removal because Scarthgap's CVE-2025-68131 base never
|
|
added that import or annotation.
|
|
|
|
(cherry picked from commit 94e0d2125fbfb183606afa9ef07754a8dba50748)
|
|
Signed-off-by: Devansh Patel <devanshp@cisco.com>
|
|
---
|
|
cbor2/_decoder.py | 116 ++++++++++++++++++++++--------------------
|
|
source/decoder.h | 2 +-
|
|
tests/test_decoder.py | 4 +-
|
|
3 files changed, 63 insertions(+), 59 deletions(-)
|
|
|
|
diff --git a/cbor2/_decoder.py b/cbor2/_decoder.py
|
|
index 5a1f65b..024c403 100644
|
|
--- a/cbor2/_decoder.py
|
|
+++ b/cbor2/_decoder.py
|
|
@@ -5,13 +5,13 @@ import struct
|
|
import sys
|
|
from codecs import getincrementaldecoder
|
|
from collections.abc import Callable, Mapping, Sequence
|
|
-from contextlib import contextmanager
|
|
from datetime import date, datetime, timedelta, timezone
|
|
from io import BytesIO
|
|
from typing import IO, TYPE_CHECKING, Any, TypeVar, cast, overload
|
|
|
|
from ._types import (
|
|
CBORDecodeEOF,
|
|
+ CBORDecodeError,
|
|
CBORDecodeValueError,
|
|
CBORSimpleValue,
|
|
CBORTag,
|
|
@@ -60,6 +60,7 @@ class CBORDecoder:
|
|
"_immutable",
|
|
"_str_errors",
|
|
"_stringref_namespace",
|
|
+ "_max_depth",
|
|
"_decode_depth",
|
|
)
|
|
|
|
@@ -73,6 +74,8 @@ class CBORDecoder:
|
|
object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
|
|
str_errors: Literal["strict", "error", "replace"] = "strict",
|
|
read_size: int = 1,
|
|
+ *,
|
|
+ max_depth: int = 100,
|
|
):
|
|
"""
|
|
:param fp:
|
|
@@ -98,6 +101,8 @@ class CBORDecoder:
|
|
position beyond the decoded data. This only matters if you need to reuse the
|
|
stream after decoding.
|
|
Ignored in the pure Python implementation, but included for API compatibility.
|
|
+ :param max_depth:
|
|
+ the maximum allowed container nesting depth
|
|
|
|
.. _Error Handlers: https://docs.python.org/3/library/codecs.html#error-handlers
|
|
|
|
@@ -110,6 +115,7 @@ class CBORDecoder:
|
|
self._shareables: list[object] = []
|
|
self._stringref_namespace: list[str | bytes] | None = None
|
|
self._immutable = False
|
|
+ self._max_depth = max_depth
|
|
self._decode_depth = 0
|
|
|
|
@property
|
|
@@ -217,13 +223,24 @@ class CBORDecoder:
|
|
|
|
return data
|
|
|
|
- def _decode(self, immutable: bool = False, unshared: bool = False) -> Any:
|
|
+ def decode(self, immutable: bool = False, unshared: bool = False) -> Any:
|
|
+ """
|
|
+ Decode the next value from the stream.
|
|
+
|
|
+ :raises CBORDecodeError: if there is any problem decoding the stream
|
|
+
|
|
+ """
|
|
+ if self._decode_depth > self._max_depth:
|
|
+ raise CBORDecodeError(f"maximum container nesting depth ({self._max_depth}) exceeded")
|
|
+
|
|
if immutable:
|
|
old_immutable = self._immutable
|
|
self._immutable = True
|
|
if unshared:
|
|
old_index = self._share_index
|
|
self._share_index = None
|
|
+
|
|
+ self._decode_depth += 1
|
|
try:
|
|
initial_byte = self.read(1)[0]
|
|
major_type = initial_byte >> 5
|
|
@@ -236,34 +253,12 @@ class CBORDecoder:
|
|
if unshared:
|
|
self._share_index = old_index
|
|
|
|
- @contextmanager
|
|
- def _decoding_context(self):
|
|
- """
|
|
- Context manager for tracking decode depth and clearing shared state.
|
|
-
|
|
- Shared state is cleared at the end of each top-level decode to prevent
|
|
- shared references from leaking between independent decode operations.
|
|
- Nested calls (from hooks) must preserve the state.
|
|
- """
|
|
- self._decode_depth += 1
|
|
- try:
|
|
- yield
|
|
- finally:
|
|
self._decode_depth -= 1
|
|
assert self._decode_depth >= 0
|
|
if self._decode_depth == 0:
|
|
self._shareables.clear()
|
|
self._share_index = None
|
|
|
|
- def decode(self) -> object:
|
|
- """
|
|
- Decode the next value from the stream.
|
|
-
|
|
- :raises CBORDecodeError: if there is any problem decoding the stream
|
|
- """
|
|
- with self._decoding_context():
|
|
- return self._decode()
|
|
-
|
|
def decode_from_bytes(self, buf: bytes) -> object:
|
|
"""
|
|
Wrap the given bytestring as a file and call :meth:`decode` with it as
|
|
@@ -273,13 +268,12 @@ class CBORDecoder:
|
|
object needs to be decoded separately from the rest but while still
|
|
taking advantage of the shared value registry.
|
|
"""
|
|
- with self._decoding_context():
|
|
- with BytesIO(buf) as fp:
|
|
- old_fp = self.fp
|
|
- self.fp = fp
|
|
- retval = self._decode()
|
|
- self.fp = old_fp
|
|
- return retval
|
|
+ with BytesIO(buf) as fp:
|
|
+ old_fp = self.fp
|
|
+ self.fp = fp
|
|
+ retval = self.decode()
|
|
+ self.fp = old_fp
|
|
+ return retval
|
|
|
|
@overload
|
|
def _decode_length(self, subtype: int) -> int: ...
|
|
@@ -430,7 +424,7 @@ class CBORDecoder:
|
|
if not self._immutable:
|
|
self.set_shareable(items)
|
|
while True:
|
|
- value = self._decode()
|
|
+ value = self.decode(unshared=True)
|
|
if value is break_marker:
|
|
break
|
|
else:
|
|
@@ -444,7 +438,7 @@ class CBORDecoder:
|
|
self.set_shareable(items)
|
|
|
|
for index in range(length):
|
|
- items.append(self._decode())
|
|
+ items.append(self.decode(unshared=True))
|
|
|
|
if self._immutable:
|
|
items_tuple = tuple(items)
|
|
@@ -461,17 +455,17 @@ class CBORDecoder:
|
|
dictionary: dict[Any, Any] = {}
|
|
self.set_shareable(dictionary)
|
|
while True:
|
|
- key = self._decode(immutable=True, unshared=True)
|
|
+ key = self.decode(immutable=True, unshared=True)
|
|
if key is break_marker:
|
|
break
|
|
else:
|
|
- dictionary[key] = self._decode(unshared=True)
|
|
+ dictionary[key] = self.decode(unshared=True)
|
|
else:
|
|
dictionary = {}
|
|
self.set_shareable(dictionary)
|
|
for _ in range(length):
|
|
- key = self._decode(immutable=True, unshared=True)
|
|
- dictionary[key] = self._decode(unshared=True)
|
|
+ key = self.decode(immutable=True, unshared=True)
|
|
+ dictionary[key] = self.decode(unshared=True)
|
|
|
|
if self._object_hook:
|
|
dictionary = self._object_hook(self, dictionary)
|
|
@@ -491,7 +485,7 @@ class CBORDecoder:
|
|
|
|
tag = CBORTag(tagnum, None)
|
|
self.set_shareable(tag)
|
|
- tag.value = self._decode(unshared=True)
|
|
+ tag.value = self.decode(unshared=True)
|
|
if self._tag_hook:
|
|
tag = self._tag_hook(self, tag)
|
|
|
|
@@ -516,17 +510,17 @@ class CBORDecoder:
|
|
#
|
|
def decode_epoch_date(self) -> date:
|
|
# Semantic tag 100
|
|
- value = self._decode()
|
|
+ value = self.decode()
|
|
return self.set_shareable(date.fromordinal(value + 719163))
|
|
|
|
def decode_date_string(self) -> date:
|
|
# Semantic tag 1004
|
|
- value = self._decode()
|
|
+ value = self.decode()
|
|
return self.set_shareable(date.fromisoformat(value))
|
|
|
|
def decode_datetime_string(self) -> datetime:
|
|
# Semantic tag 0
|
|
- value = self._decode()
|
|
+ value = self.decode()
|
|
match = timestamp_re.match(value)
|
|
if match:
|
|
(
|
|
@@ -574,7 +568,7 @@ class CBORDecoder:
|
|
|
|
def decode_epoch_datetime(self) -> datetime:
|
|
# Semantic tag 1
|
|
- value = self._decode()
|
|
+ value = self.decode()
|
|
|
|
try:
|
|
tmp = datetime.fromtimestamp(value, timezone.utc)
|
|
@@ -587,7 +581,7 @@ class CBORDecoder:
|
|
# Semantic tag 2
|
|
from binascii import hexlify
|
|
|
|
- value = self._decode()
|
|
+ value = self.decode()
|
|
if not isinstance(value, bytes):
|
|
raise CBORDecodeValueError("invalid bignum value " + str(value))
|
|
|
|
@@ -602,7 +596,7 @@ class CBORDecoder:
|
|
from decimal import Decimal
|
|
|
|
try:
|
|
- exp, sig = self._decode()
|
|
+ exp, sig = self.decode()
|
|
except (TypeError, ValueError) as e:
|
|
raise CBORDecodeValueError("Incorrect tag 4 payload") from e
|
|
tmp = Decimal(sig).as_tuple()
|
|
@@ -613,7 +607,7 @@ class CBORDecoder:
|
|
from decimal import Decimal
|
|
|
|
try:
|
|
- exp, sig = self._decode()
|
|
+ exp, sig = self.decode()
|
|
except (TypeError, ValueError) as e:
|
|
raise CBORDecodeValueError("Incorrect tag 5 payload") from e
|
|
|
|
@@ -624,7 +618,7 @@ class CBORDecoder:
|
|
if self._stringref_namespace is None:
|
|
raise CBORDecodeValueError("string reference outside of namespace")
|
|
|
|
- index: int = self._decode()
|
|
+ index: int = self.decode()
|
|
try:
|
|
value = self._stringref_namespace[index]
|
|
except IndexError:
|
|
@@ -638,13 +632,13 @@ class CBORDecoder:
|
|
self._share_index = len(self._shareables)
|
|
self._shareables.append(None)
|
|
try:
|
|
- return self._decode()
|
|
+ return self.decode()
|
|
finally:
|
|
self._share_index = old_index
|
|
|
|
def decode_sharedref(self) -> Any:
|
|
# Semantic tag 29
|
|
- value = self._decode(unshared=True)
|
|
+ value = self.decode(unshared=True)
|
|
try:
|
|
shared = self._shareables[value]
|
|
except IndexError:
|
|
@@ -659,7 +653,7 @@ class CBORDecoder:
|
|
# Semantic tag 30
|
|
from fractions import Fraction
|
|
|
|
- inputval = self._decode(immutable=True, unshared=True)
|
|
+ inputval = self.decode(immutable=True, unshared=True)
|
|
try:
|
|
value = Fraction(*inputval)
|
|
except (TypeError, ZeroDivisionError) as exc:
|
|
@@ -675,7 +669,7 @@ class CBORDecoder:
|
|
def decode_regexp(self) -> re.Pattern[str]:
|
|
# Semantic tag 35
|
|
try:
|
|
- value = re.compile(self._decode())
|
|
+ value = re.compile(self.decode())
|
|
except re.error as exc:
|
|
raise CBORDecodeValueError("error decoding regular expression") from exc
|
|
|
|
@@ -686,7 +680,7 @@ class CBORDecoder:
|
|
from email.parser import Parser
|
|
|
|
try:
|
|
- value = Parser().parsestr(self._decode())
|
|
+ value = Parser().parsestr(self.decode())
|
|
except TypeError as exc:
|
|
raise CBORDecodeValueError("error decoding MIME message") from exc
|
|
|
|
@@ -697,7 +691,7 @@ class CBORDecoder:
|
|
from uuid import UUID
|
|
|
|
try:
|
|
- value = UUID(bytes=self._decode())
|
|
+ value = UUID(bytes=self.decode())
|
|
except (TypeError, ValueError) as exc:
|
|
raise CBORDecodeValueError("error decoding UUID value") from exc
|
|
|
|
@@ -707,16 +701,16 @@ class CBORDecoder:
|
|
# Semantic tag 256
|
|
old_namespace = self._stringref_namespace
|
|
self._stringref_namespace = []
|
|
- value = self._decode()
|
|
+ value = self.decode()
|
|
self._stringref_namespace = old_namespace
|
|
return value
|
|
|
|
def decode_set(self) -> set[Any] | frozenset[Any]:
|
|
# Semantic tag 258
|
|
if self._immutable:
|
|
- return self.set_shareable(frozenset(self._decode(immutable=True)))
|
|
+ return self.set_shareable(frozenset(self.decode(immutable=True)))
|
|
else:
|
|
- return self.set_shareable(set(self._decode(immutable=True)))
|
|
+ return self.set_shareable(set(self.decode(immutable=True)))
|
|
|
|
def decode_ipaddress(self) -> IPv4Address | IPv6Address | CBORTag:
|
|
# Semantic tag 260
|
|
@@ -749,7 +743,7 @@ class CBORDecoder:
|
|
|
|
def decode_self_describe_cbor(self) -> Any:
|
|
# Semantic tag 55799
|
|
- return self._decode()
|
|
+ return self.decode()
|
|
|
|
#
|
|
# Special decoders (major tag 7)
|
|
@@ -822,6 +816,8 @@ def loads(
|
|
object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
|
|
str_errors: Literal["strict", "error", "replace"] = "strict",
|
|
read_size: int = 1,
|
|
+ *,
|
|
+ max_depth: int = 100,
|
|
) -> Any:
|
|
"""
|
|
Deserialize an object from a bytestring.
|
|
@@ -844,6 +840,8 @@ def loads(
|
|
the minimum number of bytes to read at a time.
|
|
Setting this to a higher value like 4096 improves performance.
|
|
Ignored in the pure Python implementation, but included for API compatibility.
|
|
+ :param max_depth:
|
|
+ the maximum allowed container nesting depth
|
|
:return:
|
|
the deserialized object
|
|
|
|
@@ -857,6 +855,7 @@ def loads(
|
|
object_hook=object_hook,
|
|
str_errors=str_errors,
|
|
read_size=read_size,
|
|
+ max_depth=max_depth,
|
|
).decode()
|
|
|
|
|
|
@@ -866,6 +865,8 @@ def load(
|
|
object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
|
|
str_errors: Literal["strict", "error", "replace"] = "strict",
|
|
read_size: int = 1,
|
|
+ *,
|
|
+ max_depth: int = 100,
|
|
) -> Any:
|
|
"""
|
|
Deserialize an object from an open file.
|
|
@@ -891,6 +892,8 @@ def load(
|
|
position beyond the decoded data. This only matters if you need to reuse the
|
|
stream after decoding.
|
|
Ignored in the pure Python implementation, but included for API compatibility.
|
|
+ :param max_depth:
|
|
+ the maximum allowed container nesting depth
|
|
:return:
|
|
the deserialized object
|
|
|
|
@@ -903,4 +906,5 @@ def load(
|
|
object_hook=object_hook,
|
|
str_errors=str_errors,
|
|
read_size=read_size,
|
|
+ max_depth=max_depth,
|
|
).decode()
|
|
diff --git a/source/decoder.h b/source/decoder.h
|
|
index 6d465a4..4536a4a 100644
|
|
--- a/source/decoder.h
|
|
+++ b/source/decoder.h
|
|
@@ -6,7 +6,7 @@
|
|
// Default readahead buffer size for streaming reads.
|
|
// Set to 1 for backwards compatibility (no buffering).
|
|
#define CBOR2_DEFAULT_READ_SIZE 1
|
|
-#define CBOR2_DEFAULT_MAX_DEPTH 500
|
|
+#define CBOR2_DEFAULT_MAX_DEPTH 100
|
|
|
|
// Forward declaration for function pointer typedef
|
|
struct CBORDecoderObject_;
|
|
diff --git a/tests/test_decoder.py b/tests/test_decoder.py
|
|
index e0631af..5a90adf 100644
|
|
--- a/tests/test_decoder.py
|
|
+++ b/tests/test_decoder.py
|
|
@@ -142,9 +142,9 @@ class TestMaximumDepth:
|
|
def test_default(self, impl) -> None:
|
|
with pytest.raises(
|
|
impl.CBORDecodeError,
|
|
- match="maximum container nesting depth \\(500\\) exceeded",
|
|
+ match="maximum container nesting depth \\(100\\) exceeded",
|
|
):
|
|
- impl.loads(b"\x81" * 1000 + b"\x80")
|
|
+ impl.loads(b"\x81" * 101 + b"\x80")
|
|
|
|
def test_explicit(self, impl) -> None:
|
|
with pytest.raises(
|