276 lines
9.0 KiB
Bash
Executable File
276 lines
9.0 KiB
Bash
Executable File
#!/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"
|
|
|
|
fixtures_only=0
|
|
if test "${1:-}" = --fixtures-only; then
|
|
fixtures_only=1
|
|
shift
|
|
fi
|
|
test "$#" -eq 0 || pre15_runner_fail 'B17 accepts only --fixtures-only'
|
|
|
|
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
|
spec=$fixture_dir/B17-xattr-spec.json
|
|
generator=$fixture_dir/B17-xattr-generate.py
|
|
oracle=$fixture_dir/B17-xattr-oracle.py
|
|
seed=$fixture_dir/B17-xattr-seed.erofs
|
|
artifacts=$PRE15_RUN_DIR/artifacts
|
|
first=$PRE15_CASE_TMP/first
|
|
second=$PRE15_CASE_TMP/second
|
|
rebuilt=$PRE15_CASE_TMP/rebuilt-seed.erofs
|
|
rebuild_work=$PRE15_CASE_TMP/rebuild-work
|
|
|
|
for tool in fsck.erofs git mkfs.erofs python3 sha256sum; do
|
|
command -v "$tool" >/dev/null 2>&1 || \
|
|
pre15_infra_blocked "missing B17 host tool: $tool"
|
|
done
|
|
|
|
pre15_record_fixture b17-spec "$spec"
|
|
pre15_record_fixture b17-generator "$generator"
|
|
pre15_record_fixture b17-oracle "$oracle"
|
|
pre15_record_fixture b17-seed "$seed"
|
|
pre15_record_fixture b17-case "$PRE15_DUT/tests/pre15/cases/B17-xattr-integrity.sh"
|
|
mkdir -p "$artifacts"
|
|
|
|
if python3 -B "$generator" rebuild-seed --spec "$spec" --seed "$seed" \
|
|
--output "$rebuilt" --work "$rebuild_work" \
|
|
>"$artifacts/rebuild-seed.json" 2>"$artifacts/rebuild-seed.stderr"; then
|
|
:
|
|
else
|
|
pre15_runner_fail 'B17 tracked seed is not reproducible'
|
|
fi
|
|
|
|
if python3 -B "$generator" generate --spec "$spec" --seed "$seed" \
|
|
--output "$first" >"$artifacts/generate-first.json" \
|
|
2>"$artifacts/generate-first.stderr"; then
|
|
:
|
|
else
|
|
pre15_runner_fail 'B17 first fixture generation failed'
|
|
fi
|
|
if python3 -B "$generator" generate --spec "$spec" --seed "$seed" \
|
|
--output "$second" >"$artifacts/generate-second.json" \
|
|
2>"$artifacts/generate-second.stderr"; then
|
|
:
|
|
else
|
|
pre15_runner_fail 'B17 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 'B17 fixture generation is not byte reproducible'
|
|
fi
|
|
|
|
if python3 -B "$oracle" --spec "$spec" --fixtures "$first" \
|
|
--report "$artifacts/oracle-first.json" \
|
|
>"$artifacts/oracle-first.stdout" 2>"$artifacts/oracle-first.stderr"; then
|
|
:
|
|
else
|
|
pre15_runner_fail 'B17 independent first oracle replay failed'
|
|
fi
|
|
if python3 -B "$oracle" --spec "$spec" --fixtures "$second" \
|
|
--report "$artifacts/oracle-second.json" \
|
|
>"$artifacts/oracle-second.stdout" 2>"$artifacts/oracle-second.stderr"; then
|
|
:
|
|
else
|
|
pre15_runner_fail 'B17 independent second oracle replay failed'
|
|
fi
|
|
|
|
cp "$first/fixture-index.json" "$artifacts/B17-fixture-index.json"
|
|
pre15_record_fixture b17-generated-index "$artifacts/B17-fixture-index.json"
|
|
|
|
if python3 - "$artifacts/B17-fixture-index.json" \
|
|
"$artifacts/oracle-first.json" "$artifacts/B17-matrix.tsv" <<'PY'
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
index = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
|
report = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
|
|
if index["fixture_count"] != 25 or index["legal_count"] != 10 or index["damaged_count"] != 15:
|
|
raise SystemExit("B17 fixture cardinality changed")
|
|
if report["status"] != "PASS" or report["legal_passed"] != 10 or report["damaged_passed"] != 15:
|
|
raise SystemExit("B17 oracle matrix is incomplete")
|
|
lines = ["id\tclass\terrno\treject"]
|
|
for item in report["results"]:
|
|
lines.append(
|
|
f"{item['id']}\t{item['class']}\t{item['actual_errno']}\t{item['actual_reject']}"
|
|
)
|
|
Path(sys.argv[3]).write_text("\n".join(lines) + "\n", encoding="ascii")
|
|
print(
|
|
f"B17 fixtures: {index['fixture_count']} "
|
|
f"({index['legal_count']} legal, {index['damaged_count']} damaged)"
|
|
)
|
|
print(f"B17 fixture-set SHA256: {index['fixture_set_sha256']}")
|
|
PY
|
|
then
|
|
:
|
|
else
|
|
pre15_runner_fail 'B17 fixture matrix summary failed'
|
|
fi
|
|
|
|
pre15_target_reached
|
|
if test "$fixtures_only" -eq 1; then
|
|
printf '%s\n' 'B17 fixture gate: READY'
|
|
exit 0
|
|
fi
|
|
|
|
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$artifacts/B17-source-check.json" <<'PY'
|
|
from pathlib import Path
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
|
|
root = Path(sys.argv[1])
|
|
dut = Path(sys.argv[2])
|
|
output = Path(sys.argv[3])
|
|
baseline = "98d76e4e73ed9a760b7f684d453b301c73167c22"
|
|
|
|
|
|
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 B17 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 baseline transform, found {count}")
|
|
return source.replace(old, new, 1)
|
|
|
|
|
|
current = {
|
|
name: (dut / "src" / name).read_text(encoding="utf-8")
|
|
for name in ("internal.h", "super.c", "xattr.c")
|
|
}
|
|
base = {name: committed(name) for name in current}
|
|
|
|
expected_internal = replace_once(
|
|
base["internal.h"],
|
|
"\tuint8_t xattr_prefix_count;\n",
|
|
"\tuint8_t xattr_prefix_count;\n\tuint8_t xattr_filter_reserved;\n",
|
|
"raw xattr filter state",
|
|
)
|
|
|
|
expected_super = replace_once(
|
|
base["super.c"],
|
|
"\t/* A non-zero reserved value disables the current name-filter format. */\n"
|
|
"\tif (erofs_sb_has_xattr_filter(sbi) && dsb->xattr_filter_reserved != 0)\n"
|
|
"\t\tsbi->feature_compat &= ~EROFS_FEATURE_COMPAT_XATTR_FILTER;\n",
|
|
"\t/* Preserve the raw feature declaration and gate its format at use sites. */\n"
|
|
"\tsbi->xattr_filter_reserved = dsb->xattr_filter_reserved;\n",
|
|
"raw feature preservation",
|
|
)
|
|
|
|
expected_xattr = replace_once(
|
|
base["xattr.c"],
|
|
"\tif (entry_size > remaining)\n\t\treturn (EINTEGRITY);\n",
|
|
"\tif (entry_size > remaining)\n\t\treturn (EINTEGRITY);\n"
|
|
"\tif (memchr(entry->e_name, '\\0', entry->e_name_len) != NULL)\n"
|
|
"\t\treturn (EINTEGRITY);\n",
|
|
"inline name NUL validation",
|
|
)
|
|
expected_xattr = replace_once(
|
|
expected_xattr,
|
|
"\terror = erofs_xattr_read_backing(sbi, backing_en, off, entry_size,\n"
|
|
"\t entrybuf);\n"
|
|
"\tif (error != 0)\n"
|
|
"\t\treturn (error);\n"
|
|
"\tif (entry_sizep != NULL)\n",
|
|
"\terror = erofs_xattr_read_backing(sbi, backing_en, off, entry_size,\n"
|
|
"\t entrybuf);\n"
|
|
"\tif (error != 0)\n"
|
|
"\t\treturn (error);\n"
|
|
"\terror = erofs_xattr_validate_entry(entrybuf->data, entry_size, NULL,\n"
|
|
"\t value_sizep);\n"
|
|
"\tif (error != 0) {\n"
|
|
"\t\terofs_put_metabuf(entrybuf);\n"
|
|
"\t\treturn (error);\n"
|
|
"\t}\n"
|
|
"\tif (entry_sizep != NULL)\n",
|
|
"shared name NUL validation",
|
|
)
|
|
expected_xattr = replace_once(
|
|
expected_xattr,
|
|
"\tif (!erofs_sb_has_xattr_filter(sbi))\n\t\treturn (0);\n",
|
|
"\tif (!erofs_sb_has_xattr_filter(sbi) ||\n"
|
|
"\t sbi->xattr_filter_reserved != 0)\n"
|
|
"\t\treturn (0);\n",
|
|
"xattr filter use-site gate",
|
|
)
|
|
expected_xattr = replace_once(
|
|
expected_xattr,
|
|
"\t\tprefix = buf.data;\n"
|
|
"\t\tinfix_len = len - sizeof(*prefix);\n"
|
|
"\t\tsbi->xattr_prefixes[i].base_index = prefix->base_index;\n",
|
|
"\t\tprefix = buf.data;\n"
|
|
"\t\tinfix_len = len - sizeof(*prefix);\n"
|
|
"\t\tif (memchr(prefix->infix, '\\0', infix_len) != NULL) {\n"
|
|
"\t\t\terror = EINTEGRITY;\n"
|
|
"\t\t\tgoto fail;\n"
|
|
"\t\t}\n"
|
|
"\t\tsbi->xattr_prefixes[i].base_index = prefix->base_index;\n",
|
|
"long-prefix NUL validation",
|
|
)
|
|
|
|
expected = {
|
|
"internal.h": expected_internal,
|
|
"super.c": expected_super,
|
|
"xattr.c": expected_xattr,
|
|
}
|
|
for name in expected:
|
|
if current[name] != expected[name]:
|
|
raise SystemExit(f"{name} differs from the exact B17 ledger transforms")
|
|
|
|
diff = subprocess.run(
|
|
["git", "-C", str(root), "diff", baseline, "--", "repo-pre-15/src/internal.h",
|
|
"repo-pre-15/src/super.c", "repo-pre-15/src/xattr.c"],
|
|
check=True,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
).stdout
|
|
for forbidden in ("xxh32", "bloom", "cache owner", "cache waiter"):
|
|
if forbidden in diff.lower():
|
|
raise SystemExit(f"B17 source diff contains out-of-scope marker: {forbidden}")
|
|
if "return (-E" in "".join(current.values()):
|
|
raise SystemExit("negative errno entered the FreeBSD B17 source")
|
|
|
|
result = {
|
|
"status": "PASS",
|
|
"baseline": baseline,
|
|
"changed_sources": sorted(expected),
|
|
"raw_feature_preserved": True,
|
|
"reserved_gated_at_use": True,
|
|
"inline_name_nul": True,
|
|
"shared_name_nul": True,
|
|
"prefix_infix_nul": True,
|
|
"acl_empty_suffix_preserved": True,
|
|
}
|
|
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
|
print("B17 source: exact raw-feature and name-integrity transforms present")
|
|
PY
|
|
then
|
|
:
|
|
else
|
|
pre15_dut_fail 'B17 source does not match the fixture-authorized transforms'
|
|
fi
|
|
|
|
printf '%s\n' 'B17 source gate: PASS'
|