This commit is contained in:
2026-08-18 09:20:44 +02:00
commit b826cd721a
522 changed files with 93730 additions and 0 deletions
+359
View File
@@ -0,0 +1,359 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=145d892afc8a425294029d4349f03f1b17cf8f92
artifacts=$PRE15_RUN_DIR/artifacts
xattr=$PRE15_DUT/src/xattr.c
linux_xattr=$PRE15_ROOT/src-linux/xattr.c
xattr_header=$PRE15_DUT/src/xattr.h
vnops=$PRE15_DUT/src/erofs_vnops.c
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
fixture_spec=$fixture_dir/B17-xattr-spec.json
fixture_generator=$fixture_dir/B17-xattr-generate.py
fixture_oracle=$fixture_dir/B17-xattr-oracle.py
fixture_seed=$fixture_dir/B17-xattr-seed.erofs
for tool in git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B18 host tool: $tool"
done
pre15_record_fixture b18-freebsd-xattr "$xattr"
pre15_record_fixture b18-linux-xattr "$linux_xattr"
pre15_record_fixture b18-xattr-header "$xattr_header"
pre15_record_fixture b18-vnops "$vnops"
pre15_record_fixture b18-b17-spec "$fixture_spec"
pre15_record_fixture b18-b17-generator "$fixture_generator"
pre15_record_fixture b18-b17-oracle "$fixture_oracle"
pre15_record_fixture b18-b17-seed "$fixture_seed"
pre15_record_fixture b18-case \
"$PRE15_DUT/tests/pre15/cases/B18-xattr-order.sh"
mkdir -p "$artifacts"
pre15_target_reached
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
"$artifacts/B18-xattr-order.json" <<'PY'
from __future__ import annotations
from collections import Counter
import hashlib
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])
xattr_path = dut / "src/xattr.c"
current = xattr_path.read_text(encoding="utf-8")
linux = (root / "src-linux/xattr.c").read_text(encoding="utf-8")
def committed_bytes(path: str) -> bytes:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(
f"cannot read B18 baseline {path}: "
f"{completed.stderr.decode(errors='replace')}"
)
return completed.stdout
def require_once(source: str, marker: str, label: str) -> int:
count = source.count(marker)
if count != 1:
raise SystemExit(f"{label}: expected one marker, found {count}: {marker!r}")
return source.index(marker)
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())
if brace < 0:
raise SystemExit(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
raise SystemExit(f"unterminated function: {name}")
def require_order(source: str, markers: tuple[str, ...], label: str) -> None:
position = -1
for marker in markers:
position = source.find(marker, position + 1)
if position < 0:
raise SystemExit(f"{label}: missing ordered marker: {marker}")
base = committed_bytes("src/xattr.c").decode("utf-8")
iterator_start = require_once(base, "struct erofs_xattr_iter {", "iterator")
iterator_end_marker = "\n};\n\n"
iterator_end = base.find(iterator_end_marker, iterator_start)
if iterator_end < 0:
raise SystemExit("B18 baseline iterator terminator is absent")
iterator_end += len(iterator_end_marker)
iterator_block = base[iterator_start:iterator_end]
without_iterator = base[:iterator_start] + base[iterator_end:]
acl_start = require_once(
without_iterator,
"static int\nerofs_inode_has_noacl(",
"ACL/filter block",
)
acl_end = require_once(
without_iterator,
"static int\nerofs_listxattr_foreach(",
"common iterator block",
)
if acl_end <= acl_start:
raise SystemExit("B18 baseline ACL/filter block is not before the iterator core")
acl_block = without_iterator[acl_start:acl_end]
expected = without_iterator[:acl_start] + without_iterator[acl_end:]
core_start = require_once(
expected,
"static int\nerofs_xattr_backing_size(",
"backing core",
)
expected = expected[:core_start] + iterator_block + expected[core_start:]
acl_adapter = require_once(expected, "int\nerofs_get_acl(", "ACL adapter")
expected = expected[:acl_adapter] + acl_block + expected[acl_adapter:]
if current != expected:
raise SystemExit("xattr.c differs from the two exact B18 definition moves")
base_names = re.findall(r"^(erofs_[A-Za-z0-9_]+)\s*\(", base, re.MULTILINE)
current_names = re.findall(
r"^(erofs_[A-Za-z0-9_]+)\s*\(", current, re.MULTILINE
)
if Counter(base_names) != Counter(current_names):
raise SystemExit("B18 changed the xattr function definition multiset")
base_functions = {name: extract_function(base, name) for name in base_names}
current_functions = {name: extract_function(current, name) for name in current_names}
for name in base_functions:
if current_functions[name] != base_functions[name]:
raise SystemExit(f"B18 changed function text instead of moving it: {name}")
if current_functions[name].startswith("static ") != base_functions[name].startswith(
"static "
):
raise SystemExit(f"B18 changed symbol visibility: {name}")
def direct_calls(functions: dict[str, str]) -> Counter[tuple[str, str]]:
calls: Counter[tuple[str, str]] = Counter()
for caller, function in functions.items():
brace = function.find("{")
for callee in re.findall(
r"\b(erofs_[A-Za-z0-9_]+)\s*\(", function[brace + 1 :]
):
calls[(caller, callee)] += 1
return calls
base_calls = direct_calls(base_functions)
current_calls = direct_calls(current_functions)
if current_calls != base_calls:
raise SystemExit("B18 changed the direct-call multiset")
expected_order = [
"erofs_xattr_backing_size",
"erofs_xattr_read_backing",
"erofs_xattr_read_metadata",
"erofs_xattr_move",
"erofs_xattr_load_body",
"erofs_xattr_validate_entry",
"erofs_xattr_prefix",
"erofs_xattr_namespace_prefix",
"erofs_xattr_list_move",
"erofs_xattr_resolve_name",
"erofs_xattr_name_match",
"erofs_xattr_shared_entry_offset",
"erofs_xattr_load_shared_entry",
"erofs_listxattr_foreach",
"erofs_getxattr_foreach",
"erofs_xattr_iter_inline",
"erofs_xattr_iter_shared",
"erofs_getxattr",
"erofs_listxattr",
"erofs_xattr_prefixes_cleanup",
"erofs_xattr_prefixes_init",
"erofs_inode_has_noacl",
"erofs_acl_from_mode",
"erofs_posix_acl_from_xattr",
"erofs_get_acl",
]
if current_names != expected_order:
raise SystemExit(f"B18 xattr definition order mismatch: {current_names}")
if current.index("struct erofs_xattr_iter {") > current.index(
"erofs_xattr_backing_size("
):
raise SystemExit("xattr iterator contract is not before the backing/core helpers")
linux_aligned_core = (
"erofs_listxattr_foreach(",
"erofs_getxattr_foreach(",
"erofs_xattr_iter_inline(",
"erofs_xattr_iter_shared(",
"erofs_getxattr(",
"erofs_listxattr(",
"erofs_xattr_prefixes_cleanup(",
"erofs_xattr_prefixes_init(",
)
require_order(current, linux_aligned_core, "FreeBSD common xattr core")
require_order(linux, linux_aligned_core, "Linux common xattr core")
readonly_paths = (
"src/xattr.h",
"src/erofs_vnops.c",
"tests/pre15/fixtures/B17-xattr-spec.json",
"tests/pre15/fixtures/B17-xattr-generate.py",
"tests/pre15/fixtures/B17-xattr-oracle.py",
"tests/pre15/fixtures/B17-xattr-seed.erofs",
)
for path in readonly_paths:
if (dut / path).read_bytes() != committed_bytes(path):
raise SystemExit(f"B18 changed a read-only xattr/VOP/fixture contract: {path}")
vnops = (dut / "src/erofs_vnops.c").read_text(encoding="utf-8")
getextattr = extract_function(vnops, "erofs_getextattr")
listextattr = extract_function(vnops, "erofs_listextattr")
require_order(
getextattr,
("extattr_check_cred(", "switch (ap->a_attrnamespace)", "erofs_getxattr("),
"FreeBSD getextattr adapter",
)
require_order(
listextattr,
("extattr_check_cred(", "switch (ap->a_attrnamespace)", "erofs_listxattr("),
"FreeBSD listextattr adapter",
)
if vnops.count("extattr_check_cred(") != 2:
raise SystemExit("FreeBSD extattr credential boundary changed")
if "ERANGE" in current:
raise SystemExit("Linux all-or-nothing xattr buffer semantics entered FreeBSD")
if "uiomove(value, value_size, uio)" not in current:
raise SystemExit("FreeBSD partial extattr transfer path is absent")
if re.search(r"return\s*(?:\(\s*)?-E[A-Z0-9_]+", current):
raise SystemExit("Linux negative errno entered FreeBSD xattr.c")
load_body = extract_function(current, "erofs_xattr_load_body")
require_order(
load_body,
(
"vi->xattr_isize < sizeof(*ih)",
"vi->xattr_isize == sizeof(*ih)",
"error = EOPNOTSUPP",
"header_size = sizeof(*ih)",
),
"exact-header compatibility",
)
spec_path = dut / "tests/pre15/fixtures/B17-xattr-spec.json"
spec = json.loads(spec_path.read_text(encoding="ascii"))
cases = spec.get("cases", [])
legal = sum(item.get("class") == "legal" for item in cases)
damaged = sum(item.get("class") == "damaged" for item in cases)
if len(cases) != 25 or legal != 10 or damaged != 15:
raise SystemExit("B17 xattr fixture order/cardinality changed")
seed_path = dut / "tests/pre15/fixtures" / spec["seed"]["path"]
seed_hash = hashlib.sha256(seed_path.read_bytes()).hexdigest()
if seed_hash != spec["seed"]["sha256"]:
raise SystemExit("B17 xattr seed no longer matches its frozen specification")
result = {
"status": "PASS",
"baseline": baseline,
"source_sha256": hashlib.sha256(current.encode("utf-8")).hexdigest(),
"definition_count": len(current_names),
"direct_call_edges": len(current_calls),
"direct_call_sites": sum(current_calls.values()),
"static_visibility_unchanged": True,
"function_text_unchanged": True,
"linux_core_order": list(linux_aligned_core),
"freebsd_contracts": [
"positive errno",
"partial uiomove",
"extattr_check_cred",
"USER and SYSTEM namespaces",
"exact-header EOPNOTSUPP",
"uncached xattr lookup",
],
"b17_fixture_cases": len(cases),
"b17_fixture_legal": legal,
"b17_fixture_damaged": damaged,
"b17_seed_sha256": seed_hash,
}
output.write_text(
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii"
)
print(
"B18 xattr order: exact moves, "
f"{len(current_names)} definitions, "
f"{sum(current_calls.values())} direct calls"
)
print(f"B18 B17 fixture contract: {len(cases)} cases, seed {seed_hash}")
PY
then
:
else
pre15_dut_fail 'B18 xattr ordering or FreeBSD contract equivalence failed'
fi
printf '%s\n' 'B18 xattr order: PASS'