Files
erofs-freebsd-out-tree/tests/pre15/cases/B21-super.sh
T
2026-08-18 09:20:44 +02:00

541 lines
19 KiB
Bash

#!/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'