#!/bin/sh set -eu : "${PRE15_DUT:?PRE15_DUT is required}" : "${PRE15_ROOT:?PRE15_ROOT is required}" : "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}" : "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}" : "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}" . "$PRE15_LIB_DIR/runner.sh" fixture_dir=$PRE15_DUT/tests/pre15/fixtures spec=$fixture_dir/B28-partial.json generator=$fixture_dir/B28-partial-fixtures.py probe=$fixture_dir/B28-partial-probe.c kld_builder=$fixture_dir/B28-build-kld.sh gate_script=$PRE15_DUT/tests/pre15/gates/P15-086.sh gate_input=$PRE15_DUT/tests/pre15/gates/P15-086-input.json gate_result=$PRE15_ROOT/planning/pre15/evidence/20260815T154915Z-G04-G05-P15-086/gate-output/result.json gate_capabilities=$PRE15_ROOT/planning/pre15/evidence/20260815T154915Z-G04-G05-P15-086/gate-output/authorized-capabilities.json artifacts=$PRE15_RUN_DIR/artifacts first=$PRE15_CASE_TMP/fixtures-first second=$PRE15_CASE_TMP/fixtures-second action=${1:-runtime} case "$action" in runtime|k2) ;; *) pre15_fail_usage "unknown B28 action: $action" ;; esac if test "$action" = k2; then for tool in awk clang cmp diff file nm sha256sum; do command -v "$tool" >/dev/null 2>&1 || \ pre15_infra_blocked "missing B28 K2 tool: $tool" done pre15_record_fixture b28-case "$PRE15_DUT/tests/pre15/cases/B28-partial.sh" pre15_record_fixture b28-kld-builder "$kld_builder" for source in compress.h decompressor.c decompressor_lz4.c \ decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do pre15_record_fixture "b28-$source" "$PRE15_DUT/src/$source" done mkdir -p "$artifacts" for config in 0 1; do module=$PRE15_CASE_TMP/B28-zstdio$config.ko if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \ "$PRE15_FREEBSD_SRC" "$module" \ "$PRE15_CASE_TMP/kld-work-$config" "$config" \ >"$artifacts/B28-build-zstdio$config.stdout" \ 2>"$artifacts/B28-build-zstdio$config.stderr"; then pre15_dut_fail "B28 zstdio$config cross-target KLD build failed" fi file "$module" >"$artifacts/B28-zstdio$config-file.txt" sha256sum "$module" >"$artifacts/B28-zstdio$config-sha256.txt" nm -g "$module" | LC_ALL=C sort \ >"$artifacts/B28-zstdio$config-nm-global.txt" nm -u "$module" | LC_ALL=C sort \ >"$artifacts/B28-zstdio$config-nm-u.txt" done awk '$1 != "U" { print $NF }' "$artifacts/B28-zstdio0-nm-global.txt" \ >"$PRE15_CASE_TMP/B28-zstdio0-global-names.txt" awk '$1 != "U" { print $NF }' "$artifacts/B28-zstdio1-nm-global.txt" \ >"$PRE15_CASE_TMP/B28-zstdio1-global-names.txt" if ! diff -u "$PRE15_CASE_TMP/B28-zstdio0-global-names.txt" \ "$PRE15_CASE_TMP/B28-zstdio1-global-names.txt" \ >"$artifacts/B28-config-nm-global.diff"; then pre15_dut_fail 'B28 K2 global symbols differ by ZSTDIO configuration' fi if ! grep -q ' z_erofs_decompress_supports_subextent$' \ "$artifacts/B28-zstdio0-nm-global.txt"; then pre15_dut_fail 'B28 K2 partial capability symbol is absent' fi pre15_target_reached printf '%s\n' \ 'B28 targeted FreeBSD 15 K2 PASS' \ 'zstdio0/zstdio1 link, defined-global parity, optional Zstd ABI, and partial capability symbol PASS' \ 'KLD and object files removed by owned case cleanup' exit 0 fi for tool in cc cmp diff file fsck.erofs git mkfs.erofs nm python3 sha256sum tar timeout; do command -v "$tool" >/dev/null 2>&1 || \ pre15_infra_blocked "missing B28 host tool: $tool" done pre15_record_fixture b28-case "$PRE15_DUT/tests/pre15/cases/B28-partial.sh" pre15_record_fixture b28-spec "$spec" pre15_record_fixture b28-generator "$generator" pre15_record_fixture b28-probe "$probe" pre15_record_fixture b28-kld-builder "$kld_builder" pre15_record_fixture b28-gate "$gate_script" pre15_record_fixture b28-gate-input "$gate_input" pre15_record_fixture b28-gate-result "$gate_result" pre15_record_fixture b28-gate-capabilities "$gate_capabilities" for source in compress.h decompressor.c decompressor_lz4.c \ decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do pre15_record_fixture "b28-$source" "$PRE15_DUT/src/$source" done mkdir -p "$artifacts" if ! timeout -k 5 120 python3 -B "$generator" --spec "$spec" \ --output "$first" >"$artifacts/generate-first.stdout" \ 2>"$artifacts/generate-first.stderr"; then pre15_runner_fail 'B28 first real EROFS fixture generation failed' fi if ! timeout -k 5 120 python3 -B "$generator" --spec "$spec" \ --output "$second" >"$artifacts/generate-second.stdout" \ 2>"$artifacts/generate-second.stderr"; then pre15_runner_fail 'B28 second real EROFS fixture generation failed' fi if ! diff -ru "$first" "$second" >"$artifacts/fixture-repeat.diff"; then pre15_runner_fail 'B28 real EROFS fixtures are not byte reproducible' fi cp "$first/fixture-index.json" "$artifacts/B28-fixture-index.json" if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then pre15_target_reached else if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$spec" "$gate_result" \ "$gate_capabilities" "$artifacts/B28-source-audit.json" \ "$PRE15_CASE_TMP/decision-harness.c" <<'PY' from __future__ import annotations import hashlib import json from pathlib import Path import re import subprocess import sys root = Path(sys.argv[1]) dut = Path(sys.argv[2]) spec_path = Path(sys.argv[3]) gate_result_path = Path(sys.argv[4]) gate_capabilities_path = Path(sys.argv[5]) report_path = Path(sys.argv[6]) harness_path = Path(sys.argv[7]) spec = json.loads(spec_path.read_text(encoding="ascii")) baseline = spec["baseline"] implementation = "7a0d7e4a4047ac6d6130a3be5cb677cb94979fe7" runtime_base = "d09924362eb29819bd393e1e86602eb38c7608c6" def fail(message: str) -> None: raise SystemExit(message) def sha256_path(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def committed(commit: str, name: str) -> str: completed = subprocess.run( ["git", "-C", str(root), "show", f"{commit}:repo-pre-15/src/{name}"], check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) if completed.returncode != 0: fail(f"cannot read B28 source {commit}:{name}: {completed.stderr.strip()}") return completed.stdout def function(source: str, name: str) -> str: marker = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE) if marker is None: fail(f"missing function: {name}") name_line = source.rfind("\n", 0, marker.start()) + 1 start = source.rfind("\n", 0, name_line - 1) + 1 brace = source.find("{", marker.end()) if brace < 0: fail(f"missing function body: {name}") depth = 0 state = "code" index = brace while index < len(source): char = source[index] following = source[index + 1] if index + 1 < len(source) else "" if state == "code": if char == "/" and following == "*": state = "block" index += 2 continue if char == "/" and following == "/": state = "line" index += 2 continue if char == '"': state = "string" elif char == "'": state = "character" elif char == "{": depth += 1 elif char == "}": depth -= 1 if depth == 0: return source[start : index + 1] elif state == "block" and char == "*" and following == "/": state = "code" index += 2 continue elif state == "line" and char == "\n": state = "code" elif state in {"string", "character"}: if char == "\\": index += 2 continue if (state == "string" and char == '"') or ( state == "character" and char == "'" ): state = "code" index += 1 fail(f"unterminated function: {name}") if ( spec.get("schema") != 1 or spec.get("batch") != "B28" or spec.get("candidate") != "P15-086" or spec.get("test") != "TC176-stream-runtime" ): fail("B28 fixture identity changed") for name, commit in ( ("baseline", baseline), ("implementation", implementation), ("runtime base", runtime_base), ): if subprocess.check_output( ["git", "-C", str(root), "rev-parse", commit], text=True ).strip() != commit: fail(f"B28 {name} identity changed") gate = json.loads(gate_result_path.read_text(encoding="ascii")) capabilities = json.loads(gate_capabilities_path.read_text(encoding="ascii")) if ( gate.get("status") != "GO" or gate.get("g04") != "GO" or gate.get("g05") != "GO" or gate.get("b28") != "AUTHORIZED" or gate.get("resolved_base") != "6bf5724619be70f805bbe7d1ba77dd70cdced69f" ): fail("frozen P15-086 gate no longer authorizes B28") if capabilities != { "full_fallback": ["zstd"], "partial": ["deflate", "lz4", "lzma"], "persistent_state_added": False, "schema": 1, }: fail("P15-086 per-codec authorization changed") for name, expected in ( ("gate script", spec["gate"]["script_sha256"]), ("gate input", spec["gate"]["input_sha256"]), ("gate result", spec["gate"]["result_sha256"]), ): path = { "gate script": dut / "tests/pre15/gates/P15-086.sh", "gate input": dut / "tests/pre15/gates/P15-086-input.json", "gate result": gate_result_path, }[name] if sha256_path(path) != expected: fail(f"{name} hash changed") source_names = ( "compress.h", "decompressor.c", "decompressor_lz4.c", "decompressor_lzma.c", "decompressor_deflate.c", "decompressor_zstd.c", "zdata.c", ) baseline_sources = {name: committed(baseline, name) for name in source_names} implemented_sources = {name: committed(implementation, name) for name in source_names} sources = { name: (dut / "src" / name).read_text(encoding="utf-8") for name in source_names } if implemented_sources["decompressor_lz4.c"] != baseline_sources["decompressor_lz4.c"]: fail("B28 changed the already-capable LZ4 backend implementation") if "bool supports_subextent;" not in implemented_sources["compress.h"]: fail("B28 descriptor capability is absent") if implemented_sources["compress.h"].count("z_erofs_decompress_supports_subextent") != 1: fail("B28 capability prototype count changed") if "bool supports_subextent;" not in sources["compress.h"]: fail("current tree lost the B28 descriptor capability") if sources["compress.h"].count("z_erofs_decompress_supports_subextent") != 1: fail("current B28 capability prototype count changed") def read_descriptor_decisions(source_set: dict[str, str]) -> dict[str, bool]: decisions = {} for codec in ("lzma", "deflate", "zstd"): value = re.search( rf"const struct z_erofs_decompressor z_erofs_{codec}_decomp = \{{.*?\.supports_subextent = ([01]),", source_set[f"decompressor_{codec}.c"], re.DOTALL, ) if value is None: fail(f"missing {codec} descriptor capability") decisions[codec] = value.group(1) == "1" lz4 = re.search( r"static const struct z_erofs_decompressor z_erofs_lz4_decomp = \{.*?\.supports_subextent = ([01]),", source_set["decompressor.c"], re.DOTALL, ) if lz4 is None: fail("missing LZ4 descriptor capability") decisions["lz4"] = lz4.group(1) == "1" return decisions implemented_decisions = read_descriptor_decisions(implemented_sources) if implemented_decisions != { "lz4": True, "lzma": True, "deflate": True, "zstd": False }: fail(f"B28 implementation decisions differ from G04/G05: {implemented_decisions}") descriptor_decisions = read_descriptor_decisions(sources) if descriptor_decisions != { "lz4": True, "lzma": True, "deflate": True, "zstd": False }: fail(f"current partial descriptor decisions changed: {descriptor_decisions}") expected_zstd = committed(runtime_base, "decompressor_zstd.c") if function(sources["decompressor_zstd.c"], "z_erofs_zstd_finish") != \ function(expected_zstd, "z_erofs_zstd_finish"): fail("current Zstd completion policy differs from the accepted transform") unchanged_zdata = ( "z_erofs_extent_cache_match", "z_erofs_extent_cache_copy", "z_erofs_extent_cache_publish", "z_erofs_extent_cache_init", "z_erofs_extent_cache_fini", "z_erofs_extent_cache_eligible", "z_erofs_read_data", "z_erofs_read_uio", ) baseline_zdata = baseline_sources["zdata.c"] for name in unchanged_zdata: if function(implemented_sources["zdata.c"], name) != function(baseline_zdata, name): fail(f"B28 changed unrelated zdata function {name}") implemented_decode_length = function(implemented_sources["zdata.c"], "z_erofs_decode_length") implemented_read_extent = function(implemented_sources["zdata.c"], "z_erofs_read_extent") implemented_do_read = function(implemented_sources["zdata.c"], "z_erofs_do_read") decode_length = function(sources["zdata.c"], "z_erofs_decode_length") decode_extent = function(sources["zdata.c"], "z_erofs_decode_extent") do_read = function(sources["zdata.c"], "z_erofs_do_read") if decode_length != implemented_decode_length: fail("current tree changed the B28 decode-length transform") if decode_extent[decode_extent.index("{") :] != implemented_read_extent[ implemented_read_extent.index("{") : ]: fail("current tree changed B28 provider or decoded-buffer ownership") for anchor in ( "z_erofs_decompress_supports_subextent(map)", "*partial = (map->m_flags & EROFS_MAP_PARTIAL_REF) != 0;", "*decoded_len = end;", "*partial = true;", ): if anchor not in implemented_decode_length: fail(f"B28 decode-length anchor missing: {anchor}") for anchor in ( "erofs_read_metadata(", "erofs_read_physical(", "erofs_put_metabuf(&buf);", "erofs_brelse(compressed);", "free(decoded, M_EROFS);", "*bufp = decoded;", ): if implemented_read_extent.count(anchor) != function(baseline_zdata, "z_erofs_read_extent").count(anchor): fail(f"B28 changed provider or cleanup count: {anchor}") if not ( implemented_do_read.index("z_erofs_extent_cache_copy") < implemented_do_read.index("z_erofs_decode_length") < implemented_do_read.index("z_erofs_read_extent") < implemented_do_read.index("cache_eligible && !partial") < implemented_do_read.index("z_erofs_extent_cache_publish") ): fail("B28 cache/decode/publication order changed") if "partial, &decoded" not in implemented_do_read: fail("B28 does not pass the selected partial mode to extent decode") if not ( do_read.index("z_erofs_decode_length") < do_read.index("cache_eligible = !partial") < do_read.index("z_erofs_decode_extent") ): fail("current tree no longer excludes partial decodes from cache admission") if "partial, &decoded" not in do_read: fail("current tree does not pass B28 partial mode to extent decode") changed = set( subprocess.check_output( [ "git", "-C", str(root), "diff-tree", "--no-commit-id", "--name-only", "-r", implementation, ], text=True, ).splitlines() ) expected_write_set = { "repo-pre-15/src/compress.h", "repo-pre-15/src/decompressor.c", "repo-pre-15/src/decompressor_deflate.c", "repo-pre-15/src/decompressor_lzma.c", "repo-pre-15/src/decompressor_zstd.c", "repo-pre-15/src/zdata.c", "repo-pre-15/tests/pre15/cases/B28-partial.sh", "repo-pre-15/tests/pre15/fixtures/B28-build-kld.sh", "repo-pre-15/tests/pre15/fixtures/B28-partial-fixtures.py", "repo-pre-15/tests/pre15/fixtures/B28-partial-probe.c", "repo-pre-15/tests/pre15/fixtures/B28-partial.json", } if changed != expected_write_set: fail(f"B28 exact write set differs: {sorted(changed ^ expected_write_set)}") if any(re.search(r"return\s*\(\s*-E[A-Z0-9_]+", source) for source in sources.values()): fail("negative Linux errno entered B28") harness = r'''#include #include #include #include #define EINTEGRITY 97 #define EOVERFLOW 84 #define EROFS_MAP_PARTIAL_REF 0x20 typedef uint64_t erofs_off_t; struct erofs_map_blocks { uint64_t m_llen; unsigned int m_flags; unsigned int m_algorithmformat; }; static bool z_erofs_decompress_supports_subextent(const struct erofs_map_blocks *map) { return map->m_algorithmformat <= 2; } ''' + decode_length + r''' static int failures; static void check(const char *name, struct erofs_map_blocks map, uint64_t off, size_t want, int expected_error, size_t expected_length, bool expected_partial) { size_t length = 0; bool partial = false; int error = z_erofs_decode_length(&map, off, want, &length, &partial); if (error != expected_error || (!error && (length != expected_length || partial != expected_partial))) { fprintf(stderr, "%s error=%d length=%zu partial=%d\n", name, error, length, partial); failures++; } } int main(void) { check("lz4 prefix", (struct erofs_map_blocks){1038906, 0, 0}, 0, 4096, 0, 4096, true); check("lzma middle", (struct erofs_map_blocks){151552, 0, 1}, 8192, 4096, 0, 12288, true); check("deflate cross", (struct erofs_map_blocks){22878, 0, 2}, 3584, 4096, 0, 7680, true); check("zstd prefix", (struct erofs_map_blocks){151552, 0, 3}, 0, 4096, 0, 151552, false); check("lzma tail", (struct erofs_map_blocks){151552, 0, 1}, 147456, 4096, 0, 151552, false); check("zstd partial ref", (struct erofs_map_blocks){151552, EROFS_MAP_PARTIAL_REF, 3}, 0, 4096, 0, 4096, true); check("shifted fallback", (struct erofs_map_blocks){65536, 0, 4}, 0, 4096, 0, 65536, false); check("bad range", (struct erofs_map_blocks){4096, 0, 0}, 4090, 16, EINTEGRITY, 0, false); return failures != 0; } ''' harness_path.write_text(harness, encoding="ascii") report = { "baseline": baseline, "batch": "B28", "cache_hit_before_decode": True, "candidate": "P15-086", "descriptor_decisions": descriptor_decisions, "frozen_gate_full_fallback": ["zstd"], "current_full_fallback": ["shifted", "interlaced", "unknown", "zstd"], "current_partial": ["deflate", "lz4", "lzma"], "gate": "GO", "implementation": implementation, "partial_cache_publication": False, "partial_reference_preserved": True, "persistent_state_added": False, "positive_errno": True, "provider_and_cleanup_unchanged": True, "runtime_base": runtime_base, "status": "PASS", "test": "TC176-stream-runtime", "write_set": sorted(changed), } report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii") print(json.dumps(report, indent=2, sort_keys=True)) PY then pre15_runner_fail 'B28 gate/source/write-set audit failed' fi if ! cc -std=c11 -Wall -Wextra -Werror "$PRE15_CASE_TMP/decision-harness.c" \ -o "$PRE15_CASE_TMP/decision-harness" \ >"$artifacts/decision-harness-build.stdout" \ 2>"$artifacts/decision-harness-build.stderr" || \ ! "$PRE15_CASE_TMP/decision-harness" \ >"$artifacts/decision-harness.stdout" \ 2>"$artifacts/decision-harness.stderr"; then pre15_dut_fail 'B28 extracted decode-length harness failed' fi cp "$gate_result" "$artifacts/P15-086-result.json" cp "$gate_capabilities" "$artifacts/P15-086-capabilities.json" sha256sum "$artifacts/P15-086-result.json" \ "$artifacts/P15-086-capabilities.json" \ "$artifacts/B28-fixture-index.json" \ "$artifacts/B28-source-audit.json" >"$artifacts/SHA256SUMS" fi pre15_target_reached if test "${PRE15_MODE:-host}" != qemu; then printf '%s\n' \ 'TC176 B28 host partial-subextent subset PASS' \ 'B28 LZ4/LZMA/Deflate partial policy and Zstd full fallback, cache exclusion, arithmetic, real fixtures, and cleanup PASS' \ 'QEMU runtime NOT_RUN in host mode' \ 'Full feature suite NOT_RUN' exit 0 fi for tool in awk clang scp; do command -v "$tool" >/dev/null 2>&1 || \ pre15_infra_blocked "missing B28 QEMU tool: $tool" done : "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}" : "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}" : "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}" : "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}" fixture_archive=$PRE15_CASE_TMP/B28-fixtures.tar.gz module=$PRE15_CASE_TMP/B28-erofs-zstdio1.ko if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \ "$PRE15_CASE_TMP/kld-work" 1 >"$artifacts/B28-kld-build.stdout" \ 2>"$artifacts/B28-kld-build.stderr"; then pre15_dut_fail 'B28 cross-target zstdio1 KLD build failed' fi pre15_record_module "$module" file "$module" >"$artifacts/B28-kld-file.txt" sha256sum "$module" >"$artifacts/B28-kld-sha256.txt" nm -u "$module" | LC_ALL=C sort >"$artifacts/B28-kld-nm-u.txt" tar -C "$first" -czf "$fixture_archive" . pre15_scp() { timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \ -o BatchMode=yes -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ -o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \ -i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \ "$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2" } pre15_guest_ssh_bounded() { timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" ssh -n -q \ -o BatchMode=yes -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ -o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \ -p "$PRE15_QEMU_SSH_PORT" \ "$PRE15_QEMU_SSH_USER@127.0.0.1" "$@" } if ! pre15_scp "$fixture_archive" /root/B28-fixtures.tar.gz || \ ! pre15_scp "$probe" /root/B28-partial-probe.c || \ ! pre15_scp "$module" /root/B28-erofs-zstdio1.ko; then pre15_infra_blocked 'could not transfer B28 module, probe, or fixtures' fi if ! pre15_guest_ssh_bounded \ 'rm -rf /root/B28-fixtures && mkdir /root/B28-fixtures && tar -xzf /root/B28-fixtures.tar.gz -C /root/B28-fixtures && cc -O2 -Wall -Wextra -Werror -o /root/B28-partial-probe /root/B28-partial-probe.c'; then pre15_infra_blocked 'could not prepare B28 guest fixtures/probe' fi if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then pre15_infra_blocked 'guest kernel already owns the erofs.1 interface' fi if ! pre15_guest_ssh_bounded kldload /root/B28-erofs-zstdio1.ko \ >"$artifacts/B28-kldload.stdout" 2>"$artifacts/B28-kldload.stderr"; then if grep -q 'module already loaded or in kernel' \ "$artifacts/B28-kldload.stderr"; then pre15_infra_blocked 'guest kernel already owns the erofs.1 interface' fi if grep -Eq 'link_elf|symbol|not defined' "$artifacts/B28-kldload.stderr"; then pre15_infra_blocked 'guest kernel lacks the planned ZSTDIO provider ABI' fi pre15_dut_fail 'B28 exact-source zstdio1 KLD failed to load' fi pre15_own_guest_kld B28-erofs-zstdio1.ko 'B28 exact-source KLD' attach_mount() { image=$1 label=$2 B28_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \ -f "/root/B28-fixtures/images/$image") || \ pre15_dut_fail "$label md attach failed" case "$B28_MD" in md[0-9]*) ;; *) pre15_runner_fail "unexpected B28 md unit: $B28_MD" ;; esac pre15_own_guest_md "$B28_MD" "$label md" B28_MOUNT=/mnt/pre15-b28-$label pre15_guest_ssh_bounded mkdir -p "$B28_MOUNT" pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B28_MD" "$B28_MOUNT" || \ pre15_dut_fail "$label mount failed" pre15_own_guest_mount "$B28_MOUNT" "$label mount" } valid_assertions=0 corrupt_range_assertions=0 full_corruption_assertions=0 while IFS='|' read -r codec source logical_offset logical_length decision; do reference=/root/B28-fixtures/sources/$source/payload.bin attach_mount "$codec-valid.erofs" "$codec-valid" for range in '0 4096' '3584 4096' '8192 4096'; do set -- $range offset=$((logical_offset + $1)) pre15_guest_ssh_bounded /root/B28-partial-probe range \ "$B28_MOUNT/payload.bin" "$reference" "$offset" "$2" || \ pre15_dut_fail "$codec valid subextent mismatch at $offset" valid_assertions=$((valid_assertions + 1)) done tail_offset=$((logical_offset + logical_length - 4096)) pre15_guest_ssh_bounded /root/B28-partial-probe range \ "$B28_MOUNT/payload.bin" "$reference" "$tail_offset" 4096 || \ pre15_dut_fail "$codec valid tail fallback mismatch" valid_assertions=$((valid_assertions + 1)) attach_mount "$codec-corrupt.erofs" "$codec-corrupt" if test "$decision" = GO; then pre15_guest_ssh_bounded /root/B28-partial-probe range \ "$B28_MOUNT/payload.bin" "$reference" "$logical_offset" 4096 || \ pre15_dut_fail "$codec range-before-corruption partial decode failed" else pre15_guest_ssh_bounded /root/B28-partial-probe range-errno \ "$B28_MOUNT/payload.bin" "$logical_offset" 4096 97 || \ pre15_dut_fail "$codec did not use exact full fallback" fi corrupt_range_assertions=$((corrupt_range_assertions + 1)) pre15_guest_ssh_bounded /root/B28-partial-probe full-errno \ "$B28_MOUNT/payload.bin" 97 || \ pre15_dut_fail "$codec full corruption did not return EINTEGRITY" full_corruption_assertions=$((full_corruption_assertions + 1)) done <<'EOF' deflate|stream|114326|22878|GO lz4|lz4|0|1038906|GO lzma|stream|0|151552|GO zstd|stream|0|151552|FULL_FALLBACK EOF if test "$valid_assertions" -ne 16 || \ test "$corrupt_range_assertions" -ne 4 || \ test "$full_corruption_assertions" -ne 4; then pre15_runner_fail "B28 assertion count mismatch: valid=$valid_assertions corrupt-range=$corrupt_range_assertions full-corruption=$full_corruption_assertions" fi pre15_guest_ssh_bounded dmesg >"$artifacts/B28-dmesg.txt" printf '%s\n' \ 'TC176 B28 QEMU partial-subextent subset PASS' \ 'valid-ranges=16 partial-before-corruption=4 full-corruption=4 errno=97' \ 'Full feature suite NOT_RUN'