This commit is contained in:
2026-08-18 09:20:44 +02:00
commit b826cd721a
522 changed files with 93730 additions and 0 deletions
+350
View File
@@ -0,0 +1,350 @@
#!/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=643b83c9cea6a99398fa3dd053f2e8f65e108ca0
artifacts=$PRE15_RUN_DIR/artifacts
action=${1:-callgraph}
shift || :
case "$action" in
callgraph|nm-allowlist) ;;
*) pre15_fail_usage "unknown B31 action: $action" ;;
esac
for tool in git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B31 host tool: $tool"
done
pre15_record_fixture b31-data "$PRE15_DUT/src/data.c"
pre15_record_fixture b31-dir "$PRE15_DUT/src/dir.c"
pre15_record_fixture b31-inode "$PRE15_DUT/src/inode.c"
pre15_record_fixture b31-internal "$PRE15_DUT/src/internal.h"
pre15_record_fixture b31-case \
"$PRE15_DUT/tests/pre15/cases/B31-visibility.sh"
mkdir -p "$artifacts"
if ! git -C "$PRE15_ROOT" diff --quiet "$baseline" -- \
repo-pre-15/src/data.c repo-pre-15/src/dir.c \
repo-pre-15/src/inode.c repo-pre-15/src/internal.h; then
pre15_target_reached
pre15_dut_fail 'B31 production files differ from the B30 source baseline'
fi
if test "$action" = callgraph; then
test "$#" -eq 0 || pre15_fail_usage 'B31 callgraph takes no arguments'
for source in zdata.c super.c namei.c erofs_fs.h; do
pre15_record_fixture "b31-$source" "$PRE15_DUT/src/$source"
done
pre15_record_fixture b31-linux-data "$PRE15_ROOT/src-linux/data.c"
pre15_record_fixture b31-linux-internal "$PRE15_ROOT/src-linux/internal.h"
pre15_record_fixture b31-linux-fileio "$PRE15_ROOT/src-linux/fileio.c"
pre15_record_fixture b31-linux-fscache "$PRE15_ROOT/src-linux/fscache.c"
pre15_record_fixture b31-linux-inode "$PRE15_ROOT/src-linux/inode.c"
pre15_record_fixture b31-linux-xattr "$PRE15_ROOT/src-linux/xattr.c"
pre15_record_fixture b31-linux-zmap "$PRE15_ROOT/src-linux/zmap.c"
pre15_record_fixture b31-linux-zdata "$PRE15_ROOT/src-linux/zdata.c"
pre15_target_reached
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" \
"$artifacts/B31-callgraph.json" <<'PY'
from __future__ import annotations
from collections import Counter
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)
)
if prototype_count != 1:
raise SystemExit(f"{symbol} prototype count changed: {prototype_count}")
all_source = "\n".join(
path.read_text(encoding="utf-8") for path in sorted(src.glob("*.[ch]"))
)
mtime_helper_invocations = internal.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 != 1 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")
inode = (src / "inode.c").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}")
linux_src = root / "src-linux"
linux_text = {
path.name: strip_noncode(path.read_text(encoding="utf-8"))
for path in (
linux_src / "data.c",
linux_src / "fileio.c",
linux_src / "fscache.c",
linux_src / "inode.c",
linux_src / "xattr.c",
linux_src / "zmap.c",
linux_src / "zdata.c",
linux_src / "internal.h",
)
}
linux_internal = linux_text["internal.h"]
if not re.search(r"\bint\s+erofs_map_dev\s*\(", linux_internal):
raise SystemExit("Linux erofs_map_dev external prototype is absent")
if not re.search(r"\bint\s+erofs_map_blocks\s*\(", linux_internal):
raise SystemExit("Linux erofs_map_blocks external prototype is absent")
if not re.search(
r"static\s+inline\s+erofs_off_t\s+erofs_iloc\s*\(", linux_internal
):
raise SystemExit("Linux erofs_iloc is no longer static inline")
if re.search(r"\berofs_dirent_namelen\s*\(", "\n".join(linux_text.values())):
raise SystemExit("unexpected Linux erofs_dirent_namelen symbol")
linux_consumers = {}
for symbol in ("erofs_map_dev", "erofs_map_blocks", "erofs_iloc"):
pattern = re.compile(r"\b" + re.escape(symbol) + r"\s*\(")
linux_consumers[symbol] = sorted(
filename for filename, source in linux_text.items() if pattern.search(source)
)
report = {
"decision": "STOP-NO-SOURCE",
"freebsd_call_tokens": locations,
"freebsd_cross_tu_consumers": cross_tu,
"introduced_by": history,
"linux_linkage": {
"erofs_map_dev": "external",
"erofs_map_blocks": "external",
"erofs_dirent_namelen": "absent",
"erofs_iloc": "static inline",
},
"linux_consumer_files_in_probe": linux_consumers,
"mtime_helper": {
"generator_invocations": mtime_helper_invocations,
"explicit_references": mtime_explicit_references,
},
"timestamp_anchors": timestamp_anchors,
}
report_path.write_text(
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii"
)
PY
then
pre15_dut_fail 'B31 cross-TU visibility audit failed'
fi
printf '%s\n' \
'B31 callgraph audit reached mandatory STOP' \
'erofs_map_blocks: cross-TU consumers super.c,zdata.c introduced by B07c' \
'erofs_dirent_namelen: cross-TU consumer namei.c introduced by B10' \
'erofs_map_dev and erofs_iloc remain same-TU only; atomic B31 source edit is blocked' \
'mtime helper remains generated with zero explicit references; ondisk constant and timestamp paths remain'
pre15_gate_stop \
'B31 blocked: B07c/B10 introduced cross-TU consumers for listed symbols'
fi
test "$#" -eq 2 || \
pre15_fail_usage 'B31 nm-allowlist requires zstdio0 and zstdio1 modules'
for tool in awk diff nm; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B31 nm tool: $tool"
done
module0=$1
module1=$2
pre15_record_fixture b31-zstdio0-module "$module0"
pre15_record_fixture b31-zstdio1-module "$module1"
pre15_target_reached
for config in 0 1; do
eval module=\$module$config
nm -g --defined-only "$module" | awk '{ print $NF }' | LC_ALL=C sort \
>"$artifacts/B31-zstdio$config-global-names.txt"
nm -u "$module" | awk '{ print $NF }' | LC_ALL=C sort \
>"$artifacts/B31-zstdio$config-undefined-names.txt"
for symbol in erofs_map_dev erofs_map_blocks erofs_dirent_namelen erofs_iloc; do
if ! awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
"$artifacts/B31-zstdio$config-global-names.txt"; then
pre15_dut_fail "B31 zstdio$config lost required global $symbol"
fi
done
if awk '$0 == "erofs_sb_has_mtime" { found = 1 } END { exit !found }' \
"$artifacts/B31-zstdio$config-global-names.txt" || \
awk '$0 == "erofs_sb_has_mtime" { found = 1 } END { exit !found }' \
"$artifacts/B31-zstdio$config-undefined-names.txt"; then
pre15_dut_fail "B31 zstdio$config unexpectedly emits erofs_sb_has_mtime"
fi
done
if ! diff -u "$artifacts/B31-zstdio0-global-names.txt" \
"$artifacts/B31-zstdio1-global-names.txt" \
>"$artifacts/B31-config-global.diff"; then
pre15_dut_fail 'B31 defined globals differ by ZSTDIO configuration'
fi
printf '%s\n' \
'B31 nm allowlist PASS' \
'global delta from B30 source baseline: zero' \
'four listed symbols remain global in both configurations after mandatory STOP' \
'erofs_sb_has_mtime is not emitted or undefined in either configuration'