. "$BST_LIB" # BST-TIMEOUT: 2400 # FIVE python3 starts, and they are the whole cost. # # Everything else below reads the filesystem or dpkg with # # shell builtins, or greps the output of one of the five. # # # # The five are: a bare `import\'; a second import whose # # STDERR is the assertion (`import-quiet\'); the probe, # # which takes ONE start for two hundred measurements; and # # TWO runs of the shipped /usr/bin/cbor2, which is reached # # through its own `#!/usr/bin/env python3\' shebang and so # # is not foldable into the probe -- launching the installed # # script IS what those two cases measure. # # # # 2400 is 480s per start, against the ~60s a python3 start # # has measured at on this arm. The margin over # # 99-python3-pyparsing\'s 400s-per-start is for the probe\'s # # two 70000-byte strings and its three absurd-declared-length # # refusals, which are the only cases here that touch more # # than a few dozen bytes. # # # # NOTHING HERE IS QUADRATIC IN ANYTHING. The deepest input # # is 400 nested arrays decoded against a recursion limit # # lowered to 200, so it stops at 200 frames by construction. # # # # NOT MEASURED ON TARGET YET: this port\'s first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-cbor2 5.6.4 (meta-openembedded/meta-python # recipes-devtools/python/python3-cbor2_5.6.4.bb, `inherit pypi # python_setuptools_build_meta ptest\', PyPI sdist, FOUR CVE patches in # SRC_URI) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # cbor2 implements CBOR, RFC 8949 -- the binary object representation used # where JSON\'s shape is wanted without JSON\'s size or its inability to carry # binary data. `dumps\'/`loads\' over bytes, `dump\'/`load\' over a file object, # the semantic-tag machinery, and hooks for the types the format does not # define. # # ============================================================================ # THE ONE THING THIS PORT DECIDED, AND WHY EVERY CASE BELOW DEPENDS ON IT. # ============================================================================ # # cbor2 ships TWO implementations of the same API: the pure-Python codec under # cbor2/, and `_cbor2\', a top-level CPython extension. cbor2/__init__.py picks # between them with `try: from _cbor2 import *\' guarded by `except ImportError\'. # # THIS IMAGE SHIPS THE PURE-PYTHON HALF AND NO EXTENSION AT ALL. The stock # recipe builds the extension and it LINKS -- measured, 446288 bytes of real # wasm side module -- but it can never load: it carries 147 env.Py* / env._Py* # imports and this image\'s python3.12 exports 35 symbols, none of them Py*, # with no libpython3.12.so shipped to bind against instead (BRINTOS-2046, first # measured on python3-bitarray in BRINTOS-2032). So python3-cbor2_%.bbappend # sets CBOR2_BUILD_C_EXTENSION=0 for class-target and the file is not built. # # `pkg-no-so\' and `no-c-ext\' below are therefore not hygiene cases: they are # the assertion that the bbappend still takes. A run where they went red would # mean every behavioural case in this suite had silently changed which code it # was measuring. # # ============================================================================ # WHERE THE EXPECTED VALUES CAME FROM. # ============================================================================ # # Every expectation in this file is the OUTPUT of a control run, not a reading # of the source: cbor2 5.6.4 with all four CVE patches applied, pure-Python, # on CPython 3.12.13 -- the same version pair the target runs, taken from this # build\'s own python3-native sysroot rather than from a distro python. # # Where a value is also an RFC 8949 Appendix A literal, it agreed with the RFC # byte for byte, and the encode cases run `canonical=True\' for the reason the # probe\'s header gives: cbor2\'s DEFAULT encoder writes every float as a # float64, so plain dumps(1.0) is fb3ff0000000000000 and comparing it against # the RFC\'s f93c00 would red on a difference that is not an error. Both forms # are pinned (`default-float-1\' beside `canonical-float-1\') so that the # distinction is recorded rather than rediscovered. # # ============================================================================ # THE TWO FINDINGS MOST LIKELY TO BE MISREAD AS PORT BUGS. # ============================================================================ # # DEEP NESTING RAISES RecursionError, NOT CBORDecodeError. cbor2 has no # configurable maximum decode depth; the pure-Python decoder recurses in Python # per nesting level, so the bound it hits is sys.getrecursionlimit(). The # "depth" CVE-2025-68131 added to this decoder is a RE-ENTRANCY COUNTER for # clearing shared references between top-level decodes, not a limit. # `deep-nesting\' pins both halves of that -- the exception name AND that it is # not a CBORDecodeError -- so a reader who expected otherwise is corrected here. # # A SELF-REFERENTIAL STRUCTURE DOES NOT SURVIVE ITS OWN ENCODER, and that is # upstream\'s, not this platform\'s. The ENCODE half is correct and both of its # documented arms are pinned: by default cbor2 REJECTS a cycle # (CBOREncodeValueError), and with value_sharing=True it emits RFC 8746\'s tags # 28/29 -- for [1, 2, ], exactly d81c830102d81d00. Feeding those bytes # BACK gives a list whose third element is an int rather than the list itself. # Upstream\'s own test vector for the same shape with ONE element, # d81c81d81d00, resolves correctly, and `selfref-cycle1-rt\' pins that it does. # BOTH results reproduce identically on the x86-64 control at the same version # pair, with and without the CVE patches, so neither is about wasm32-hwjs. # `selfref-cycle3-rt\' records the behaviour as measured rather than asserting # the one a reader would assume. # # ============================================================================ # WHAT IS DELIBERATELY NOT COVERED. # ============================================================================ # # The recipe inherits `ptest\', but the -ptest sub-package RDEPENDS on # python3-hypothesis, python3-pytest and python3-unittest-automake-output, none # of which this image can satisfy. So the suite drives the API directly instead # of running upstream\'s tests, and there is no ptest case here. # # CVE-2025-64076 and CVE-2026-26209-pre1 patch the C decoder ONLY. They are not # reproducible on this image because the code they change is not shipped. # `tstr-long-*\' feeds the pure-Python path the same >65535-byte definite-length # string CVE-2025-64076 was about, as a regression guard for the day the # accelerator is reinstated -- but it is a guard, not a reproduction, and this # comment is here so nobody reads it as one. # # ============================================================================ P=python3-cbor2 # `suite-canary\' is emitted FIRST and unconditionally. A suite that dies before # its first result line is indistinguishable from one that was never scheduled, # so this one line is what separates "ran and found nothing" from "did not run". bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/cbor2 DI=$SITE/cbor2-5.6.4.dist-info PKGLIST=/var/lib/dpkg/info/python3-cbor2.list PROBE=$BST_DATA/python3-cbor2-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-cbor2.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- packaging -- # WHERE THE PACKAGING SAYS THE MODULE LANDS. These run before any import, so a # red here says the .deb is wrong rather than that the codec misbehaves. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/package-init test -s "$PKG/__init__.py" # It is a PACKAGE, not a single module. A release that flattened it would still # import and still pass every behavioural case, so the shape is asserted. check $P/no-flat-module test ! -f "$SITE/cbor2.py" # The five modules the public names come out of, plus the three deprecated # shims. `__init__.py\' does `from ._decoder import ...\', so a truncated install # fails at import rather than here -- naming them individually is what turns # "the package is complete" into a measurement the packaging can be judged by. for _m in _decoder _encoder _types decoder encoder types tool; do check $P/module-$_m test -s "$PKG/$_m.py" done check $P/py-typed test -f "$PKG/py.typed" # ---------------------------------------------------- THE EXTENSION IS ABSENT -- # THE ASSERTION THE WHOLE PORT TURNS ON. `_cbor2.cpython-312.so\' is what the # stock recipe builds and what python3-cbor2_%.bbappend declines; it cannot bind # this interpreter\'s C API (BRINTOS-2046) and would ship 446 KB whose best # outcome is being swallowed by an except-clause. Both the file and the shape of # the package list are asserted, because the second one catches a .so landing # under any other name. check $P/no-c-ext-so test ! -e "$SITE/_cbor2.cpython-312.so" check $P/no-c-ext-any sh -c "! ls $SITE/_cbor2*.so >/dev/null 2>&1" # The shipped console script. Unlike python3-pyparsing this package DOES install # an executable -- pyproject.toml carries a [project.scripts] table -- so the # shebang path is reachable and `cli-*\' below exercises it. check $P/bin-script test -x /usr/bin/cbor2 # --------------------------------------------------------- the dist-info ------ check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 5.6.4" cat "$DI/METADATA" check_out $P/dist-info-name "Name: cbor2" cat "$DI/METADATA" check $P/dist-info-wheel test -f "$DI/WHEEL" # Root-Is-Purelib: true IS THE BBAPPEND\'S SIGNATURE IN THE METADATA. With the # extension built this wheel is platlib and the tag is a platform tag; with it # declined the wheel is pure and the tag is py3-none-any. So these two lines # say, from the packaging side rather than from a file check, that the build # produced a pure-Python distribution. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" # top_level.txt names ONLY `cbor2\'. With the extension built it names `_cbor2\' # as well, so this one-word file is the shortest statement that the accelerator # was not packaged. check_eq $P/dist-info-top-level "cbor2" "$(cat "$DI/top_level.txt" 2>/dev/null)" check_out $P/dist-info-entrypoint "cbor2 = cbor2.tool:main" cat "$DI/entry_points.txt" # ------------------------------------------------------------ the .deb list --- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NO SHARED OBJECT ANYWHERE IN THE PACKAGE. The file check above names the one # path the extension would take; this one catches it under any name. check_eq $P/pkg-no-so "" \ "$(grep -c '\.so$' "$W/pkgf" 2>/dev/null | grep -v '^0$')" # Everything shipped is Python, metadata or the one console script. A file that # is none of those is a packaging change nobody described. _bad="" while IFS= read -r _f; do case "$_f" in ''|/usr/lib/python3.12/site-packages) continue ;; */) continue ;; esac [ -d "$_f" ] && continue case "$_f" in *.py|*.pyc|*/py.typed) ;; /usr/bin/cbor2) ;; */METADATA|*/WHEEL|*/RECORD|*/INSTALLER|*/LICENSE|*/LICENSE.txt) ;; */entry_points.txt|*/top_level.txt) ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ------------------------------------------------------------- dpkg status ---- sed -n '/^Package: python3-cbor2$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE DEPENDENCY SET, AND IT IS SHORT ON PURPOSE. python3-cbor2 declares # python3-core and python3-datetime and nothing else. python3-datetime is not # decoration: the encoder\'s tag-1 path does `from calendar import timegm\' and # `calendar.py\' is in that sub-package, so tag 1 is exactly what it is there # for. Everything else the codec imports at module scope -- codecs, collections, # contextlib, functools, io, reprlib, re, struct, math, threading, typing -- is # in python3-core, and decode_bignum\'s `binascii\' is a BUILTIN of this # interpreter rather than a shipped .py, so tags 2 and 3 need no package at all. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" for _d in python3-core python3-datetime; do check_out $P/pkg-depends-$_d "$_d" cat "$W/dep" done # ----------------------------------------------------------------- import ----- check $P/import python3 -c 'import cbor2' # AND IT MUST BE SILENT. `import cbor2\' on a build that still shipped an # unloadable _cbor2.so could succeed and print -- the except-clause swallows the # ImportError, but nothing swallows a loader diagnostic on stderr. An empty # stderr is therefore a statement about the extension as much as about the # module. check_eq $P/import-quiet "" \ "$(python3 -c 'import cbor2' 2>&1 >/dev/null)" # ------------------------------------------------------------------ probe ----- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" exit 0 fi bst_ok $P/probe-present # 1200s is half the suite budget for the one start that does two hundred # measurements, two 70000-byte strings and three absurd-declared-length # refusals. On overrun bst_probe names the last line the probe printed, which # names the case it stopped at. bst_probe $P/probe-run "$W/probe" 1200 python3 "$PROBE" OUT="$W/probe" # One `grep\' per value, split on the FIRST `=\' so a value containing one is # safe. Every key the probe emits is [a-z0-9_]+. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE PROBE RAN TO THE END AND EMITTED EVERYTHING. Without these two, a probe # that died half way would report every case after the death as a mismatch # against an empty string, which reads as two hundred behavioural failures # instead of one truncated transcript. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "205" "$(pv emitted_count)" # ---------------------------------------------------------------- identity -- # WHICH COPY ANSWERED, AND WHICH OF THE TWO IMPLEMENTATIONS IT IS. `impl-module' # is `cbor2' on the pure-Python path and `_cbor2' on the accelerated one, because # the `except ImportError' arm rewrites each public callable's __module__. It is # the one-word answer to which half of the codec every case below is about. check_eq $P/module-answered "$PKG/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/impl-module "cbor2" "$(pv impl_module)" check_eq $P/c-ext-not-loaded "no" "$(pv c_ext_loaded)" check_eq $P/c-ext-import-fails "ModuleNotFoundError: No module named '_cbor2'" "$(pv c_ext_import)" check_eq $P/dist-version "5.6.4" "$(pv dist_version)" # The deprecated shims warn on import and the underscore modules do not; a # release that changed either has moved the API. check_eq $P/submodule-priv-decoder "yes" "$(pv submodule__decoder)" check_eq $P/submodule-priv-encoder "yes" "$(pv submodule__encoder)" check_eq $P/submodule-priv-types "yes" "$(pv submodule__types)" check_eq $P/submodule-decoder "yes|deprecated" "$(pv submodule_decoder)" check_eq $P/submodule-encoder "yes|deprecated" "$(pv submodule_encoder)" check_eq $P/submodule-types "yes|deprecated" "$(pv submodule_types)" check_eq $P/submodule-tool "yes" "$(pv submodule_tool)" # -------------------------------------------- major type 0/1: integer widths -- # THE BOUNDARY OF EVERY INTEGER ENCODING. CBOR picks the shortest of five # widths -- inline 0..23, then uint8/uint16/uint32/uint64 -- and major type 1 # encodes a negative as -1-n over the same five. Each pair 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. check_eq $P/int-enc-u0 "00" "$(pv int_enc_u0)" check_eq $P/int-enc-u23 "17" "$(pv int_enc_u23)" check_eq $P/int-enc-u24 "1818" "$(pv int_enc_u24)" check_eq $P/int-enc-u255 "18ff" "$(pv int_enc_u255)" check_eq $P/int-enc-u256 "190100" "$(pv int_enc_u256)" check_eq $P/int-enc-u65535 "19ffff" "$(pv int_enc_u65535)" check_eq $P/int-enc-u65536 "1a00010000" "$(pv int_enc_u65536)" check_eq $P/int-enc-u4294967295 "1affffffff" "$(pv int_enc_u4294967295)" check_eq $P/int-enc-u4294967296 "1b0000000100000000" "$(pv int_enc_u4294967296)" check_eq $P/int-enc-u-uint64-max "1bffffffffffffffff" "$(pv int_enc_u_uint64_max)" check_eq $P/int-enc-n1 "20" "$(pv int_enc_n1)" check_eq $P/int-enc-n24 "37" "$(pv int_enc_n24)" check_eq $P/int-enc-n25 "3818" "$(pv int_enc_n25)" check_eq $P/int-enc-n256 "38ff" "$(pv int_enc_n256)" check_eq $P/int-enc-n257 "390100" "$(pv int_enc_n257)" check_eq $P/int-enc-n65536 "39ffff" "$(pv int_enc_n65536)" check_eq $P/int-enc-n65537 "3a00010000" "$(pv int_enc_n65537)" check_eq $P/int-enc-n4294967296 "3affffffff" "$(pv int_enc_n4294967296)" check_eq $P/int-enc-n4294967297 "3b0000000100000000" "$(pv int_enc_n4294967297)" check_eq $P/int-enc-n-int64-min "3bffffffffffffffff" "$(pv int_enc_n_int64_min)" # ------------------------------------------------------------ and back again -- # The same twenty values through dumps->loads. A round-trip alone would # cancel a symmetric width error out, which is why the literals above come # first; these say the value is preserved as well as the bytes. check_eq $P/int-rt-u0 "0" "$(pv int_rt_u0)" check_eq $P/int-rt-u23 "23" "$(pv int_rt_u23)" check_eq $P/int-rt-u24 "24" "$(pv int_rt_u24)" check_eq $P/int-rt-u255 "255" "$(pv int_rt_u255)" check_eq $P/int-rt-u256 "256" "$(pv int_rt_u256)" check_eq $P/int-rt-u65535 "65535" "$(pv int_rt_u65535)" check_eq $P/int-rt-u65536 "65536" "$(pv int_rt_u65536)" check_eq $P/int-rt-u4294967295 "4294967295" "$(pv int_rt_u4294967295)" check_eq $P/int-rt-u4294967296 "4294967296" "$(pv int_rt_u4294967296)" check_eq $P/int-rt-u-uint64-max "18446744073709551615" "$(pv int_rt_u_uint64_max)" check_eq $P/int-rt-n1 "-1" "$(pv int_rt_n1)" check_eq $P/int-rt-n24 "-24" "$(pv int_rt_n24)" check_eq $P/int-rt-n25 "-25" "$(pv int_rt_n25)" check_eq $P/int-rt-n256 "-256" "$(pv int_rt_n256)" check_eq $P/int-rt-n257 "-257" "$(pv int_rt_n257)" check_eq $P/int-rt-n65536 "-65536" "$(pv int_rt_n65536)" check_eq $P/int-rt-n65537 "-65537" "$(pv int_rt_n65537)" check_eq $P/int-rt-n4294967296 "-4294967296" "$(pv int_rt_n4294967296)" check_eq $P/int-rt-n4294967297 "-4294967297" "$(pv int_rt_n4294967297)" check_eq $P/int-rt-n-int64-min "-18446744073709551616" "$(pv int_rt_n_int64_min)" # ------------------------------------------------- tags 2 and 3: the bignums -- # 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) or tag 3 # (negative) wrapping a byte string. These are also the only cases here that # reach `binascii' -- decode_bignum does `from binascii import hexlify' -- # which is linked into this python3.12 (PyInit_binascii) rather than shipped # as a .py, so they need no package this recipe does not declare. check_eq $P/bignum-pos-enc "c249010000000000000000" "$(pv bignum_pos_enc)" check_eq $P/bignum-neg-enc "c349010000000000000000" "$(pv bignum_neg_enc)" check_eq $P/bignum-pos-dec "18446744073709551616" "$(pv bignum_pos_dec)" check_eq $P/bignum-neg-dec "-18446744073709551617" "$(pv bignum_neg_dec)" check_eq $P/bignum-pos-rt "13407807929942597099574024998205846127479365820592393377723561443721764030073546976801874298166903427690031858186486050853753882811946569946433649006084096" "$(pv bignum_pos_rt)" # -------------------------------------------- major type 7: the float widths -- # canonical=True selects the shortest float that round-trips, so these are # RFC 8949 Appendix A's own literals: half where half suffices, single where # it does not, double otherwise. check_eq $P/float-enc-p0 "f90000" "$(pv float_enc_p0)" check_eq $P/float-enc-n0 "f98000" "$(pv float_enc_n0)" check_eq $P/float-enc-p1 "f93c00" "$(pv float_enc_p1)" check_eq $P/float-enc-p1-1 "fb3ff199999999999a" "$(pv float_enc_p1_1)" check_eq $P/float-enc-p1-5 "f93e00" "$(pv float_enc_p1_5)" check_eq $P/float-enc-half-max "f97bff" "$(pv float_enc_half_max)" check_eq $P/float-enc-single-100000 "fa47c35000" "$(pv float_enc_single_100000)" check_eq $P/float-enc-single-max "fa7f7fffff" "$(pv float_enc_single_max)" check_eq $P/float-enc-double-1e300 "fb7e37e43c8800759c" "$(pv float_enc_double_1e300)" check_eq $P/float-enc-subnormal-half "f90001" "$(pv float_enc_subnormal_half)" check_eq $P/float-enc-half-min-normal "f90400" "$(pv float_enc_half_min_normal)" check_eq $P/float-enc-neg4 "f9c400" "$(pv float_enc_neg4)" check_eq $P/float-enc-neg4-1 "fbc010666666666666" "$(pv float_enc_neg4_1)" # ------------------------------------------------------------ and back again -- check_eq $P/float-rt-p0 "0.0" "$(pv float_rt_p0)" check_eq $P/float-rt-n0 "-0.0" "$(pv float_rt_n0)" check_eq $P/float-rt-p1 "1.0" "$(pv float_rt_p1)" check_eq $P/float-rt-p1-1 "1.1" "$(pv float_rt_p1_1)" check_eq $P/float-rt-p1-5 "1.5" "$(pv float_rt_p1_5)" check_eq $P/float-rt-half-max "65504.0" "$(pv float_rt_half_max)" check_eq $P/float-rt-single-100000 "100000.0" "$(pv float_rt_single_100000)" check_eq $P/float-rt-single-max "3.4028234663852886e+38" "$(pv float_rt_single_max)" check_eq $P/float-rt-double-1e300 "1e+300" "$(pv float_rt_double_1e300)" check_eq $P/float-rt-subnormal-half "5.960464477539063e-08" "$(pv float_rt_subnormal_half)" check_eq $P/float-rt-half-min-normal "6.103515625e-05" "$(pv float_rt_half_min_normal)" check_eq $P/float-rt-neg4 "-4.0" "$(pv float_rt_neg4)" check_eq $P/float-rt-neg4-1 "-4.1" "$(pv float_rt_neg4_1)" # -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. check_eq $P/float-neg-zero-sign "-1.0" "$(pv float_neg_zero_sign)" # ------------------------------ the three values with no ordinary comparison -- # NaN != NaN, so the NaN 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 exactly how a broken one gets through. check_eq $P/float-enc-inf "f97c00" "$(pv float_enc_inf)" check_eq $P/float-enc-ninf "f9fc00" "$(pv float_enc_ninf)" check_eq $P/float-enc-nan "f97e00" "$(pv float_enc_nan)" check_eq $P/float-rt-inf "inf" "$(pv float_rt_inf)" check_eq $P/float-rt-ninf "-inf" "$(pv float_rt_ninf)" check_eq $P/float-rt-nan-isnan "yes" "$(pv float_rt_nan_isnan)" check_eq $P/float-dec-half-nan "yes" "$(pv float_dec_half_nan)" check_eq $P/float-dec-half-inf "inf" "$(pv float_dec_half_inf)" # ------------------------------------------- major type 7: the simple values -- # true / false / null / undefined, and both simple-value widths. `undefined' # is cbor2's singleton for CBOR's 0xf7, which JSON has no counterpart for; it # is the one value here with no Python equivalent, so it is asserted by # IDENTITY (`is undefined') and not only by encoding. check_eq $P/simple-enc-true "f5" "$(pv simple_enc_true)" check_eq $P/simple-enc-false "f4" "$(pv simple_enc_false)" check_eq $P/simple-enc-null "f6" "$(pv simple_enc_null)" check_eq $P/simple-enc-undefined "f7" "$(pv simple_enc_undefined)" check_eq $P/simple-dec-true "True" "$(pv simple_dec_true)" check_eq $P/simple-dec-false "False" "$(pv simple_dec_false)" check_eq $P/simple-dec-null "None" "$(pv simple_dec_null)" check_eq $P/simple-dec-undefined-is "yes" "$(pv simple_dec_undefined_is)" check_eq $P/simple-enc-16 "f0" "$(pv simple_enc_16)" check_eq $P/simple-enc-255 "f8ff" "$(pv simple_enc_255)" check_eq $P/simple-dec-16 "CBORSimpleValue(value=16)" "$(pv simple_dec_16)" check_eq $P/simple-dec-255 "CBORSimpleValue(value=255)" "$(pv simple_dec_255)" # -------------------------------------------- major type 2/3: bytes and text -- # BYTE STRINGS AND TEXT STRINGS ARE DIFFERENT MAJOR TYPES, and a codec that # conflated them would still round-trip on ASCII. The three non-ASCII # fixtures are one per UTF-8 length above one -- U+00FC two bytes, U+6C34 # three, U+10151 four -- and they separate "encodes UTF-8" from "encodes # latin-1 and gets away with it on the a-z corpus". # # THE DECODED TEXT IS REPORTED AS CODEPOINTS, not as text. This image's guest # runs under the C locale; a case that printed U+6C34 would be measuring the # guest's stdout encoding rather than cbor2. check_eq $P/bstr-enc-empty "40" "$(pv bstr_enc_empty)" check_eq $P/bstr-enc-4 "4401020304" "$(pv bstr_enc_4)" check_eq $P/bstr-enc-24 "5818000000000000000000000000000000000000000000000000" "$(pv bstr_enc_24)" check_eq $P/tstr-enc-empty "60" "$(pv tstr_enc_empty)" check_eq $P/tstr-enc-a "6161" "$(pv tstr_enc_a)" check_eq $P/tstr-enc-ietf "6449455446" "$(pv tstr_enc_ietf)" check_eq $P/tstr-enc-escapes "62225c" "$(pv tstr_enc_escapes)" check_eq $P/tstr-enc-u00fc "62c3bc" "$(pv tstr_enc_u00fc)" check_eq $P/tstr-enc-u6c34 "63e6b0b4" "$(pv tstr_enc_u6c34)" check_eq $P/tstr-enc-u10151 "64f0908591" "$(pv tstr_enc_u10151)" check_eq $P/bstr-dec-4 "01020304" "$(pv bstr_dec_4)" check_eq $P/tstr-dec-ietf "'IETF'" "$(pv tstr_dec_ietf)" check_eq $P/tstr-dec-u00fc "252" "$(pv tstr_dec_u00fc)" check_eq $P/tstr-dec-u6c34 "27700" "$(pv tstr_dec_u6c34)" check_eq $P/tstr-dec-u10151 "65873" "$(pv tstr_dec_u10151)" check_eq $P/tstr-dec-type "str" "$(pv tstr_dec_type)" check_eq $P/bstr-dec-type "bytes" "$(pv bstr_dec_type)" # --------------------------------- a definite-length string past 65535 bytes -- # The input 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 for # the day the accelerator is reinstated -- not a reproduction. `tstr-long-prefix' # pins the 4-byte length prefix (7a 00011170) as well as the round-tripped # length, because a decoder that returned a short string would still satisfy # a length check on its own output. check_eq $P/tstr-long-rt-len "70000" "$(pv tstr_long_rt_len)" check_eq $P/tstr-long-prefix "7a00011170" "$(pv tstr_long_prefix)" check_eq $P/bstr-long-rt-len "70000" "$(pv bstr_long_rt_len)" check_eq $P/len-longer-bstr "CBORDecodeEOF" "$(pv len_longer_bstr)" check_eq $P/len-longer-tstr "CBORDecodeEOF" "$(pv len_longer_tstr)" check_eq $P/len-longer-array "CBORDecodeEOF" "$(pv len_longer_array)" check_eq $P/len-longer-map "CBORDecodeEOF" "$(pv len_longer_map)" # ------------------------------------------- major type 4/5: arrays and maps -- # 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-nesting' below measures on purpose. check_eq $P/array-enc-empty "80" "$(pv array_enc_empty)" check_eq $P/array-enc-123 "83010203" "$(pv array_enc_123)" check_eq $P/array-enc-nested "8301820203820405" "$(pv array_enc_nested)" check_eq $P/array-enc-25 "98190102030405060708090a0b0c0d0e0f101112131415161718181819" "$(pv array_enc_25)" check_eq $P/map-enc-empty "a0" "$(pv map_enc_empty)" check_eq $P/map-enc-int "a201020304" "$(pv map_enc_int)" check_eq $P/map-enc-mixed "a26161016162820203" "$(pv map_enc_mixed)" check_eq $P/map-enc-in-array "826161a161626163" "$(pv map_enc_in_array)" check_eq $P/map-enc-5 "a56161614161626142616361436164614461656145" "$(pv map_enc_5)" check_eq $P/array-dec-nested "[1, [2, 3], [4, 5]]" "$(pv array_dec_nested)" check_eq $P/map-dec-mixed "{'a': 1, 'b': [2, 3]}" "$(pv map_dec_mixed)" check_eq $P/map-dec-key-type "int" "$(pv map_dec_key_type)" check_eq $P/nest10-enc "8181818181818181818100" "$(pv nest10_enc)" check_eq $P/nest10-rt "[[[[[[[[[[0]]]]]]]]]]" "$(pv nest10_rt)" # ----------------------------------------------- the indefinite-length forms -- # 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 Appendix A # gives a literal for each: chunked byte and text strings, unbounded arrays # and maps, and the three mixed forms where one nesting level is unbounded # and its neighbours are not. # # `indef-tstr-len' asserts the LENGTH as well as the text: the chunk # boundaries are transport and must not survive into the value, and a decoder # that returned only the first chunk would still return a str. check_eq $P/indef-bstr-dec "0102030405" "$(pv indef_bstr_dec)" check_eq $P/indef-tstr-dec "'streaming'" "$(pv indef_tstr_dec)" check_eq $P/indef-array-empty-dec "[]" "$(pv indef_array_empty_dec)" check_eq $P/indef-array-dec "[1, [2, 3], [4, 5]]" "$(pv indef_array_dec)" check_eq $P/indef-array-mixed-dec "[1, [2, 3], [4, 5]]" "$(pv indef_array_mixed_dec)" check_eq $P/indef-array-tail-dec "[1, [2, 3], [4, 5]]" "$(pv indef_array_tail_dec)" check_eq $P/indef-array-head-dec "[1, [2, 3], [4, 5]]" "$(pv indef_array_head_dec)" check_eq $P/indef-array-25-dec "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]" "$(pv indef_array_25_dec)" check_eq $P/indef-map-dec "{'a': 1, 'b': [2, 3]}" "$(pv indef_map_dec)" check_eq $P/indef-map-in-array-dec "['a', {'b': 'c'}]" "$(pv indef_map_in_array_dec)" check_eq $P/indef-map-empty-dec "{}" "$(pv indef_map_empty_dec)" check_eq $P/indef-tstr-len "9" "$(pv indef_tstr_len)" # ------------------------------------------------------------------ the tags -- # TAG 0 AND TAG 1 ARE THE SAME INSTANT IN TWO ENCODINGS -- a date/time STRING # and an epoch NUMBER -- and both decode to an aware datetime. # `tag0-tag1-same-instant' is the case a suite testing each tag alone would # miss. These are also the only cases that reach calendar.timegm, which is # what python3-datetime is in the Depends line for. # # 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. check_eq $P/tag0-enc "c074323031332d30332d32315432303a30343a30305a" "$(pv tag0_enc)" check_eq $P/tag1-enc "c11a514b67b0" "$(pv tag1_enc)" check_eq $P/tag0-dec "2013-03-21T20:04:00+00:00" "$(pv tag0_dec)" check_eq $P/tag1-dec "2013-03-21T20:04:00+00:00" "$(pv tag1_dec)" check_eq $P/tag1-frac-dec "2013-03-21T20:04:00.500000+00:00" "$(pv tag1_frac_dec)" check_eq $P/tag0-dec-type "datetime" "$(pv tag0_dec_type)" check_eq $P/tag1-dec-tzaware "yes" "$(pv tag1_dec_tzaware)" check_eq $P/tag0-tag1-same-instant "yes" "$(pv tag0_tag1_same_instant)" check_eq $P/tag-unknown-dec "CBORTag(23, b'\\x01\\x02\\x03\\x04')" "$(pv tag_unknown_dec)" check_eq $P/tag24-dec "CBORTag(24, b'dIETF')" "$(pv tag24_dec)" check_eq $P/tag32-dec "CBORTag(32, 'http://www.example.com')" "$(pv tag32_dec)" check_eq $P/tag-roundtrip "d917706178" "$(pv tag_roundtrip)" check_eq $P/tag-roundtrip-back "CBORTag(6000, 'x')" "$(pv tag_roundtrip_back)" # ----------------------------------------- tags needing an undeclared module -- # 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 and # python3-datetime. The C halves ARE linked into python3.12 (PyInit__decimal, # PyInit__uuid); it is the .py wrapper a package has to ship. # # So BOTH answers are correct and which one this image gives is decided by # whether some other EMCRAFT_TOOLS member pulled the sub-package in. The probe # normalises that: `ok' means either the module was absent and the decode failed # on exactly that import, or it was present and the tag decoded to the value the # RFC gives. Anything else is a red carrying its own detail. check_eq $P/tag4-verdict "ok" "$(pv tag4_verdict)" check_eq $P/tag5-verdict "ok" "$(pv tag5_verdict)" check_eq $P/tag37-verdict "ok" "$(pv tag37_verdict)" # WHICH ARM THIS IMAGE TOOK is recorded rather than asserted against one value, # because either is a correct image. A third answer would mean the probe could # not tell, which IS a red. for _o in decimal uuid; do case "$(pv ${_o}_module)" in present|absent) bst_ok $P/optional-module-$_o ;; *) bst_notok $P/optional-module-$_o "neither present nor absent: $(pv ${_o}_module)" ;; esac done # -------------------------------------------- the self-referential structure -- # THE ENCODER MUST EITHER HANDLE IT BY THE DOCUMENTED RULE OR REJECT IT, and # cbor2 does both, selected by `value_sharing'. By default it REJECTS, with # CBOREncodeValueError -- 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: tag 28 (shareable) around the container, tag # 29 (sharedref) carrying the index for the back-reference. # # THE DECODE SIDE IS PINNED AS MEASURED, NOT AS ASSUMED. Upstream's own # vector for a ONE-element cycle, d81c81d81d00, comes back with the reference # resolved (`selfref-cycle1-rt'). This encoder's own output for [1, 2, ] # does NOT: the third element comes back an int. Both reproduce identically on # the x86-64 control at the same version pair, with and without the CVE # patches, so neither is about wasm32-hwjs and neither is this port's to fix. check_eq $P/selfref-default "CBOREncodeValueError: cyclic data structure detected but value sharing is disabled" "$(pv selfref_default)" check_eq $P/selfref-shared-enc "d81c830102d81d00" "$(pv selfref_shared_enc)" check_eq $P/selfref-cycle1-rt "yes" "$(pv selfref_cycle1_rt)" check_eq $P/selfref-cycle3-rt "int|resolved=no" "$(pv selfref_cycle3_rt)" # ---------------------------------------------------- the decoder's refusals -- # 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 # 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. # # THE THREE `len-absurd-*' CASES ARE THE MEMORY ONES. Each announces 2**64-1 # bytes over two bytes of data; the property is that the decoder refuses from # the stream it actually has rather than pre-allocating on the declared # length, which on this guest would take the whole heap. They are also why # this suite's timeout is what it is. check_eq $P/trunc-empty "CBORDecodeEOF" "$(pv trunc_empty)" check_eq $P/trunc-uint16 "CBORDecodeEOF" "$(pv trunc_uint16)" check_eq $P/trunc-uint64 "CBORDecodeEOF" "$(pv trunc_uint64)" check_eq $P/len-longer-bstr "CBORDecodeEOF" "$(pv len_longer_bstr)" check_eq $P/len-longer-tstr "CBORDecodeEOF" "$(pv len_longer_tstr)" check_eq $P/len-longer-array "CBORDecodeEOF" "$(pv len_longer_array)" check_eq $P/len-longer-map "CBORDecodeEOF" "$(pv len_longer_map)" check_eq $P/indef-unterminated "CBORDecodeEOF" "$(pv indef_unterminated)" check_eq $P/eof-is-decode-error "yes" "$(pv eof_is_decode_error)" check_eq $P/value-err-is-decode-error "yes" "$(pv value_err_is_decode_error)" check_eq $P/len-absurd-bstr "CBORDecodeValueError" "$(pv len_absurd_bstr)" check_eq $P/len-absurd-tstr "CBORDecodeValueError" "$(pv len_absurd_tstr)" check_eq $P/len-absurd-array "CBORDecodeValueError" "$(pv len_absurd_array)" # --------------------------------- deep nesting, and what it actually raises -- # RecursionError, NOT CBORDecodeError, and both halves are pinned. cbor2 has # no configurable maximum depth; the pure-Python decoder recurses in Python # per level, so the bound is sys.getrecursionlimit(). The probe LOWERS that # limit to 200 and feeds 400 levels, deliberately: 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. check_eq $P/deep-nesting "RecursionError|cbor_decode_error=no" "$(pv deep_nesting)" # ---------------- CVE-2025-68131: shared state does not leak between decodes -- # The patch's own property, on the half of the codec this image ships: # references registered during one TOP-LEVEL decode must not be visible to # the next one on the SAME decoder. A bare tag 29 fed after a decode that # registered a shareable must FAIL rather than resolve to the previous # decode's object. check_eq $P/shared-state-cleared "CBORDecodeValueError" "$(pv shared_state_cleared)" # ------------------------------ CVE-2026-26209: read_size is back on the API -- # The patch restores read_size=1 as the default -- so a caller can still read # the stream position after a decode -- and adds the parameter to load() and # loads(). The default AND that an explicit buffered value decodes # identically is the whole of what the pure-Python side of it changed. check_eq $P/read-size-default "1" "$(pv read_size_default)" check_eq $P/read-size-explicit "[1, 2, 3]" "$(pv read_size_explicit)" # ----------------------------------------- canonical form versus the default -- # THE 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 -- LENGTH first, then bytewise, so a one-character key sorts # before a two-character one whatever the letters are. Both are correct CBOR # and Appendix A's literals are the canonical ones. check_eq $P/default-float-1 "fb3ff0000000000000" "$(pv default_float_1)" check_eq $P/canonical-float-1 "f93c00" "$(pv canonical_float_1)" check_eq $P/default-map-order "a2616201616102" "$(pv default_map_order)" check_eq $P/canonical-map-order "a2616102616201" "$(pv canonical_map_order)" check_eq $P/canonical-key-len-first "a261620262616101" "$(pv canonical_key_len_first)" # ------------------------------------------------------ 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. check_eq $P/stream-dump-matches "yes" "$(pv stream_dump_matches)" check_eq $P/stream-load-matches "[1, 2, 3]" "$(pv stream_load_matches)" check_eq $P/stream-load-position "[1, 2, 3]|'IETF'" "$(pv stream_load_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, `tag_hook' for a tag # the decoder does not know, and `object_hook' for every map. Without these a # caller cannot extend the format at all, and a port that lost them would # still pass every case above. `encode-no-hook-raises' is the other half: # with no hook the same value must raise rather than be silently dropped. check_eq $P/encode-default-hook "d90fa0820102" "$(pv encode_default_hook)" check_eq $P/encode-no-hook-raises "CBOREncodeTypeError" "$(pv encode_no_hook_raises)" check_eq $P/decode-tag-hook "'point1,2'" "$(pv decode_tag_hook)" check_eq $P/decode-object-hook "'map-of-2'" "$(pv decode_object_hook)" # -------------------------------------------------------- immutable map 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, 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. check_eq $P/key-map-type "FrozenDict" "$(pv key_map_type)" check_eq $P/key-array-type "tuple" "$(pv key_array_type)" check_eq $P/frozendict-hashable "yes" "$(pv frozendict_hashable)" # --------------------------------------------------------------- the CLI ------ # THE SHIPPED CONSOLE SCRIPT, REACHED THROUGH ITS OWN SHEBANG. /usr/bin/cbor2 is # a `#!/usr/bin/env python3\' wrapper around cbor2.tool:main, so these two cases # measure something the probe structurally cannot: that the entry point is # installed, executable, and resolves its own import of the package. That is a # different failure mode from the module being importable -- a package can ship a # working module and a script whose shebang or sys.path is wrong. # # THE INPUT ARRIVES BASE64 ON STDIN, THROUGH A PIPE. `-d\' is the tool\'s own # base64 mode; using it keeps this suite free of a binary fixture, and a PIPE # rather than a redirect keeps it clear of the dup2(2)-returns-513 trap that a # stdin redirect from the virtiofs root falls into (see bst_local). # # gwECAw== is 83010203, the RFC\'s [1, 2, 3]. # omFiAWFhAg== is a2616201616102, {"b": 1, "a": 2} in insertion order -- so # `-k\' has something to reorder and the case says whether it did. check_out $P/cli-decode "[1, 2, 3]" \ sh -c 'echo gwECAw== | cbor2 -d' check_out $P/cli-sort-keys '{"a": 2, "b": 1}' \ sh -c 'echo omFiAWFhAg== | cbor2 -d -k' rm -rf "$W"