#!/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" baseline=e769e4ae32d4967c1f69067dc92628b60b62b648 fixture_dir=$PRE15_DUT/tests/pre15/fixtures spec=$fixture_dir/B21-super-spec.json generator=$fixture_dir/B21-super-generate.py oracle=$fixture_dir/B21-super-oracle.py qemu_probe=$fixture_dir/B21-qemu-probe.c kld_builder=$fixture_dir/B28-build-kld.sh artifacts=$PRE15_RUN_DIR/artifacts first=$PRE15_CASE_TMP/first second=$PRE15_CASE_TMP/second for tool in cmp fsck.erofs git mkfs.erofs python3 sha256sum; do command -v "$tool" >/dev/null 2>&1 || \ pre15_infra_blocked "missing B21 host tool: $tool" done pre15_record_fixture b21-spec "$spec" pre15_record_fixture b21-generator "$generator" pre15_record_fixture b21-oracle "$oracle" pre15_record_fixture b21-qemu-probe "$qemu_probe" pre15_record_fixture b21-kld-builder "$kld_builder" pre15_record_fixture b21-case "$PRE15_DUT/tests/pre15/cases/B21-super.sh" pre15_record_fixture b21-super "$PRE15_DUT/src/super.c" pre15_record_fixture b21-internal "$PRE15_DUT/src/internal.h" pre15_record_fixture b21-xattr "$PRE15_DUT/src/xattr.c" mkdir -p "$artifacts" if python3 -B "$generator" --spec "$spec" --output "$first" \ --work "$PRE15_CASE_TMP/first-work" \ >"$artifacts/generate-first.json" \ 2>"$artifacts/generate-first.stderr"; then : else pre15_runner_fail 'B21 first fixture generation failed' fi if python3 -B "$generator" --spec "$spec" --output "$second" \ --work "$PRE15_CASE_TMP/second-work" \ >"$artifacts/generate-second.json" \ 2>"$artifacts/generate-second.stderr"; then : else pre15_runner_fail 'B21 second fixture generation failed' fi if cmp "$first/fixture-index.json" "$second/fixture-index.json" && \ find "$first" -type f ! -name fixture-index.json -printf '%f\n' | sort | \ while IFS= read -r name; do cmp "$first/$name" "$second/$name" || exit 1; done then : else pre15_runner_fail 'B21 fixture generation is not byte reproducible' fi if python3 -B "$oracle" --fixtures "$first" \ --report "$artifacts/oracle-first.json" \ >"$artifacts/oracle-first.stdout" 2>"$artifacts/oracle-first.stderr" && \ python3 -B "$oracle" --fixtures "$second" \ --report "$artifacts/oracle-second.json" \ >"$artifacts/oracle-second.stdout" 2>"$artifacts/oracle-second.stderr" && \ cmp "$artifacts/oracle-first.json" "$artifacts/oracle-second.json" then : else pre15_runner_fail 'B21 independent oracle replay failed' fi if fsck.erofs -d0 "$first/legal-control.erofs" \ >"$artifacts/legal-fsck.stdout" 2>"$artifacts/legal-fsck.stderr"; then : else pre15_runner_fail 'B21 legal control is not accepted by fsck.erofs' fi cp "$first/fixture-index.json" "$artifacts/B21-fixture-index.json" pre15_record_fixture b21-generated-index "$artifacts/B21-fixture-index.json" pre15_target_reached if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then : else if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \ "$artifacts/B21-source-check.json" <<'PY' from __future__ import annotations import json from pathlib import Path import re import subprocess import sys root = Path(sys.argv[1]) dut = Path(sys.argv[2]) baseline = sys.argv[3] output = Path(sys.argv[4]) src = dut / "src" def committed(name: str) -> str: completed = subprocess.run( ["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{name}"], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if completed.returncode != 0: raise SystemExit(f"cannot read B21 baseline {name}: {completed.stderr}") return completed.stdout def replace_once(source: str, old: str, new: str, label: str) -> str: count = source.count(old) if count != 1: raise SystemExit(f"{label}: expected one transform source, found {count}") return source.replace(old, new, 1) def extract_function(source: str, name: str) -> str: match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE) if not match: raise SystemExit(f"missing function: {name}") name_line = source.rfind("\n", 0, match.start()) + 1 start = source.rfind("\n", 0, name_line - 1) + 1 brace = source.find("{", match.end()) 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 raise SystemExit(f"unterminated function: {name}") def require_order(source: str, markers: list[str], label: str) -> None: position = -1 for marker in markers: position = source.find(marker, position + 1) if position < 0: raise SystemExit(f"{label} is missing ordered marker: {marker}") current = { name: (src / name).read_text(encoding="utf-8") for name in ("internal.h", "super.c", "xattr.c") } base = {name: committed(name) for name in current} if current["internal.h"] != base["internal.h"]: raise SystemExit("B21 changed internal.h despite all required helpers existing") for helper in ( "EROFS_FEATURE_FUNCS(fragments, incompat, INCOMPAT_FRAGMENTS)", "EROFS_FEATURE_FUNCS(sb_chksum, compat, COMPAT_SB_CHKSUM)", "EROFS_FEATURE_FUNCS(plain_xattr_pfx, compat, COMPAT_PLAIN_XATTR_PFX)", ): if current["internal.h"].count(helper) != 1: raise SystemExit(f"B21 feature helper is missing or duplicated: {helper}") expected_super = replace_once( base["super.c"], "\tif ((le32toh(dsb->feature_compat) & EROFS_FEATURE_COMPAT_SB_CHKSUM) == 0)\n", "\tif (!erofs_sb_has_sb_chksum(sbi))\n", "checksum helper", ) expected_super = replace_once( expected_super, "\tif ((sbi->feature_incompat & EROFS_FEATURE_INCOMPAT_FRAGMENTS) != 0 &&\n" "\t sbi->packed_nid > 0) {\n", "\tif (erofs_sb_has_fragments(sbi) && sbi->packed_nid > 0) {\n", "fragments helper", ) old_read = extract_function(expected_super, "erofs_read_superblock") new_read = extract_function(current["super.c"], "erofs_read_superblock") expected_read = replace_once( old_read, "\tif (dsb->dirblkbits != 0)\n" "\t\treturn (EOPNOTSUPP);\n" "\tsbi->feature_compat = le32toh(dsb->feature_compat);\n", "\tsbi->blkszbits = dsb->blkszbits;\n" "\tsbi->block_size = 1u << sbi->blkszbits;\n" "\tsbi->feature_compat = le32toh(dsb->feature_compat);\n" "\terror = erofs_superblock_csum_verify(sbi, dsb);\n" "\tif (error != 0)\n" "\t\treturn (error);\n\n" "\tif (dsb->dirblkbits != 0)\n" "\t\treturn (EOPNOTSUPP);\n", "checksum trust order", ) dead_exception = """\t/* \t * Narrowly allow one extra combination: long xattr prefixes enabled \t * with non-plain prefix table stored in a packed inode, which adds \t * the FRAGMENTS (0x20) incompat bit. This is NOT a declaration of \t * general fragments support; per-inode data layout is still gated \t * by plain/inline checks in erofs_read_inode(). \t */ \tif (unsupported != 0) { \t\tif (unsupported != EROFS_FEATURE_INCOMPAT_FRAGMENTS || \t\t (sbi->feature_incompat & \t\t\tEROFS_FEATURE_INCOMPAT_XATTR_PREFIXES) == 0 || \t\t (sbi->feature_compat & \t\t\tEROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX) != 0 || \t\t sbi->packed_nid == 0) \t\t\treturn (EOPNOTSUPP); \t} \tsbi->blkszbits = dsb->blkszbits; \tsbi->block_size = 1u << sbi->blkszbits; """ expected_read = replace_once( expected_read, dead_exception, "\tif (unsupported != 0)\n\t\treturn (EOPNOTSUPP);\n", "dead fragments exception", ) old_checksum_site = ( "\terror = erofs_superblock_csum_verify(sbi, dsb);\n" "\tif (error != 0)\n" "\t\treturn (error);\n" ) if expected_read.count(old_checksum_site) != 2: raise SystemExit("checksum trust-order transform did not produce two sites") old_checksum_offset = expected_read.rfind(old_checksum_site) expected_read = ( expected_read[:old_checksum_offset] + expected_read[old_checksum_offset + len(old_checksum_site) :] ) if new_read != expected_read: raise SystemExit("erofs_read_superblock differs from exact B21 transforms") expected_super = expected_super.replace(old_read, expected_read, 1) if current["super.c"] != expected_super: raise SystemExit("super.c changed outside exact B21 transforms") expected_xattr = replace_once( base["xattr.c"], "\tif ((sbi->feature_incompat & EROFS_FEATURE_INCOMPAT_XATTR_PREFIXES) ==\n" "\t\t0 ||\n" "\t sbi->xattr_prefix_count == 0)\n", "\tif (!erofs_sb_has_xattr_prefixes(sbi) || sbi->xattr_prefix_count == 0)\n", "xattr-prefix helper", ) expected_xattr = replace_once( expected_xattr, "\tif ((sbi->feature_compat & EROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX) == 0) {\n", "\tif (!erofs_sb_has_plain_xattr_pfx(sbi)) {\n", "plain-prefix helper", ) if current["xattr.c"] != expected_xattr: raise SystemExit("xattr.c changed outside exact B21 helper conversions") read_super = new_read checksum_call = read_super.index("erofs_superblock_csum_verify(sbi, dsb)") before_checksum = read_super[:checksum_call] for protected in ( "dirblkbits", "feature_incompat", "packed_nid", "extra_devices", "sb_extslots", "meta_blkaddr", "xattr_blkaddr", "xattr_prefix_start", "xattr_prefix_count", "blocks_root", ): if protected in before_checksum: raise SystemExit(f"protected field used before checksum: {protected}") require_order( read_super, [ "dsb->magic", "dsb->blkszbits < 9", "sbi->feature_compat = le32toh(dsb->feature_compat)", "erofs_superblock_csum_verify(sbi, dsb)", "dsb->dirblkbits", "sbi->feature_incompat = le32toh(dsb->feature_incompat)", "unsupported = sbi->feature_incompat", "sbi->sb_size = 128 + dsb->sb_extslots", "sbi->xattr_prefix_start = le32toh(dsb->xattr_prefix_start)", "erofs_validate_device_size", "erofs_load_generation_seed", "z_erofs_parse_cfgs", ], "superblock trust order", ) mountfs = extract_function(current["super.c"], "erofs_mountfs") require_order( mountfs, [ "erofs_read_superblock", "erofs_scan_devices", "erofs_init_packed_inode", "erofs_init_metabox_inode", "erofs_read_inode(sbi, sbi->root_nid", "erofs_xattr_prefixes_init", "mp->mnt_data = sbi", ], "FreeBSD mount publication", ) if "fail:\n\terofs_sb_free(sbi);\n\treturn (error);" not in mountfs: raise SystemExit("mount failure no longer funnels through erofs_sb_free") sb_free = extract_function(current["super.c"], "erofs_sb_free") require_order( sb_free, [ "z_erofs_extent_cache_fini", "erofs_xattr_prefixes_cleanup", "erofs_drop_internal_inodes", "erofs_free_dev_context", "erofs_release_device_info(&sbi->dif0)", "free(sbi, M_EROFS)", ], "mount cleanup", ) release = extract_function(current["super.c"], "erofs_release_device_info") require_order( release, ["g_topology_lock", "g_vfs_close", "g_topology_unlock", "vrele", "dev_rel"], "GEOM release", ) if any("return (-E" in current[name] for name in current): raise SystemExit("negative errno entered the FreeBSD B21 write set") result = { "baseline": baseline, "checksum_before_protected_fields": True, "dead_fragments_exception_removed": True, "feature_helpers": True, "geom_release_preserved": True, "internal_h_unchanged": True, "mount_cleanup_preserved": True, "mount_publication_preserved": True, "positive_errno_preserved": True, "status": "PASS", } output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii") print("B21 source: exact trust-order and feature-helper transforms present") PY then : else pre15_dut_fail 'B21 source contract failed' fi fi if python3 - "$artifacts/B21-fixture-index.json" \ "$artifacts/oracle-first.json" "$artifacts/B21-matrix.tsv" <<'PY' import json from pathlib import Path import sys index = json.loads(Path(sys.argv[1]).read_text(encoding="ascii")) oracle = json.loads(Path(sys.argv[2]).read_text(encoding="ascii")) if index["case_count"] != 13 or index["legal_count"] != 1 or index["damaged_count"] != 12: raise SystemExit("B21 fixture cardinality changed") if oracle["status"] != "PASS" or oracle["passed_count"] != 13: raise SystemExit("B21 oracle matrix is incomplete") lines = ["id\tauthenticated\terrno\treject\tsha256"] for fixture, result in zip(index["cases"], oracle["results"], strict=True): lines.append( f"{fixture['id']}\t{str(fixture['authenticated']).lower()}\t" f"{result['actual_errno']}\t{result['actual_reject']}\t{fixture['sha256']}" ) Path(sys.argv[3]).write_text("\n".join(lines) + "\n", encoding="ascii") print( f"B21 fixtures: {index['case_count']} " f"({index['legal_count']} legal, {index['damaged_count']} damaged)" ) PY then : else pre15_runner_fail 'B21 fixture matrix summary failed' fi if test "${PRE15_MODE:-host}" != qemu; then printf '%s\n' \ 'B21 super gate: host PASS' \ 'TC172 QEMU runtime NOT_RUN in host mode' exit 0 fi for tool in awk clang file nm scp tar timeout; do command -v "$tool" >/dev/null 2>&1 || \ pre15_infra_blocked "missing B21 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/B21-fixtures.tar.gz module=$PRE15_CASE_TMP/B21-erofs-zstdio0.ko if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \ "$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \ >"$artifacts/B21-kld-build.stdout" 2>"$artifacts/B21-kld-build.stderr"; then pre15_dut_fail 'B21 cross-target zstdio0 KLD build failed' fi pre15_record_module "$module" file "$module" >"$artifacts/B21-kld-file.txt" sha256sum "$module" >"$artifacts/B21-kld-sha256.txt" nm -u "$module" | LC_ALL=C sort >"$artifacts/B21-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" } if ! pre15_scp "$fixture_archive" /root/B21-fixtures.tar.gz || \ ! pre15_scp "$qemu_probe" /root/B21-qemu-probe.c || \ ! pre15_scp "$module" /root/B21-erofs-zstdio0.ko; then pre15_infra_blocked 'could not transfer B21 module, probe, or fixtures' fi if ! pre15_guest_ssh_bounded \ 'rm -rf /root/B21-fixtures && mkdir /root/B21-fixtures && tar -xzf /root/B21-fixtures.tar.gz -C /root/B21-fixtures && cc -O2 -Wall -Wextra -Werror -std=c17 -o /root/B21-qemu-probe /root/B21-qemu-probe.c'; then pre15_infra_blocked 'could not prepare B21 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/B21-erofs-zstdio0.ko \ >"$artifacts/B21-kldload.stdout" 2>"$artifacts/B21-kldload.stderr"; then if grep -q 'module already loaded or in kernel' \ "$artifacts/B21-kldload.stderr"; then pre15_infra_blocked 'guest kernel already owns the erofs.1 interface' fi pre15_dut_fail 'B21 exact-source zstdio0 KLD failed to load' fi pre15_own_guest_kld erofs 'B21 exact-source KLD' pre15_guest_ssh_bounded dmesg >"$artifacts/B21-dmesg-before.txt" pre15_guest_ssh_bounded mkdir -p /mnt/pre15-b21 b21_unown() { kind=$1 value=$2 tmp=$PRE15_CASE_TMP/ownership.$$ awk -F '\t' -v kind="$kind" -v value="$value" \ '!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp" mv "$tmp" "$PRE15_OWNERSHIP_FILE" } printf 'id\texpected_errno\tactual_errno\treject\n' \ >"$artifacts/B21-qemu-matrix.tsv" while IFS="$(printf '\t')" read -r id authenticated expected_errno reject fixture_hash; do test "$id" != id || continue b21_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \ -f "/root/B21-fixtures/$id.erofs") || \ pre15_dut_fail "$id md attach failed" case "$b21_md" in md[0-9]*) ;; *) pre15_runner_fail "unexpected B21 md unit: $b21_md" ;; esac pre15_own_guest_md "$b21_md" "$id md" result=$(pre15_guest_ssh_bounded /root/B21-qemu-probe \ "/dev/$b21_md" /mnt/pre15-b21) || \ pre15_infra_blocked "$id guest mount probe failed" actual_errno=$(printf '%s\n' "$result" | sed -n 's/.* errno=\([0-9][0-9]*\).*/\1/p') test -n "$actual_errno" || pre15_runner_fail "$id mount probe did not report errno" if test "$actual_errno" = 0; then pre15_own_guest_mount /mnt/pre15-b21 "$id mount" fi printf '%s\t%s\t%s\t%s\n' "$id" "$expected_errno" \ "$actual_errno" "$reject" >>"$artifacts/B21-qemu-matrix.tsv" if test "$actual_errno" != "$expected_errno"; then pre15_dut_fail "$id expected errno $expected_errno, received $actual_errno" fi if test "$actual_errno" = 0; then pre15_guest_ssh_bounded find /mnt/pre15-b21 -mindepth 1 -maxdepth 1 \ -print >"$artifacts/B21-legal-readdir.txt" pre15_guest_ssh_bounded umount /mnt/pre15-b21 || \ pre15_dut_fail "$id unmount failed" b21_unown guest-mount /mnt/pre15-b21 fi pre15_guest_ssh_bounded mdconfig -d -u "$b21_md" || \ pre15_dut_fail "$id md detach failed" b21_unown guest-md "$b21_md" done <"$artifacts/B21-matrix.tsv" pre15_guest_ssh_bounded dmesg >"$artifacts/B21-dmesg-after.txt" diff -u "$artifacts/B21-dmesg-before.txt" "$artifacts/B21-dmesg-after.txt" \ >"$artifacts/B21-dmesg.diff" || true sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/B21-dmesg.diff" \ >"$artifacts/B21-dmesg-added.txt" if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free' \ "$artifacts/B21-dmesg-added.txt"; then pre15_dut_fail 'TC172 produced panic, WITNESS, or UAF evidence' fi printf '%s\n' \ 'TC172-super-trust B21 QEMU PASS' \ '13/13 legal/damaged images returned exact mount errno with checksum trust order preserved'