#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-cbor2. Emits `key=value' lines, one per line, in file order; the suite reads them with grep. A python3 start on this arm is tens of seconds and every case below is a few dozen bytes encoded or decoded -- microseconds beside the start -- so taking the start ONCE is the difference between a half-minute suite and a long one. THREE RULES THE VALUES OBEY, ALL ABOUT WHAT SURVIVES THE TRANSPORT. The runner parses its input line by line, so no value may contain a NEWLINE. Every byte string below is reported as lowercase HEX and every decoded value as a single-line repr. The suite splits each line on the FIRST `=', so a VALUE containing `=' is safe and a KEY containing one is not. Every key emitted here is `[a-z0-9_]+'. And no value may depend on the encoding stdout happens to be in. This image's guest runs under the C locale, so every NON-ASCII fixture below is written as a `\\u' escape in the SOURCE and reported as HEX or as a codepoint list -- never printed as text. A suite that printed U+6C34 would red on a stdout coercion rather than on cbor2. WHY THE ENCODE CASES ARE CANONICAL AND THE DECODE CASES ARE NOT. RFC 8949 Appendix A gives one literal encoding per value, and for floats it is the SHORTEST form that round-trips -- 1.0 as the half-precision `f93c00'. cbor2's DEFAULT encoder does not do that: it writes every float as a float64, so plain `dumps(1.0)' is `fb3ff0000000000000' and comparing it against the RFC would red on a difference that is not an error. `canonical=True' selects the minimal-length float and the deterministic map order that Appendix A uses, so the ENCODE cases below run canonical and the RFC literal is the expectation. DECODING has no such freedom: every literal in Appendix A must decode to the value the RFC names, whichever form it is in, so the decode cases feed the RFC bytes verbatim and assert the value. That is the direction that catches a byte-order or length-prefix slip, because a round-trip alone would cancel a symmetric one out. """ import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def guard(key, fn): """Emit fn()'s value, or `: ' if it raised. Nothing in this probe may abort the run: a single unexpected exception would cost every case after it, and the suite would report them as absent rather than as red. """ try: emit(key, fn()) except Exception as exc: emit(key, '%s: %s' % (type(exc).__name__, exc)) try: import cbor2 from cbor2 import (CBORTag, CBORSimpleValue, undefined, FrozenDict, CBORDecodeError, CBORDecodeEOF, CBORDecodeValueError, CBOREncodeError, CBOREncodeValueError, CBOREncodeTypeError) except Exception as exc: # pragma: no cover print('import_error=%s: %s' % (type(exc).__name__, exc)) sys.exit(1) import datetime import math # --- helpers the cases below call; defined here because `guard` invokes its # --- argument immediately, so every name must exist before its case runs. def _import_cbor2_ext(): import _cbor2 # noqa: F401 return 'imported' def _nest(n): v = 0 for _ in range(n): v = [v] return v def _optional_module(module): try: __import__(module) return 'present' except ImportError: return 'absent' def _optional_tag(module, hexstr, want): """`ok' on EITHER correct arm; anything else describes what went wrong. Two answers are correct here and which one this image gives is decided by whether some other package in EMCRAFT_TOOLS happens to pull the sub-package in. So the verdict is normalised: the module is absent and the decode fails on exactly that import, or the module is present and the tag decodes to the value RFC 8949 gives. Anything else -- the module present and the tag decoding wrongly, or absent and the failure being something other than the import -- is a red with its own detail. """ if _optional_module(module) == 'absent': try: dec(hexstr) return 'DECODED WITHOUT %s' % module except ImportError as exc: return 'ok' if (exc.name or module) == module else 'wrong-import:%s' % exc.name except Exception as exc: return 'wrong-exception:%s' % type(exc).__name__ got = str(dec(hexstr)) return 'ok' if got == want else 'wrong:%s' % got def _cycle(): a = [1, 2] a.append(a) return a def _selfref_default(): try: cbor2.dumps(_cycle()) return 'NO ERROR' except Exception as exc: return '%s: %s' % (type(exc).__name__, exc) def _selfref_shared_enc(): return h(cbor2.dumps(_cycle(), value_sharing=True)) def _selfref_cycle1_rt(): # Upstream's own vector (tests/test_decoder.py::test_cyclic_array): # tag 28 wrapping a ONE-element array whose element is tag 29 index 0. v = dec('d81c81d81d00') return 'yes' if v[-1] is v else 'no' def _selfref_cycle3_rt(): # The SAME shape with two scalars in front -- which is exactly what this # package's own encoder emits for [1, 2, ] -- and the reference does # NOT come back resolved. See the block above this case for what that is. v = cbor2.loads(cbor2.dumps(_cycle(), value_sharing=True)) return '%s|resolved=%s' % (type(v[-1]).__name__, 'yes' if v[-1] is v else 'no') def _refusal(buf): try: cbor2.loads(buf) return 'NO ERROR' except Exception as exc: return type(exc).__name__ def _deep(): old = sys.getrecursionlimit() try: sys.setrecursionlimit(200) try: cbor2.loads(b'\x81' * 400 + b'\x00') return 'NO ERROR' except Exception as exc: return '%s|cbor_decode_error=%s' % ( type(exc).__name__, 'yes' if isinstance(exc, CBORDecodeError) else 'no') finally: sys.setrecursionlimit(old) def _shared_state(): from io import BytesIO from cbor2 import CBORDecoder d = CBORDecoder(BytesIO(bytes.fromhex('d81c83010203') + bytes.fromhex('d81d00'))) first = d.decode() try: second = d.decode() return 'LEAKED: %r after %r' % (second, first) except Exception as exc: return type(exc).__name__ def _sig_default(name): import inspect p = inspect.signature(cbor2.loads).parameters.get(name) return 'absent' if p is None else repr(p.default) def _stream_dump(): from io import BytesIO buf = BytesIO() cbor2.dump([1, 2, 3], buf) return 'yes' if buf.getvalue() == cbor2.dumps([1, 2, 3]) else h(buf.getvalue()) def _stream_load(): from io import BytesIO return repr(cbor2.load(BytesIO(bytes.fromhex('83010203')))) def _stream_position(): from io import BytesIO buf = BytesIO(bytes.fromhex('83010203') + bytes.fromhex('6449455446')) first = cbor2.load(buf) second = cbor2.load(buf) return '%r|%r' % (first, second) class _Point(object): def __init__(self, x, y): self.x = x self.y = y def _default_hook(): def enc_point(encoder, value): encoder.encode(CBORTag(4000, [value.x, value.y])) return h(cbor2.dumps(_Point(1, 2), default=enc_point, canonical=True)) def _no_hook(): try: cbor2.dumps(_Point(1, 2)) return 'NO ERROR' except Exception as exc: return type(exc).__name__ def _tag_hook(): def hook(decoder, tag): if tag.tag == 4000: return 'point%d,%d' % (tag.value[0], tag.value[1]) return tag return repr(cbor2.loads(bytes.fromhex('d90fa0820102'), tag_hook=hook)) def _object_hook(): def hook(decoder, d): return 'map-of-%d' % len(d) return repr(cbor2.loads(bytes.fromhex('a201020304'), object_hook=hook)) def h(b): return b.hex() def enc(value, **kw): return h(cbor2.dumps(value, **kw)) def cenc(value, **kw): return h(cbor2.dumps(value, canonical=True, **kw)) def dec(hexstr): return cbor2.loads(bytes.fromhex(hexstr)) # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED, AND WHICH OF THE TWO IMPLEMENTATIONS IT IS. # # cbor2 ships a pure-Python codec AND a C accelerator, `_cbor2', and # cbor2/__init__.py picks between them at import with a `try: from _cbor2 # import *' guarded by `except ImportError'. On the ACCELERATED path every # public callable comes out of the extension and reports `__module__ == # "_cbor2"'; on the pure-Python path the `except' arm rewrites each one's # `__module__' to "cbor2". So `dumps.__module__' is the one-word answer to # which half is running, and `_cbor2' in sys.modules is the same fact read off # the import system instead of off an attribute. # # THIS IMAGE IS PURE PYTHON BY CONSTRUCTION, not by accident: python3-cbor2's # bbappend sets CBOR2_BUILD_C_EXTENSION=0 for class-target, because the # extension needs ~147 env.Py* imports and this image's python3.12 exports 35 # symbols and no Py* at all (BRINTOS-2046). A run that reported `_cbor2' here # would mean the bbappend had stopped taking, which is worth a red of its own # rather than a silently different set of code under every case below. emit('module_file', getattr(cbor2, '__file__', '')) emit('is_package', 'yes' if hasattr(cbor2, '__path__') else 'no') emit('impl_module', getattr(cbor2.dumps, '__module__', '')) emit('c_ext_loaded', 'yes' if '_cbor2' in sys.modules else 'no') guard('c_ext_import', lambda: _import_cbor2_ext()) try: from importlib.metadata import version as _dist_version emit('dist_version', _dist_version('cbor2')) except Exception as exc: emit('dist_version', '%s: %s' % (type(exc).__name__, exc)) # The submodules the public names are re-exported from. `__init__.py' does # `from ._decoder import ...', so a truncated install fails at import rather # than here -- naming them individually turns "the package is complete" into a # measurement the packaging can be judged by. # `decoder', `encoder' and `types' are DEPRECATED SHIMS that re-export from # the underscore-prefixed modules and emit a UserWarning on import. The # warning is captured rather than left to reach stderr: the suite asserts that # `import cbor2' writes NOTHING to stderr, and a probe that leaked three # warnings would make its own transcript disagree with that case for no reason. # Which of the seven warns is itself pinned, because a release that stopped # warning has removed the shim's deprecation, and one that started warning on # the underscore modules has moved the API. import warnings for _m in ('_decoder', '_encoder', '_types', 'decoder', 'encoder', 'types', 'tool'): try: with warnings.catch_warnings(record=True) as _w: warnings.simplefilter('always') __import__('cbor2.' + _m) emit('submodule_' + _m, 'yes|deprecated' if any(issubclass(x.category, UserWarning) for x in _w) else 'yes') except Exception as exc: emit('submodule_' + _m, '%s: %s' % (type(exc).__name__, exc)) # ------------------------------------------------- major type 0/1: integers -- # THE BOUNDARY OF EVERY INTEGER ENCODING, from both sides. # # CBOR encodes an unsigned integer in one of five widths and picks the # shortest: 0..23 inline in the initial byte, then uint8 (24), uint16 (25), # uint32 (26) and uint64 (27). A negative integer uses major type 1 over the # same five widths, encoding -1-n. Every boundary below is the LAST value of # one width and the FIRST of the next, which is where an off-by-one in the # width selection shows and nowhere else. _ints = [ ('u0', 0), ('u23', 23), ('u24', 24), ('u255', 255), ('u256', 256), ('u65535', 65535), ('u65536', 65536), ('u4294967295', 4294967295), ('u4294967296', 4294967296), ('u_uint64_max', 18446744073709551615), ('n1', -1), ('n24', -24), ('n25', -25), ('n256', -256), ('n257', -257), ('n65536', -65536), ('n65537', -65537), ('n4294967296', -4294967296), ('n4294967297', -4294967297), ('n_int64_min', -18446744073709551616), ] for _k, _v in _ints: guard('int_enc_' + _k, (lambda v=_v: cenc(v))) for _k, _v in _ints: guard('int_rt_' + _k, (lambda v=_v: repr(cbor2.loads(cbor2.dumps(v))))) # PAST THE 64-BIT WIDTHS THE ENCODING CHANGES KIND, not just width: an integer # that does not fit a uint64 becomes a TAG 2 (positive bignum) or TAG 3 # (negative bignum) wrapping a byte string. These two are the ticket's "big # integer through tags 2 and 3" and they are also the only cases here that # reach `binascii' on the way back -- decode_bignum does `from binascii import # hexlify' -- which is a BUILTIN in this interpreter (PyInit_binascii is linked # into python3.12) rather than a shipped .py, so no package declares it. guard('bignum_pos_enc', lambda: cenc(18446744073709551616)) guard('bignum_neg_enc', lambda: cenc(-18446744073709551617)) guard('bignum_pos_dec', lambda: repr(dec('c249010000000000000000'))) guard('bignum_neg_dec', lambda: repr(dec('c349010000000000000000'))) guard('bignum_pos_rt', lambda: repr(cbor2.loads(cbor2.dumps(2 ** 512)))) # ---------------------------------------------------- major type 7: floats -- # EVERY FLOAT WIDTH, AND THE THREE VALUES THAT HAVE NO ORDINARY COMPARISON. # # canonical=True selects the shortest float that round-trips, so these are the # RFC's own literals: half where half suffices (1.0 -> f93c00), single where it # does not (100000.0 -> fa47c35000), double otherwise. _floats = [ ('p0', 0.0), ('n0', -0.0), ('p1', 1.0), ('p1_1', 1.1), ('p1_5', 1.5), ('half_max', 65504.0), ('single_100000', 100000.0), ('single_max', 3.4028234663852886e+38), ('double_1e300', 1.0e+300), ('subnormal_half', 5.960464477539063e-8), ('half_min_normal', 0.00006103515625), ('neg4', -4.0), ('neg4_1', -4.1), ] for _k, _v in _floats: guard('float_enc_' + _k, (lambda v=_v: cenc(v))) for _k, _v in _floats: guard('float_rt_' + _k, (lambda v=_v: repr(cbor2.loads(cbor2.dumps(v))))) # -0.0 IS NOT 0.0 AND `repr' IS THE ONLY THING THAT SAYS SO. `-0.0 == 0.0' is # True in Python, so a round-trip case comparing values would pass on an # encoder that lost the sign bit. copysign reads the bit directly. guard('float_neg_zero_sign', lambda: repr(math.copysign(1.0, cbor2.loads(cbor2.dumps(-0.0))))) # NaN AND THE INFINITIES. `NaN != NaN', so the round-trip is asserted through # math.isnan rather than through equality -- a case written as `loads(dumps(nan)) # == nan' is False on a CORRECT implementation and would have to be inverted to # pass, which is how a broken one gets through. guard('float_enc_inf', lambda: cenc(float('inf'))) guard('float_enc_ninf', lambda: cenc(float('-inf'))) guard('float_enc_nan', lambda: cenc(float('nan'))) guard('float_rt_inf', lambda: repr(cbor2.loads(cbor2.dumps(float('inf'))))) guard('float_rt_ninf', lambda: repr(cbor2.loads(cbor2.dumps(float('-inf'))))) guard('float_rt_nan_isnan', lambda: 'yes' if math.isnan(cbor2.loads(cbor2.dumps(float('nan')))) else 'no') guard('float_dec_half_nan', lambda: 'yes' if math.isnan(dec('f97e00')) else 'no') guard('float_dec_half_inf', lambda: repr(dec('f97c00'))) # ------------------------------------------- major type 7: the simple values -- # true / false / null / undefined, and the two simple-value widths. `undefined' # is cbor2's own singleton for CBOR's 0xf7, which JSON has no counterpart for; # it is the one value here that has no Python equivalent and so is asserted by # identity as well as by encoding. guard('simple_enc_true', lambda: cenc(True)) guard('simple_enc_false', lambda: cenc(False)) guard('simple_enc_null', lambda: cenc(None)) guard('simple_enc_undefined', lambda: cenc(undefined)) guard('simple_dec_true', lambda: repr(dec('f5'))) guard('simple_dec_false', lambda: repr(dec('f4'))) guard('simple_dec_null', lambda: repr(dec('f6'))) guard('simple_dec_undefined_is', lambda: 'yes' if dec('f7') is undefined else 'no') guard('simple_enc_16', lambda: cenc(CBORSimpleValue(16))) guard('simple_enc_255', lambda: cenc(CBORSimpleValue(255))) guard('simple_dec_16', lambda: repr(dec('f0'))) guard('simple_dec_255', lambda: repr(dec('f8ff'))) # ------------------------------------------ major type 2/3: strings, bytes ---- # BYTE STRINGS AND TEXT STRINGS ARE DIFFERENT MAJOR TYPES, and a codec that # conflated them would still round-trip on ASCII. The non-ASCII cases are the # ones that separate "encodes UTF-8" from "encodes latin-1 and gets away with # it on the a-z corpus": U+00FC is two bytes, U+6C34 three, U+10151 four -- one # fixture per UTF-8 length above one. # # WRITTEN AS \\u ESCAPES ON PURPOSE. This source file must stay pure ASCII: a # guest under the C locale that read a literal U+6C34 out of a UTF-8 source # would be measuring the interpreter's source decoding, not cbor2's. guard('bstr_enc_empty', lambda: cenc(b'')) guard('bstr_enc_4', lambda: cenc(bytes.fromhex('01020304'))) guard('bstr_enc_24', lambda: cenc(b'\x00' * 24)) guard('tstr_enc_empty', lambda: cenc(u'')) guard('tstr_enc_a', lambda: cenc(u'a')) guard('tstr_enc_ietf', lambda: cenc(u'IETF')) guard('tstr_enc_escapes', lambda: cenc(u'"\\')) guard('tstr_enc_u00fc', lambda: cenc(u'\u00fc')) guard('tstr_enc_u6c34', lambda: cenc(u'\u6c34')) guard('tstr_enc_u10151', lambda: cenc(u'\U00010151')) guard('bstr_dec_4', lambda: h(dec('4401020304'))) guard('tstr_dec_ietf', lambda: repr(dec('6449455446'))) # The decoded text is reported as its CODEPOINTS, not as text: printing the # character itself would put a non-ASCII byte on stdout and make the case a # statement about the guest's locale. guard('tstr_dec_u00fc', lambda: ','.join(str(ord(c)) for c in dec('62c3bc'))) guard('tstr_dec_u6c34', lambda: ','.join(str(ord(c)) for c in dec('63e6b0b4'))) guard('tstr_dec_u10151', lambda: ','.join(str(ord(c)) for c in dec('64f0908591'))) guard('tstr_dec_type', lambda: type(dec('6161')).__name__) guard('bstr_dec_type', lambda: type(dec('4101')).__name__) # A DEFINITE-LENGTH STRING LONGER THAN 65535 BYTES is the case CVE-2025-64076 # was about: decode_definite_long_string computed its chunk length wrongly and # issued a read(-1). The bug was in the C decoder, which this image does not # ship, so this is a REGRESSION GUARD rather than a reproduction -- it pins that # the pure-Python path handles the same input, and it is the case that would # turn red first if the accelerator were ever reinstated unpatched. guard('tstr_long_rt_len', lambda: str(len(cbor2.loads(cbor2.dumps(u'x' * 70000))))) guard('tstr_long_prefix', lambda: h(cbor2.dumps(u'x' * 70000)[:5])) guard('bstr_long_rt_len', lambda: str(len(cbor2.loads(cbor2.dumps(b'y' * 70000))))) # ------------------------------------ major type 4/5: arrays, maps, nesting --- guard('array_enc_empty', lambda: cenc([])) guard('array_enc_123', lambda: cenc([1, 2, 3])) guard('array_enc_nested', lambda: cenc([1, [2, 3], [4, 5]])) guard('array_enc_25', lambda: cenc(list(range(1, 26)))) guard('map_enc_empty', lambda: cenc({})) guard('map_enc_int', lambda: cenc({1: 2, 3: 4})) guard('map_enc_mixed', lambda: cenc({u'a': 1, u'b': [2, 3]})) guard('map_enc_in_array', lambda: cenc([u'a', {u'b': u'c'}])) guard('map_enc_5', lambda: cenc({u'a': u'A', u'b': u'B', u'c': u'C', u'd': u'D', u'e': u'E'})) guard('array_dec_nested', lambda: repr(dec('8301820203820405'))) guard('map_dec_mixed', lambda: repr(dec('a26161016162820203'))) guard('map_dec_key_type', lambda: type(list(dec('a201020304'))[0]).__name__) # NESTED TO A FIXED DEPTH, both ways. Ten levels is deep enough that a # length-prefix slip at any level shows and shallow enough to say nothing about # the recursion limit, which `deep_*' below measures on purpose. guard('nest10_enc', lambda: cenc(_nest(10))) guard('nest10_rt', lambda: repr(cbor2.loads(cbor2.dumps(_nest(10))))) # ------------------------------------------ indefinite-length, both major types -- # THE INDEFINITE FORMS ARE DECODE-ONLY HERE, AND THAT IS NOT A GAP. # # cbor2's encoder never emits an indefinite-length item for an ordinary Python # value -- a list is a definite-length array -- so there is no encode case to # write. What the port must do is READ them, because a peer is free to send # them, and RFC 8949 Appendix A gives a literal for each: a chunked byte # string, a chunked text string, an unbounded array and an unbounded map. guard('indef_bstr_dec', lambda: h(dec('5f42010243030405ff'))) guard('indef_tstr_dec', lambda: repr(dec('7f657374726561646d696e67ff'))) guard('indef_array_empty_dec', lambda: repr(dec('9fff'))) guard('indef_array_dec', lambda: repr(dec('9f018202039f0405ffff'))) guard('indef_array_mixed_dec', lambda: repr(dec('9f01820203820405ff'))) guard('indef_array_tail_dec', lambda: repr(dec('83018202039f0405ff'))) guard('indef_array_head_dec', lambda: repr(dec('83019f0203ff820405'))) guard('indef_array_25_dec', lambda: repr(dec('9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff'))) guard('indef_map_dec', lambda: repr(dec('bf61610161629f0203ffff'))) guard('indef_map_in_array_dec', lambda: repr(dec('826161bf61626163ff'))) guard('indef_map_empty_dec', lambda: repr(dec('bfff'))) # A chunked string decodes to ONE string; the chunk boundaries are transport # and must not survive into the value. Asserting the length as well as the # text is what separates "joined the chunks" from "returned the first one". guard('indef_tstr_len', lambda: str(len(dec('7f657374726561646d696e67ff')))) # --------------------------------------------------------------- the tags ---- # TAG 0 AND TAG 1 ARE THE SAME INSTANT IN TWO ENCODINGS -- a date/time STRING # and an epoch NUMBER -- and cbor2 decodes both to an aware `datetime'. These # are also the only cases in this probe that reach `calendar.timegm', which is # why this package's one non-core RDEPENDS is python3-datetime. _dt = datetime.datetime(2013, 3, 21, 20, 4, 0, tzinfo=datetime.timezone.utc) guard('tag0_enc', lambda: cenc(_dt, datetime_as_timestamp=False)) guard('tag1_enc', lambda: cenc(_dt, datetime_as_timestamp=True)) guard('tag0_dec', lambda: dec('c074323031332d30332d32315432303a30343a30305a').isoformat()) guard('tag1_dec', lambda: dec('c11a514b67b0').isoformat()) guard('tag1_frac_dec', lambda: dec('c1fb41d452d9ec200000').isoformat()) guard('tag0_dec_type', lambda: type(dec('c074323031332d30332d32315432303a30343a30305a')).__name__) guard('tag1_dec_tzaware', lambda: 'yes' if dec('c11a514b67b0').tzinfo is not None else 'no') # The two encodings must agree on the instant. This is the case a suite that # tested each tag alone would miss. guard('tag0_tag1_same_instant', lambda: 'yes' if (dec('c074323031332d30332d32315432303a30343a30305a') == dec('c11a514b67b0')) else 'no') # AN UNKNOWN TAG IS DATA, NOT AN ERROR. cbor2 hands back a CBORTag rather than # refusing, which is what lets a caller carry a tag it does not understand # through a decode/encode cycle unchanged. Tag 23 (expected base16) and tag 24 # (encoded CBOR data item) are the RFC's own examples. guard('tag_unknown_dec', lambda: repr(dec('d74401020304'))) guard('tag24_dec', lambda: repr(dec('d818456449455446'))) guard('tag32_dec', lambda: repr(dec('d82076687474703a2f2f7777772e6578616d706c652e636f6d'))) guard('tag_roundtrip', lambda: h(cbor2.dumps(CBORTag(6000, u'x'), canonical=True))) guard('tag_roundtrip_back', lambda: repr(cbor2.loads(cbor2.dumps(CBORTag(6000, u'x'))))) # TAGS THAT NEED A STDLIB MODULE NO RDEPENDS OF THIS PACKAGE NAMES. # # decode_fraction (tag 4) and decode_bigfloat (tag 5) do `from decimal import # Decimal', and decode_uuid (tag 37) does `from uuid import UUID'. On this # image `decimal.py' is in python3-numbers and `uuid.py' in python3-netclient, # and python3-cbor2 declares NEITHER -- its Depends line is `python3-core, # python3-datetime' and nothing else. The C halves are present (PyInit__decimal # and PyInit__uuid are linked into python3.12); it is the .py wrapper that a # package has to ship. # # Both are therefore reported as a VERDICT rather than as a value, because # either answer is correct and which one this image gives depends on whether # some OTHER package in EMCRAFT_TOOLS pulled the sub-package in. `available' # means the module was importable AND the tag decoded to the right value; # `absent:' means the import failed and names what is missing. A red # here is neither of those -- it is the tag decoding wrongly on an image that # had the module all along. guard('decimal_module', lambda: _optional_module('decimal')) guard('uuid_module', lambda: _optional_module('uuid')) guard('tag4_verdict', lambda: _optional_tag('decimal', 'c48221196ab3', '273.15')) guard('tag5_verdict', lambda: _optional_tag('decimal', 'c5822003', '1.5')) guard('tag37_verdict', lambda: _optional_tag( 'uuid', 'd825505c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c', '5c9c9c9c-9c9c-9c9c-9c9c-9c9c9c9c9c9c')) # ------------------------------------------- the self-referential structure --- # THE ENCODER MUST EITHER HANDLE THIS BY THE DOCUMENTED RULE OR REJECT IT, and # cbor2 does BOTH, selected by `value_sharing'. # # By default it REJECTS -- CBOREncodeValueError, because emitting a cycle into a # stream a peer may not be able to read back is worse than refusing. With # value_sharing=True it HANDLES it by RFC 8746's rule: the container is wrapped # in tag 28 (shareable) and the back-reference becomes tag 29 (sharedref) # carrying the index. Both arms are pinned, because a build that lost the # cycle detection would silently recurse until the stack went. guard('selfref_default', lambda: _selfref_default()) guard('selfref_shared_enc', lambda: _selfref_shared_enc()) guard('selfref_cycle1_rt', lambda: _selfref_cycle1_rt()) guard('selfref_cycle3_rt', lambda: _selfref_cycle3_rt()) # --------------------------------------------------- the decoder's refusals --- # THIS IS THE SUBSTANCE OF THE PORT, not an afterthought: four CVE patches ride # in this recipe's SRC_URI and all four are against the decoder. Each case # below asserts the EXCEPTION TYPE, because "raised something" is not the # property that matters -- a decoder that consumed memory or returned a short # value on malformed input is what the patches exist to prevent. # # CBORDecodeEOF is a subclass of CBORDecodeError, so the type name is reported # and the subclass relation is asserted separately. guard('trunc_empty', lambda: _refusal(b'')) guard('trunc_uint16', lambda: _refusal(bytes.fromhex('19'))) guard('trunc_uint64', lambda: _refusal(bytes.fromhex('1b0000'))) guard('len_longer_bstr', lambda: _refusal(bytes.fromhex('43ab'))) guard('len_longer_tstr', lambda: _refusal(bytes.fromhex('6461'))) guard('len_longer_array', lambda: _refusal(bytes.fromhex('8301'))) guard('len_longer_map', lambda: _refusal(bytes.fromhex('a20102'))) guard('indef_unterminated', lambda: _refusal(bytes.fromhex('9f0102'))) guard('eof_is_decode_error', lambda: 'yes' if issubclass(CBORDecodeEOF, CBORDecodeError) else 'no') guard('value_err_is_decode_error', lambda: 'yes' if issubclass(CBORDecodeValueError, CBORDecodeError) else 'no') # A DECLARED LENGTH THE INPUT CANNOT POSSIBLY SATISFY. `5bffffffffffffffff' is # a byte string announcing 2**64-1 bytes over two bytes of data. The property # under test is that the decoder does not TRY to allocate it: it must refuse # from the stream it actually has. A decoder that pre-allocated on the declared # length would take the guest's whole heap here, so this case is also the reason # the suite's timeout is what it is. guard('len_absurd_bstr', lambda: _refusal(bytes.fromhex('5bffffffffffffffff'))) guard('len_absurd_tstr', lambda: _refusal(bytes.fromhex('7bffffffffffffffff'))) guard('len_absurd_array', lambda: _refusal(bytes.fromhex('9bffffffffffffffff'))) # NESTING PAST THE RECURSION LIMIT, AND WHAT IT ACTUALLY RAISES. # # The pure-Python decoder recurses in PYTHON for each nesting level, so the # bound it hits is `sys.getrecursionlimit()' and the exception is # RecursionError -- NOT a CBORDecodeError. That is worth stating plainly # because it is easy to expect otherwise: cbor2 has no configurable maximum # depth, and the "depth" the CVE-2025-68131 patch added to this decoder is a # RE-ENTRANCY COUNTER for clearing shared references between top-level decodes, # not a limit. # # THE LIMIT IS LOWERED FIRST, ON PURPOSE, AND THAT IS A SAFETY MEASURE RATHER # THAN A CONVENIENCE. At the default limit this case would drive ~1000 nested # Python frames on a wasm32 guest whose native stack is not the host's, and a # stack that went before the interpreter's own counter did would take the whole # guest down -- in a shared batch image that is every member's round, not this # suite's one case. Setting the limit to 200 and feeding 400 levels makes the # interpreter raise at a depth two orders of magnitude below anything the stack # could care about, and the answer is the same one the default limit would give. guard('deep_nesting', lambda: _deep()) # ------------------------------- CVE-2025-68131: shared state does not leak --- # The patch's own property, on the half of the codec this image ships: shared # references registered during one TOP-LEVEL decode must not be visible to the # next one on the SAME decoder. Feeding a bare tag 29 (sharedref index 0) after # a decode that registered a shareable must FAIL rather than resolve to the # previous decode's object. guard('shared_state_cleared', lambda: _shared_state()) # ------------------------- CVE-2026-26209: read_size is back on the API ------- # The patch restores read_size=1 as the default and adds the parameter to # load()/loads() for API completeness. Asserting the DEFAULT and that an # explicit buffered value decodes identically is the whole of what the pure # Python side of that patch changed. guard('read_size_default', lambda: _sig_default('read_size')) guard('read_size_explicit', lambda: repr(cbor2.loads(bytes.fromhex('83010203'), read_size=4096))) # ------------------------------------------------ canonical vs default form --- # THE ONE DIFFERENCE MOST LIKELY TO BE MISREAD AS A PORT BUG. cbor2's DEFAULT # encoder writes every float as a float64 and leaves map keys in insertion # order; `canonical=True' selects the shortest float and sorts keys by their # encoded bytes. Both are correct CBOR and the RFC's Appendix A literals are # the canonical ones, so a reader comparing plain `dumps(1.0)' against the RFC # would find a difference that is not an error. Both forms are pinned here so # that the distinction is recorded rather than rediscovered. guard('default_float_1', lambda: enc(1.0)) guard('canonical_float_1', lambda: cenc(1.0)) guard('default_map_order', lambda: enc({u'b': 1, u'a': 2})) guard('canonical_map_order', lambda: cenc({u'b': 1, u'a': 2})) # Canonical key order is by ENCODED LENGTH first, then bytewise -- so a # one-character key sorts before a two-character one whatever the letters are. guard('canonical_key_len_first', lambda: cenc({u'aa': 1, u'b': 2})) # ----------------------------------------------------- the streaming halves --- # dump/load against a file object must agree with dumps/loads against bytes, # and `load' must leave the stream positioned after the item it read so a # second call reads the next one. That position is exactly what CVE-2026-26209 # restored the default read_size to protect. guard('stream_dump_matches', lambda: _stream_dump()) guard('stream_load_matches', lambda: _stream_load()) guard('stream_load_position', lambda: _stream_position()) # ------------------------------------------------------------- the hooks ----- # THE HOOKS FOR TYPES THE FORMAT DOES NOT DEFINE, both directions. `default' # is consulted for a value the encoder has no rule for, and `tag_hook' for a # tag the decoder does not know. Without these a caller cannot extend the # format at all, and a port that lost them would still pass every case above. guard('encode_default_hook', lambda: _default_hook()) guard('encode_no_hook_raises', lambda: _no_hook()) guard('decode_tag_hook', lambda: _tag_hook()) guard('decode_object_hook', lambda: _object_hook()) # ------------------------------------------------------- FrozenDict, keys ----- # A CBOR MAP MAY BE KEYED BY A CONTAINER, which Python cannot be -- so cbor2 # returns an IMMUTABLE key: a map key that is itself a map comes back as # FrozenDict, and one that is an array comes back as a tuple. A caller # expecting a dict or a list there gets a TypeError at the point of use rather # than at decode, so the types are pinned here. guard('key_map_type', lambda: type(list(dec('a1a10102 03'.replace(' ', '')))[0]).__name__) guard('key_array_type', lambda: type(list(dec('a18201020 3'.replace(' ', '')))[0]).__name__) guard('frozendict_hashable', lambda: 'yes' if isinstance(hash(FrozenDict({1: 2})), int) else 'no') # ----------------------------------------------------------------- the end --- emit('emitted_count', str(len(emitted))) emit('probe_done', 'yes')