#!/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=fb79d17edba7c47fae6e60b2a92771a8f3a2fcdf zmap=$PRE15_DUT/src/zmap.c internal=$PRE15_DUT/src/internal.h linux_zmap=$PRE15_ROOT/src-linux/zmap.c oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json gate_input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json artifacts=$PRE15_RUN_DIR/artifacts for tool in git python3 sha256sum; do command -v "$tool" >/dev/null 2>&1 || \ pre15_infra_blocked "missing B24 host tool: $tool" done pre15_record_fixture b24-zmap "$zmap" pre15_record_fixture b24-internal "$internal" pre15_record_fixture b24-linux-zmap "$linux_zmap" pre15_record_fixture b24-map-oracle "$oracle" pre15_record_fixture b24-gate-input "$gate_input" pre15_record_fixture b24-case \ "$PRE15_DUT/tests/pre15/cases/B24-zmap-order.sh" mkdir -p "$artifacts" pre15_target_reached if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$oracle" \ "$gate_input" "$artifacts/B24-zmap-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] oracle_path = Path(sys.argv[4]) gate_input_path = Path(sys.argv[5]) report_path = Path(sys.argv[6]) def committed(path: str) -> str: completed = subprocess.run( ["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if completed.returncode != 0: raise SystemExit(f"cannot read B24 baseline {path}: {completed.stderr}") return completed.stdout def function(source: str, name: str) -> str: match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE) if match is None: 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}") function_order = [ "z_erofs_index_base", "z_erofs_index_advance", "z_erofs_lcluster_count", "z_erofs_lcluster_pos", "z_erofs_lcn_advance", "z_erofs_compact_pblk", "z_erofs_fragment_offset", "z_erofs_post_eof_len", "z_erofs_physical_end", "z_erofs_read_index", "z_erofs_load_full_lcluster", "decode_compactedbits", "get_compacted_la_distance", "z_erofs_load_compact_lcluster", "z_erofs_load_lcluster_from_disk", "z_erofs_extent_lookback", "z_erofs_get_extent_compressedlen", "z_erofs_get_extent_decompressedlen", "z_erofs_extent_add", "z_erofs_extent_roundup", "z_erofs_extent_table_pos", "z_erofs_extent_record_pos", "z_erofs_read_extent", "z_erofs_extent_lstart", "z_erofs_validate_extent_table", "z_erofs_map_blocks_fo", "z_erofs_map_blocks_ext", "z_erofs_fill_inode", "z_erofs_map_sanity_check", "z_erofs_map_blocks", ] linux_algorithm_order = [ "z_erofs_load_full_lcluster", "decode_compactedbits", "get_compacted_la_distance", "z_erofs_load_compact_lcluster", "z_erofs_load_lcluster_from_disk", "z_erofs_extent_lookback", "z_erofs_get_extent_compressedlen", "z_erofs_get_extent_decompressedlen", "z_erofs_map_blocks_fo", "z_erofs_map_blocks_ext", "z_erofs_fill_inode", "z_erofs_map_sanity_check", ] current = (dut / "src/zmap.c").read_text(encoding="utf-8") before = committed("src/zmap.c") current_internal = (dut / "src/internal.h").read_text(encoding="utf-8") before_internal = committed("src/internal.h") linux = (root / "src-linux/zmap.c").read_text(encoding="utf-8") moved_start = before.index("static int\nz_erofs_extent_add(") moved_end = before.index("static int\nz_erofs_map_blocks_ext(", moved_start) moved = before[moved_start:moved_end] without_moved = before[:moved_start] + before[moved_end:] insert_at = without_moved.index("static int\nz_erofs_map_blocks_fo(") expected = without_moved[:insert_at] + moved + without_moved[insert_at:] if current != expected: raise SystemExit("zmap.c differs from the exact B24 validator-group move") before_functions = {name: function(before, name) for name in function_order} current_functions = {name: function(current, name) for name in function_order} for name in function_order: if current_functions[name] != before_functions[name]: raise SystemExit(f"B24 changed function text instead of moving it: {name}") def definition_position(source: str, name: str) -> int: match = re.search( r"^(?:" + re.escape(name) + r"|(?:static\s+)?" r"[A-Za-z_][A-Za-z0-9_ *]*\b" + re.escape(name) + r")\s*\(", source, re.MULTILINE, ) if match is None: raise SystemExit(f"missing definition position: {name}") return match.start() positions = [definition_position(current, name) for name in function_order] if positions != sorted(positions): raise SystemExit("B24 zmap definition order does not match the reviewed order") for source, label in ((current, "FreeBSD"), (linux, "Linux")): positions = [definition_position(source, name) for name in linux_algorithm_order] if positions != sorted(positions): raise SystemExit(f"{label} common zmap algorithm order drifted") def direct_calls(functions: dict[str, str]) -> Counter[tuple[str, str]]: calls: Counter[tuple[str, str]] = Counter() for caller, body in functions.items(): brace = body.find("{") for callee in re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", body[brace + 1 :]): if callee not in {"if", "for", "while", "switch", "return", "sizeof"}: calls[(caller, callee)] += 1 return calls before_calls = direct_calls(before_functions) current_calls = direct_calls(current_functions) if current_calls != before_calls: raise SystemExit("B24 changed the direct-call multiset") global_definitions = sorted( name for name, body in current_functions.items() if not body.startswith("static ") ) if global_definitions != ["z_erofs_fill_inode", "z_erofs_map_blocks"]: raise SystemExit(f"unexpected zmap global definitions: {global_definitions}") if current_internal != before_internal: raise SystemExit("B24 changed internal.h despite no removable zmap global") inode_source = (dut / "src/inode.c").read_text(encoding="utf-8") data_source = (dut / "src/data.c").read_text(encoding="utf-8") if inode_source.count("z_erofs_fill_inode(sbi, vi)") != 1: raise SystemExit("eager z_erofs_fill_inode no longer has its inode consumer") if data_source.count("z_erofs_map_blocks(sbi, vi, &next)") != 1: raise SystemExit("compressed map backend no longer has its common adapter consumer") for prototype in ( "int z_erofs_fill_inode(struct erofs_sb_info *sbi, struct erofs_inode *vi);", "int z_erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n" " struct erofs_map_blocks *map);", ): if current_internal.count(prototype) != 1: raise SystemExit(f"required FreeBSD zmap prototype drifted: {prototype!r}") if "int z_erofs_map_blocks_iter(struct inode *inode, struct erofs_map_blocks *map," not in linux: raise SystemExit("Linux zmap iterator naming anchor changed") for marker in ("erofs_read_metadata(", "erofs_put_metabuf(", "goto out;"): if current.count(marker) != before.count(marker): raise SystemExit(f"B24 changed provider I/O or cleanup marker count: {marker}") if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", current): raise SystemExit("B24 introduced Linux negative errno") oracle = json.loads(oracle_path.read_text(encoding="ascii")) gate_input = json.loads(gate_input_path.read_text(encoding="ascii")) records = oracle.get("records", []) if oracle.get("map_case_count") != 80 or len(records) != 80: raise SystemExit("frozen G02/B07 map tuple denominator changed") if gate_input.get("expected_device_case_count") != 13: raise SystemExit("frozen G02/B07 device denominator changed") tuple_bytes = b"".join(bytes.fromhex(record["tuple_hex"]) for record in records) tuple_digest = hashlib.sha256(tuple_bytes).hexdigest() if tuple_digest != oracle.get("tuple_bytes_sha256"): raise SystemExit("frozen G02/B07 tuple bytes no longer match their digest") if len({record["id"] for record in records}) != 80: raise SystemExit("frozen G02/B07 tuple IDs are not unique") changed = set( subprocess.run( ["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"], check=True, text=True, stdout=subprocess.PIPE, ).stdout.splitlines() ) changed.update( subprocess.run( [ "git", "-C", str(root), "ls-files", "--others", "--exclude-standard", "--", "repo-pre-15", ], check=True, text=True, stdout=subprocess.PIPE, ).stdout.splitlines() ) allowed = { "repo-pre-15/src/zmap.c", "repo-pre-15/tests/pre15/cases/B24-zmap-order.sh", } if changed != allowed: raise SystemExit(f"B24 write set differs: {sorted(changed ^ allowed)}") function_digest = hashlib.sha256( "\n\n".join(current_functions[name] for name in function_order).encode("utf-8") ).hexdigest() report = { "baseline": baseline, "batch": "B24", "candidate": "P15-016", "direct_call_edges": sum(current_calls.values()), "freebsd_backend_name": "z_erofs_map_blocks", "freebsd_positive_errno": True, "function_count": len(function_order), "function_text_sha256": function_digest, "global_definitions": global_definitions, "linux_iterator_name": "z_erofs_map_blocks_iter", "map_cases": len(records), "device_cases": gate_input["expected_device_case_count"], "provider_io_unchanged": True, "metadata_cleanup_unchanged": True, "status": "PASS", "tuple_bytes_sha256": tuple_digest, "write_set": sorted(changed), } report_path.write_text( json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="ascii", ) print( "B24 order/callgraph PASS " f"functions={len(function_order)} calls={sum(current_calls.values())} globals=2" ) print( "B24 map tuples PASS " f"maps={len(records)} devices={gate_input['expected_device_case_count']} " f"sha256={tuple_digest}" ) PY then : else pre15_dut_fail 'B24 order, callgraph, visibility, or tuple proof failed' fi sha256sum "$artifacts/B24-zmap-order.json" >"$artifacts/SHA256SUMS" printf '%s\n' \ 'B24 host PASS exact validator move and Linux algorithm order' \ 'B24 host PASS frozen G02 map tuples; QEMU and full feature suite NOT_RUN'