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

335 lines
12 KiB
Bash
Executable File

#!/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"
artifacts=$PRE15_RUN_DIR/artifacts
action=${1:-callgraph}
shift || :
case "$action" in
callgraph|nm-allowlist) ;;
*) pre15_fail_usage "unknown B31r action: $action" ;;
esac
for tool in git nm python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B31r host tool: $tool"
done
for source in data.c dir.c inode.c internal.h namei.c super.c zdata.c zmap.c; do
pre15_record_fixture "b31r-$source" "$PRE15_DUT/src/$source"
done
pre15_record_fixture b31r-case \
"$PRE15_DUT/tests/pre15/cases/B31r-visibility.sh"
mkdir -p "$artifacts"
if test "$action" = callgraph; then
test "$#" -eq 0 || pre15_fail_usage 'B31r callgraph takes no arguments'
pre15_target_reached
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" \
"$artifacts/B31r-callgraph.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])
report_path = Path(sys.argv[3])
src = dut / "src"
symbols = {
"erofs_map_dev": "data.c",
"erofs_map_blocks": "data.c",
"erofs_dirent_namelen": "dir.c",
"erofs_iloc": "inode.c",
}
expected_occurrences = {
"erofs_map_dev": {"data.c": 2},
"erofs_map_blocks": {"data.c": 3, "super.c": 1, "zdata.c": 1},
"erofs_dirent_namelen": {"dir.c": 3, "namei.c": 1},
"erofs_iloc": {"inode.c": 3},
}
expected_cross_tu = {
"erofs_map_dev": [],
"erofs_map_blocks": ["super.c", "zdata.c"],
"erofs_dirent_namelen": ["namei.c"],
"erofs_iloc": [],
}
introduced_by = {
("erofs_map_blocks", "super.c"): "55c609db",
("erofs_map_blocks", "zdata.c"): "55c609db",
("erofs_dirent_namelen", "namei.c"): "c7508502",
}
def strip_noncode(source: str) -> str:
output = list(source)
state = "code"
index = 0
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
output[index] = output[index + 1] = " "
state = "block"
index += 2
continue
if char == "/" and following == "/":
output[index] = output[index + 1] = " "
state = "line"
index += 2
continue
if char == '"':
output[index] = " "
state = "string"
elif char == "'":
output[index] = " "
state = "character"
elif state == "block":
if char == "*" and following == "/":
output[index] = output[index + 1] = " "
state = "code"
index += 2
continue
if char != "\n":
output[index] = " "
elif state == "line":
if char == "\n":
state = "code"
else:
output[index] = " "
else:
if char == "\\" and following:
output[index] = output[index + 1] = " "
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
if char != "\n":
output[index] = " "
index += 1
if state == "block":
raise SystemExit("unterminated block comment")
return "".join(output)
sources = {
path.name: strip_noncode(path.read_text(encoding="utf-8"))
for path in sorted(src.glob("*.c"))
}
locations: dict[str, list[dict[str, object]]] = {}
cross_tu: dict[str, list[str]] = {}
history: dict[str, str] = {}
for symbol, owner in symbols.items():
pattern = re.compile(r"\b" + re.escape(symbol) + r"\s*\(")
sites = []
counts = Counter()
for filename, source in sources.items():
for match in pattern.finditer(source):
line = source.count("\n", 0, match.start()) + 1
sites.append({"file": filename, "line": line})
counts[filename] += 1
actual_counts = dict(sorted(counts.items()))
if actual_counts != expected_occurrences[symbol]:
raise SystemExit(
f"{symbol} call-token inventory changed: {actual_counts!r}"
)
consumers = sorted(filename for filename in counts if filename != owner)
if consumers != expected_cross_tu[symbol]:
raise SystemExit(f"{symbol} cross-TU consumers changed: {consumers!r}")
locations[symbol] = sites
cross_tu[symbol] = consumers
for filename in consumers:
for site in sites:
if site["file"] != filename:
continue
blamed = subprocess.run(
[
"git",
"-C",
str(root),
"blame",
"--line-porcelain",
f"-L{site['line']},{site['line']}",
"--",
f"repo-pre-15/src/{filename}",
],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()[0].split()[0]
expected = introduced_by[(symbol, filename)]
if not blamed.startswith(expected):
raise SystemExit(
f"{symbol} {filename} introducer changed: {blamed}"
)
history[f"{symbol}:{filename}"] = blamed
internal = (src / "internal.h").read_text(encoding="utf-8")
for symbol in symbols:
prototype_count = len(
re.findall(r"\b" + re.escape(symbol) + r"\s*\(", internal)
)
expected = 1 if symbol in {"erofs_map_blocks", "erofs_dirent_namelen"} else 0
if prototype_count != expected:
raise SystemExit(
f"{symbol} prototype count changed: {prototype_count}, expected {expected}"
)
data = (src / "data.c").read_text(encoding="utf-8")
inode = (src / "inode.c").read_text(encoding="utf-8")
if not re.search(r"\bstatic\s+int\s+erofs_map_dev\s*\(", data):
raise SystemExit("erofs_map_dev is not static in data.c")
if not re.search(r"\bstatic\s+erofs_off_t\s+erofs_iloc\s*\(", inode):
raise SystemExit("erofs_iloc is not static in inode.c")
all_source = "\n".join(
path.read_text(encoding="utf-8") for path in sorted(src.glob("*.[ch]"))
)
mtime_helper_invocations = all_source.count(
"EROFS_FEATURE_FUNCS(mtime, compat, COMPAT_MTIME)"
)
mtime_explicit_references = len(re.findall(r"\berofs_sb_has_mtime\b", all_source))
if mtime_helper_invocations != 0 or mtime_explicit_references != 0:
raise SystemExit(
"mtime helper inventory changed: "
f"generator={mtime_helper_invocations} explicit={mtime_explicit_references}"
)
erofs_fs = (src / "erofs_fs.h").read_text(encoding="utf-8")
super_source = (src / "super.c").read_text(encoding="utf-8")
timestamp_anchors = {
"ondisk_constant": "#define EROFS_FEATURE_COMPAT_MTIME" in erofs_fs,
"compact_checked_add": "__builtin_add_overflow(sbi->epoch," in inode,
"extended_timestamp": "(int64_t)le64toh(die->i_mtime)" in inode,
"epoch_decode": "sbi->epoch = (int64_t)le64toh(dsb->epoch);" in super_source,
"fixed_nsec_decode": "sbi->fixed_nsec = le32toh(dsb->fixed_nsec);" in super_source,
}
if not all(timestamp_anchors.values()):
raise SystemExit(f"timestamp path changed: {timestamp_anchors!r}")
source_hashes = {}
for path in sorted(src.glob("*.[ch]")):
source_hashes[path.relative_to(dut).as_posix()] = hashlib.sha256(
path.read_bytes()
).hexdigest()
report = {
"decision": "PASS",
"freebsd_call_tokens": locations,
"freebsd_cross_tu_consumers": cross_tu,
"introduced_by": history,
"linkage": {
"erofs_map_dev": "static",
"erofs_map_blocks": "external",
"erofs_dirent_namelen": "external",
"erofs_iloc": "static",
},
"internal_prototypes": {
symbol: len(re.findall(r"\b" + re.escape(symbol) + r"\s*\(", internal))
for symbol in symbols
},
"mtime_helper": {
"generator_invocations": mtime_helper_invocations,
"explicit_references": mtime_explicit_references,
},
"timestamp_anchors": timestamp_anchors,
"source_hashes": source_hashes,
}
report_path.write_text(
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii"
)
(report_path.parent / "B31r-source-sha256.txt").write_text(
"".join(f"{digest} {path}\n" for path, digest in source_hashes.items()),
encoding="ascii",
)
PY
then
pre15_dut_fail 'B31r independent callgraph audit failed'
fi
printf '%s\n' \
'B31r independent cross-TU callgraph PASS' \
'erofs_map_dev and erofs_iloc are same-TU and static' \
'erofs_map_blocks and erofs_dirent_namelen retain external linkage' \
'erofs_sb_has_mtime helper is absent; timestamp anchors remain' \
'source hashes and exact consumers recorded in B31r-callgraph.json'
exit 0
fi
test "$#" -eq 2 || \
pre15_fail_usage 'B31r nm-allowlist requires zstdio0 and zstdio1 modules'
for module in "$@"; do
pre15_record_fixture "b31r-module-$module" "$module"
done
pre15_target_reached
for config in 0 1; do
case "$config" in
0) module=$1 ;;
1) module=$2 ;;
esac
nm -an "$module" >"$artifacts/B31r-zstdio$config-nm-an.txt"
awk '$NF ~ /^(erofs_map_dev|erofs_map_blocks|erofs_dirent_namelen|erofs_iloc|erofs_sb_has_mtime)$/' \
"$artifacts/B31r-zstdio$config-nm-an.txt" \
>"$artifacts/B31r-zstdio$config-target-bindings.txt"
nm -g --defined-only "$module" | awk '{ print $NF }' | LC_ALL=C sort \
>"$artifacts/B31r-zstdio$config-global-names.txt"
nm -u "$module" | awk '{ print $NF }' | LC_ALL=C sort \
>"$artifacts/B31r-zstdio$config-undefined-names.txt"
for symbol in erofs_map_blocks erofs_dirent_namelen; do
if ! awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
"$artifacts/B31r-zstdio$config-global-names.txt"; then
pre15_dut_fail "B31r zstdio$config lost required global $symbol"
fi
done
for symbol in erofs_map_dev erofs_iloc erofs_sb_has_mtime; do
if awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
"$artifacts/B31r-zstdio$config-global-names.txt" || \
awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
"$artifacts/B31r-zstdio$config-undefined-names.txt"; then
pre15_dut_fail "B31r zstdio$config unexpectedly exposes $symbol"
fi
done
for symbol in erofs_map_dev erofs_iloc; do
if ! awk -v symbol="$symbol" \
'$NF == symbol && $(NF - 1) !~ /^[a-z]$/ { bad = 1 } END { exit bad }' \
"$artifacts/B31r-zstdio$config-nm-an.txt"; then
pre15_dut_fail "B31r zstdio$config has non-local binding for $symbol"
fi
done
if awk '$NF == "erofs_sb_has_mtime" { found = 1 } END { exit !found }' \
"$artifacts/B31r-zstdio$config-nm-an.txt"; then
pre15_dut_fail "B31r zstdio$config emits erofs_sb_has_mtime"
fi
done
if ! diff -u "$artifacts/B31r-zstdio0-global-names.txt" \
"$artifacts/B31r-zstdio1-global-names.txt" \
>"$artifacts/B31r-config-global.diff"; then
pre15_dut_fail 'B31r defined globals differ by ZSTDIO configuration'
fi
printf '%s\n' \
'B31r nm allowlist PASS' \
'cross-TU symbols remain global in both configurations' \
'same-TU symbols are absent or locally bound and never global/undefined' \
'erofs_sb_has_mtime is absent from all symbol bindings' \
'zstdio0/zstdio1 defined-global sets match'