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