. "$BST_LIB" # BST-TIMEOUT: 900 # ONE python3 start, and that start is the whole cost. # # Everything below either reads the filesystem/dpkg with # # shell builtins or greps the single probe run's output. # # 99-python3-pathspec carries 900 for the same shape and # # this suite does strictly less arithmetic than that one -- # # two validate passes and a file round trip, which is # # nothing beside a python3 start on this arm. # # 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-configobj 5.0.8 (meta-openembedded meta-python/recipes-devtools/python, # python3-configobj_5.0.8.bb, `inherit pypi setuptools3', carrying the recipe's # CVE-2023-26112.patch) -- AN INI READER, WRITER AND VALIDATOR on the wasm32-hwjs # glibc image. The distinguishing feature over an ordinary ini parser is the # CONFIGSPEC: a second file that declares each key's type and default, against # which a config is coerced and checked. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL, AND WHAT THE .so CASES ARE REALLY SAYING. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly). A module # carrying a compiled extension could therefore not be imported here -- not # "would be slower", could not be imported. configobj is pure Python, which is # the entire reason it ports: the sdist carries no .c and no .pyx. # # So `module-no-so' and `pkg-no-so' are not boilerplate. They are a REGRESSION # TRIPWIRE on a version bump: configobj grows no extension today, and the day a # release adds a compiled speedup these go red at INSTALL time rather than at # `import configobj' on somebody else's ticket three rounds later. The assertion # is made from BOTH sides because the two fail differently -- a find catches a # file the package does not declare, the dpkg list catches a declared file that # is not Python. # # ============================================================================ # THE TWO RDEPENDS ARE LOAD-BEARING, AND THEY GATE DIFFERENT HALVES. # ============================================================================ # # The recipe declares RDEPENDS python3-pprint and python3-six. Neither is # optional and neither degrades gracefully, because both are imported at module # scope: # # six -- configobj/__init__.py:22. A missing six fails `import configobj' # outright: the reader half does not come up at all. # pprint -- configobj/validate.py:166. A missing pprint fails # `import configobj.validate': the READER still works and only the # VALIDATOR half is gone. # # That asymmetry is why this suite imports the two halves separately below # instead of taking one `import configobj' as proof of both. A dropped # python3-pprint would leave every read case here green and take out only the # spec cases, which is exactly the failure a single import case would misreport # as "configobj is fine". # # ============================================================================ # THE CVE PATCH IS ASSERTED DIRECTLY, BECAUSE NOTHING ELSE HERE WOULD NOTICE. # ============================================================================ # # The recipe carries CVE-2023-26112.patch, which narrows the Validator's # check-parsing regex from `(.+?)\((.*)\)' to `([^\(\)]+?)\((.*)\)' -- a ReDoS # fix. It changes the meaning of NO valid check, so every behavioural case in # this file passes identically with the patch present or absent. `cve-func-re' # reads the compiled pattern off the installed Validator and is the ONLY thing # here that would go red if the patch stopped being applied. It is asserted for # that reason and no other. # # ============================================================================ # THE PORT ALSO OCCUPIES THE BARE NAME `validate' IN site-packages. # ============================================================================ # # setup.py declares PACKAGES = ['configobj', 'validate']: alongside the # configobj package it installs a top-level `validate' shim that re-exports # configobj.validate for backwards compatibility. That is a fact about the # NAMESPACE this image now has spoken for, so it is asserted rather than left # for a later port to collide with. The case that matters is `shim-same-class' # -- a second COPY of the module would give two Validator types that fail each # other's isinstance checks while every other case here still passed. # # ============================================================================ # EVERY EXPECTATION BELOW WAS MEASURED, NOT DERIVED. # ============================================================================ # # data/python3-configobj-probe.py writes a fixture pair into a scratch tree and # prints one key=value per line; the suite asserts on that output. Every value # was measured on an x86-64 control (CPython 3.10.12) against configobj 5.0.8 # unpacked from the same sdist this image's recipe builds -- sha256 # 6f704434a07dc4f4dc7c9a745172c1cad449feb548febd9f7fe362629c627a97, the recipe's # own SRC_URI checksum -- with CVE-2023-26112.patch applied, recorded # 2026-08-31 and kept in the ticket's design doc and test-plan section # BRINTOS-1911. They are not predictions and they are not read off the source. # # What makes the control transferable is that all of it is pure-Python and # content-determined: an ini parse and a type coercion depend on the file's # bytes and the spec's text, not on pointer width, byte order, the platform # string, the clock or hash randomisation. Nothing arch-dependent is asserted # here. The target interpreter is CPython 3.12 against the control's 3.10; no # value asserted is version-sensitive, and a difference would itself be the # finding. # # THE FIXTURE IS ONE CONFIG AND ONE SPEC, and each key earns its place: # # retries integer 3 a coercion, int # ratio float 0.25 a coercion, float # enabled boolean True a coercion, bool -- the one whose string # form is TRUTHY either way, so only a type # check can tell coerced from not coerced # hosts list alpha,beta,gamma a coercion to list, len 3 # name string brintos the control: stays str # absent integer (absent) NOT IN EITHER CONFIG FILE -- its value can # only have come from the spec's default # [server] url interpolated %(scheme)s://%(host)s/api must read back # RESOLVED # [[limits]] max_conn integer a key two levels down, so the spec is # proven to descend into nested sections # # WHY THE INTERPOLATED KEY REFERENCES ONLY STRING-TYPED KEYS. configobj resolves # interpolation at __getitem__ time, while validate() has already REPLACED the # referenced value with its coerced type. An interpolation naming an integer key # therefore raises TypeError out of the interpolation engine (`expected string # or bytes-like object') the moment the section is walked. That is upstream # configobj behaviour on x86-64 too, not a property of this arm and not a defect # of this port -- it is a fixture hazard, and the fixture avoids it by # interpolating `scheme' and `host', which the spec types as string. Named here # because a later editor adding %(port)s would break this suite in a way whose # traceback points at configobj rather than at the edit. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - The recipe's ptest package is not installed and not asserted: its # dependencies are not ported and BRINTOS-1911 rules them out of scope. # - configobj's `unrepr' mode and its ConfigParser-compatibility shims. The # ticket closes on the read-validate-write path. # - Any network or clock behaviour. configobj has none; the whole observable # surface here is a function of files in a scratch tree. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-configobj # THE CANARY, which asserts nothing about python3-configobj. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE # IS COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require' guard deliberately, # because a `require' that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages CO=$SITE/configobj VD=$SITE/validate DI=$SITE/configobj-5.0.8.dist-info PKGLIST=/var/lib/dpkg/info/python3-configobj.list D=$BST_DATA PROBE=$D/python3-configobj-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-configobj.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. The import cases further down are the functional # statement; these are the ones that say WHICH copy answered, so a module that # imported out of some other path is distinguishable from one that imported out # of the package this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-dir test -d "$CO" check $P/module-init test -f "$CO/__init__.py" # THE VALIDATE HALF, as a file. This is the ticket's "and its validate half": # the reader and the validator are two files in one package and a packaging # change could ship one without the other. check $P/module-validate test -f "$CO/validate.py" check $P/module-version test -f "$CO/_version.py" # The legacy top-level `validate' package -- see the header. Asserted as files # because it is a namespace this image now occupies. check $P/shim-dir test -d "$VD" check $P/shim-init test -f "$VD/__init__.py" # THE DIST-INFO DIRECTORY CARRIES THE PINNED VERSION IN ITS NAME. This is a # second, independent statement of the version -- from the installed layout # rather than from the module's __version__ -- and the two disagreeing would # mean a mixed install that no single case could otherwise see. check $P/dist-info-dir test -d "$DI" check $P/dist-info-record test -f "$DI/RECORD" check $P/dist-info-metadata test -f "$DI/METADATA" # NO COMPILED EXTENSION, ANYWHERE UNDER EITHER PACKAGE. See the header: on this # image a .so could not be loaded at all, so this is the case that fails a # version bump which grew one -- loudly, here, instead of at `import' later. check_eq $P/module-no-so "" \ "$(find "$CO" "$VD" -name '*.so' -o -name '*.so.*' 2>/dev/null)" # ...and the same assertion from the PACKAGE's side rather than the filesystem's. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS UNDER site-packages IS PYTHON OR ITS # METADATA. The two cases above name the failure that is expected; this one # catches the shape nobody predicted -- a binary, a .pyd, a shared archive -- # without having to enumerate what it might be. # # THE `-f' TEST IS LOAD-BEARING. A dpkg .list names DIRECTORIES too, and without # a trailing slash, so a name-only filter would report every directory in the # package as an unexpected shape and this case would be red on a CORRECT # package. _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # --------------------------------------------------------- the package ------ # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-configobj is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list. The # anchor is exact: an unanchored match would report a -dev or -ptest stanza as # this one. sed -n '/^Package: python3-configobj$/,/^$/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" check_out $P/pkg-version "5.0.8-r0" cat "$W/st" # THE TWO RUNTIME DEPENDENCIES THE RECIPE DECLARES, read off the installed # stanza. See the header: each gates a different half of the package, so they # are asserted separately -- a stanza that lost python3-pprint and kept # python3-six is a real and differently-broken image. check_out $P/pkg-depends-six "python3-six" cat "$W/st" check_out $P/pkg-depends-pprint "python3-pprint" cat "$W/st" # ---------------------------------------------------------- the interpreter -- # THE TWO IMPORTS, SEPARATELY, BEFORE THE PROBE. If configobj cannot be imported # then every probe case below reds at once and each names a different symptom; # these cases are what say the cause is an import, and WHICH import. Splitting # them is the header's asymmetry made mechanical: the first fails without six, # the second without pprint. check $P/import python3 -c 'import configobj' check $P/import-validate python3 -c 'from configobj.validate import Validator' check $P/import-shim python3 -c 'import validate' # ------------------------------------------------------------- the probe ----- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ONE interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 600 python3 "$PROBE" "$W" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the anchors keep `v_hosts' and # `v_hosts_len' distinguishable. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken parser, when what actually happened is that no measurement was taken. # The word `unscorable' is PARSED -- brintos_local_verify re-measures a suite # that prints it on a fresh guest rather than trusting a shared boot's numbers # (BRINTOS-1644) -- so it goes in this structural check, not a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # The probe caught its own import failure and said so, rather than every case # below reding with got:''. check_eq $P/probe-no-fatal "" "$(pv fatal)" # THE VERSION THE RECIPE PINS, read from the module rather than from dpkg. A # mismatch here means the recipe and what actually landed have gone out of step, # and every expectation in this file is then describing a different configobj. check_eq $P/version "5.0.8" "$(pv version)" # The module answered out of the package this image installed, not from a stray # copy on sys.path. This is what makes the payload cases above and the behaviour # cases below statements about the SAME configobj. check_eq $P/module-file "$CO/__init__.py" "$(pv module_file)" check_eq $P/validate-file "$CO/validate.py" "$(pv validate_file)" # ...and it answered out of a .py, not an extension. The third statement of the # .so negative, this one from inside the interpreter. check_eq $P/module-is-py "yes" "$(pv module_is_py)" # ------------------------------------------------------------- the API ------- check_eq $P/api-configobj "yes" "$(pv api_configobj)" check_eq $P/api-validator "yes" "$(pv api_validator)" check_eq $P/api-flatten-errors "yes" "$(pv api_flatten_errors)" # THE TWO DEPENDENCIES, RESOLVED FROM INSIDE THE INTERPRETER. The dpkg cases # above say the recipe DECLARED them; these say they actually import on target. # Asserted as booleans and not as paths -- six lives in site-packages here and # in dist-packages on the x86-64 control, which is a property of the distro # rather than of this port. check_eq $P/dep-six "yes" "$(pv dep_six)" check_eq $P/dep-pprint "yes" "$(pv dep_pprint)" # THE CVE-2023-26112 PATCH. See the header: this is the only case in the file # that would notice if the recipe's patch stopped being applied, because the fix # changes the meaning of no valid check. check_eq $P/cve-func-re '([^\(\)]+?)\((.*)\)' "$(pv func_re)" # THE LEGACY SHIM RESOLVES TO THE SAME CLASS, not to a second copy. Two copies # would give two Validator types that fail each other's isinstance checks while # every other case in this file still passed. check_eq $P/shim-same-class "yes" "$(pv shim_same_class)" # --------------------------------------------------- read + validate + coerce -- # The fixtures were written by the probe into its scratch tree. check_eq $P/fixture-config "yes" "$(pv fixture_config)" check_eq $P/fixture-spec "yes" "$(pv fixture_spec)" # VALIDATION SUCCEEDS, and succeeds as the literal True rather than as a # per-key dict. configobj returns a dict the moment ANY key fails, so # `validate-is-true' and `validate-result' together separate "all keys passed" # from "some passed". check_eq $P/validate-result "True" "$(pv validate_result)" check_eq $P/validate-is-true "yes" "$(pv validate_is_true)" # THE COERCIONS, BY TYPE. This is the ticket's requirement stated exactly: a # validator that returned the literal text would satisfy a string comparison of # the VALUE and fail here. `enabled' is the case that most needs it -- the # string "True" and the boolean True are both truthy, so only the type says # whether the spec did anything at all. check_eq $P/type-retries "int" "$(pv t_retries)" check_eq $P/type-ratio "float" "$(pv t_ratio)" check_eq $P/type-enabled "bool" "$(pv t_enabled)" check_eq $P/type-hosts "list" "$(pv t_hosts)" # The control: a key the spec types as string must STAY a str, so a validator # that coerced everything to something is caught here. check_eq $P/type-name "str" "$(pv t_name)" check_eq $P/type-port "int" "$(pv t_server_port)" check_eq $P/type-max-conn "int" "$(pv t_max_conn)" # ...and the values, so a coercion to the right TYPE carrying the wrong VALUE # is a distinct red from a coercion that did not happen. check_eq $P/value-retries "3" "$(pv v_retries)" check_eq $P/value-ratio "0.25" "$(pv v_ratio)" check_eq $P/value-enabled "True" "$(pv v_enabled)" check_eq $P/value-name "brintos" "$(pv v_name)" check_eq $P/value-hosts "alpha|beta|gamma" "$(pv v_hosts)" # The list's LENGTH, separately: a "list" that arrived as one string of three # comma-separated words has the right type and the wrong shape. check_eq $P/value-hosts-len "3" "$(pv v_hosts_len)" # THE NESTED SECTION. max_conn is two levels down, so these say the configspec # descended into subsections rather than validating only the root. check_eq $P/nested-is-section "yes" "$(pv nested_is_section)" check_eq $P/section-names "server" "$(pv section_names)" check_eq $P/subsection-names "limits" "$(pv server_subsections)" check_eq $P/value-server-host "example.net" "$(pv v_server_host)" check_eq $P/value-server-port "8080" "$(pv v_server_port)" check_eq $P/value-max-conn "50" "$(pv v_max_conn)" # THE DEFAULT FILL. `absent' appears in the spec and in NEITHER config file, so # its value can only have come from the spec's default -- and its type says the # default went through the same coercion the present keys did, rather than # arriving as the string "42". check_eq $P/default-value "42" "$(pv v_absent)" check_eq $P/default-type "int" "$(pv t_absent)" check_eq $P/default-recorded "yes" "$(pv absent_in_defaults)" # THE INTERPOLATION. Written as %(scheme)s://%(host)s/api and must read back # resolved; the raw form appearing here would mean interpolation was off, and a # partially-resolved form would mean one reference failed. check_eq $P/interpolation "http://example.net/api" "$(pv v_url)" # ------------------------------------------------------- the failure path --- # A config that violates the spec must FAIL validation and name EXACTLY the # offending key. "Returned false" on its own would not distinguish this from a # validator that rejected everything, which is why the count, the key and the # per-key verdicts are all asserted. check_eq $P/bad-is-true "no" "$(pv bad_is_true)" check_eq $P/bad-result-dict "yes" "$(pv bad_result_is_dict)" # The per-key verdicts inside that dict: the offending key False, an innocent # neighbour True. A validator that failed the whole file would fail this second # case and only this second case. check_eq $P/bad-key-false "False" "$(pv bad_result_retries)" check_eq $P/bad-other-true "True" "$(pv bad_result_ratio)" # EXACTLY ONE offending key, and it is the one the fixture broke. check_eq $P/bad-error-count "1" "$(pv bad_error_count)" check_eq $P/bad-offending "retries" "$(pv bad_offending_keys)" check_eq $P/bad-section "" "$(pv bad_offending_section)" # The same failure under preserve_errors=True, which replaces the bare False # with the exception the check raised. This is what says the value was rejected # for being the WRONG TYPE rather than false for some unnamed reason. check_eq $P/bad-pe-count "1" "$(pv bad_pe_count)" check_eq $P/bad-pe-key "retries" "$(pv bad_pe_keys)" check_eq $P/bad-pe-type "VdtTypeError" "$(pv bad_pe_type)" # ---------------------------------------------------------- write + reread --- # THE ROUND TRIP THE TICKET CLOSES ON. The validated object is written back and # re-read through the SAME spec, and the re-read must equal the validated # object. A writer that lost the nesting, flattened the list, or dropped the # default-filled key shows up here as an inequality rather than as a silently # different file on somebody's target. check_eq $P/write-file "yes" "$(pv written_file)" check_eq $P/reread-validates "True" "$(pv reread_validate_result)" check_eq $P/roundtrip-equal "yes" "$(pv roundtrip_equal)" # ...and the pieces individually, so an inequality above says WHAT differed. # The default-filled key is the one most likely to be lost: it was never in the # input file and only exists because the spec put it there. check_eq $P/roundtrip-type-retries "int" "$(pv rt_t_retries)" check_eq $P/roundtrip-hosts "alpha|beta|gamma" "$(pv rt_v_hosts)" check_eq $P/roundtrip-default "42" "$(pv rt_v_absent)" check_eq $P/roundtrip-nested "50" "$(pv rt_v_max_conn)" check_eq $P/roundtrip-url "http://example.net/api" "$(pv rt_v_url)" # The probe ran to the end. Every case above greps a key, so a probe that died # halfway would leave the remaining keys absent and each case would red with # got:'' -- this case is the one that says the cause was a truncated run. check_eq $P/probe-complete "yes" "$(pv done)" rm -rf "$W"