#!/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=8e85a8b64dd1a956376ff1cb31d2157bc73df8ad artifacts=$PRE15_RUN_DIR/artifacts inode=$PRE15_DUT/src/inode.c vnops=$PRE15_DUT/src/erofs_vnops.c super=$PRE15_DUT/src/super.c internal=$PRE15_DUT/src/internal.h namei=$PRE15_DUT/src/namei.c for tool in git python3 sha256sum; do command -v "$tool" >/dev/null 2>&1 || \ pre15_infra_blocked "missing B09 host tool: $tool" done pre15_record_fixture b09-inode "$inode" pre15_record_fixture b09-vnops "$vnops" pre15_record_fixture b09-super "$super" pre15_record_fixture b09-internal "$internal" pre15_record_fixture b09-namei "$namei" pre15_record_fixture b09-case "$PRE15_DUT/tests/pre15/cases/B09-vnode.sh" mkdir -p "$artifacts" pre15_target_reached if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$artifacts" <<'PY' from __future__ import annotations 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] artifacts = Path(sys.argv[4]) src = dut / "src" def committed(path: str) -> str: completed = subprocess.run( ["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{path}"], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if completed.returncode != 0: raise SystemExit(f"cannot read B09 baseline {path}: {completed.stderr}") return completed.stdout def replace_once(source: str, old: str, new: str, label: str) -> str: count = source.count(old) if count != 1: raise SystemExit(f"{label}: expected one transform source, found {count}") return source.replace(old, new, 1) 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 initializer(source: str, declaration: str, prefix: str) -> list[tuple[str, str]]: match = re.search( re.escape(declaration) + r"\s*=\s*\{(.*?)\n\};", source, re.DOTALL ) if not match: raise SystemExit(f"initializer absent: {declaration}") return re.findall( rf"^\s*\.({prefix}_[A-Za-z0-9_]+)\s*=\s*([^,]+),", match.group(1), re.MULTILINE, ) def require_order(source: str, markers: list[str], label: str) -> None: position = -1 for marker in markers: position = source.find(marker, position + 1) if position < 0: raise SystemExit(f"{label} is missing ordered marker: {marker}") current = { name: (src / name).read_text(encoding="utf-8") for name in ("inode.c", "erofs_vnops.c", "super.c", "internal.h", "namei.c") } base = {name: committed(name) for name in current} moved_marker = "static u_int\nerofs_vfs_hash(erofs_nid_t nid)" moved_offset = base["inode.c"].find(moved_marker) if moved_offset < 0: raise SystemExit("B09 baseline vnode adapter block is absent") moved_block = base["inode.c"][moved_offset:] expected_inode = base["inode.c"][:moved_offset].rstrip("\n") + "\n" if current["inode.c"] != expected_inode: raise SystemExit("inode.c changed outside the exact vnode adapter movement") if current["internal.h"] != base["internal.h"]: raise SystemExit("internal.h changed despite no B09 interface delta") if current["namei.c"] != base["namei.c"]: raise SystemExit("namecache/lookup code changed outside the B09 write set") expected_vnops = replace_once( base["erofs_vnops.c"], "#include \n", "#include \n#include \n", "vnode hash include", ) old_open = """\tif (vp->v_type == VREG) { \t\tif (vnode_create_vobject(vp, vi->size, ap->a_td) != 0) \t\t\treturn (ENOMEM); \t} """ new_open = """\tif (vp->v_type == VREG) \t\tvnode_create_vobject(vp, vi->size, ap->a_td); """ expected_vnops = replace_once(expected_vnops, old_open, new_open, "pager dead branch") expected_vnops = replace_once( expected_vnops, "static int\nerofs_reclaim(struct vop_reclaim_args *ap)", moved_block + "\nstatic int\nerofs_reclaim(struct vop_reclaim_args *ap)", "vnode adapter placement", ) if current["erofs_vnops.c"] != expected_vnops: raise SystemExit("erofs_vnops.c differs from the two declared B09 transforms") expected_super = replace_once( base["super.c"], "static vfs_vget_t erofs_vgetf;\n", "", "vget declaration" ) expected_super = replace_once( expected_super, """static int erofs_vgetf(struct mount *mp, ino_t ino, int flags, struct vnode **vpp) { \treturn (erofs_vget(mp, ino, flags, vpp)); } """, "", "vget wrapper", ) expected_super = replace_once( expected_super, "\t.vfs_vget = erofs_vgetf,", "\t.vfs_vget = erofs_vget,", "vfs_vget slot", ) if current["super.c"] != expected_super: raise SystemExit("super.c differs from the exact direct-vget transform") moved_functions = ( "erofs_vfs_hash", "erofs_vfs_hash_cmp", "erofs_fill_vnode", "erofs_vget", ) for name in moved_functions: if extract_function(base["inode.c"], name) != extract_function( current["erofs_vnops.c"], name ): raise SystemExit(f"moved function changed semantics: {name}") definitions = { path: len(re.findall(r"^" + re.escape(name) + r"\s*\(", text, re.MULTILINE)) for path, text in current.items() } if definitions["erofs_vnops.c"] != 1 or sum(definitions.values()) != 1: raise SystemExit(f"B09 function ownership is not unique: {name} {definitions}") base_vnode_slots = initializer( base["erofs_vnops.c"], "struct vop_vector erofs_vnodeops", "vop" ) base_fifo_slots = initializer( base["erofs_vnops.c"], "struct vop_vector erofs_fifoops", "vop" ) vnode_slots = initializer( current["erofs_vnops.c"], "struct vop_vector erofs_vnodeops", "vop" ) fifo_slots = initializer( current["erofs_vnops.c"], "struct vop_vector erofs_fifoops", "vop" ) base_vfs_slots = initializer(base["super.c"], "static struct vfsops erofs_vfsops", "vfs") vfs_slots = initializer(current["super.c"], "static struct vfsops erofs_vfsops", "vfs") expected_vfs_slots = [ (slot, "erofs_vget" if slot == "vfs_vget" else target) for slot, target in base_vfs_slots ] if vnode_slots != base_vnode_slots or fifo_slots != base_fifo_slots: raise SystemExit("FreeBSD KOBJ VOP slot order or targets changed") if vfs_slots != expected_vfs_slots: raise SystemExit("VFS slots changed beyond direct erofs_vget registration") if ("vop_lookup", "vfs_cache_lookup") not in vnode_slots or ( "vop_cachedlookup", "erofs_lookup" ) not in vnode_slots: raise SystemExit("FreeBSD namecache slots were not preserved") if "erofs_vgetf" in current["super.c"]: raise SystemExit("trivial erofs_vgetf wrapper remains") vget = extract_function(current["erofs_vnops.c"], "erofs_vget") require_order( vget, [ "td = curthread;", "nid = (uint64_t)ino;", "shared = (flags & LK_TYPE_MASK) == LK_SHARED;", "hash = erofs_vfs_hash(nid);", "error = vfs_hash_get(", "if (error != 0 || *vpp != NULL)", "sbi = MTOE(mp);", "vi = malloc(sizeof(*vi), M_EROFS, M_WAITOK | M_ZERO);", "error = getnewvnode(\"erofs\", mp, &erofs_vnodeops, &vp);", "vp->v_data = vi;", "vi->nid = nid;", "lockmgr(vp->v_vnlock, LK_EXCLUSIVE, NULL);", "error = insmntque(vp, mp);", "error = vfs_hash_insert(", "if (error != 0 || *vpp != NULL)", "error = erofs_read_inode(sbi, nid, vi);", "vgone(vp);", "vput(vp);", "erofs_fill_vnode(sbi, vp, vi);", "vn_set_state(vp, VSTATE_CONSTRUCTED);", "if (shared)", "VOP_LOCK(vp, LK_DOWNGRADE);", "*vpp = vp;", ], "erofs_vget lifecycle", ) if vget.count("erofs_vfs_hash_cmp") != 2: raise SystemExit("vget no longer uses one comparator at both hash callsites") if vget.count("free(vi, M_EROFS);") != 2 or vget.count("*vpp = NULL;") != 3: raise SystemExit("vget allocation/insmntque/read failure cleanup changed") if vget.count("vgone(vp);") != 1 or vget.count("vput(vp);") != 1: raise SystemExit("vget failed-inode vnode cleanup changed") if re.search(r"return\s*\(\s*-", vget): raise SystemExit("negative errno entered the FreeBSD vget path") reclaim = extract_function(current["erofs_vnops.c"], "erofs_reclaim") if reclaim != extract_function(base["erofs_vnops.c"], "erofs_reclaim"): raise SystemExit("reclaim/hash removal semantics changed") require_order( reclaim, ["vi = VTOE(vp);", "vfs_hash_remove(vp);", "free(vi, M_EROFS);", "vp->v_data = NULL;"], "erofs_reclaim lifecycle", ) open_body = extract_function(current["erofs_vnops.c"], "erofs_open") if open_body.count("vnode_create_vobject(vp, vi->size, ap->a_td);") != 1: raise SystemExit("regular-vnode pager setup is not a single direct call") if "ENOMEM" in open_body or "vnode_create_vobject" not in open_body: raise SystemExit("dead pager errno folding remains") slot_report = { "status": "PASS", "vnode": [{"slot": slot, "target": target} for slot, target in vnode_slots], "fifo": [{"slot": slot, "target": target} for slot, target in fifo_slots], "vfs": [{"slot": slot, "target": target} for slot, target in vfs_slots], } (artifacts / "B09-vop-slots.json").write_text( json.dumps(slot_report, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="ascii", ) callgraph = [ "VFS root -> erofs_vget", "namecache cachedlookup -> erofs_lookup -> erofs_vget", "VFS vget slot -> erofs_vget", "erofs_vget -> vfs_hash_get[erofs_vfs_hash_cmp]", "erofs_vget -> getnewvnode -> LK_EXCLUSIVE -> insmntque", "erofs_vget -> vfs_hash_insert[erofs_vfs_hash_cmp]", "erofs_vget -> erofs_read_inode -> erofs_fill_vnode", "erofs_vget -> VSTATE_CONSTRUCTED -> optional LK_DOWNGRADE", "erofs_reclaim -> vfs_hash_remove -> free inode", ] (artifacts / "B09-vget-callgraph.txt").write_text( "\n".join(callgraph) + "\n", encoding="ascii" ) result = { "status": "PASS", "final_ids": ["P15-008", "P15-050", "P15-088"], "baseline": baseline, "moved_functions": list(moved_functions), "moved_functions_byte_identical": True, "vnode_slots": len(vnode_slots), "fifo_slots": len(fifo_slots), "vfs_slots": len(vfs_slots), "hash_comparator_callsites": 2, "exclusive_construct_lock": True, "shared_result_downgrade": True, "insmntque_cleanup_preserved": True, "reclaim_hash_removal_preserved": True, "namecache_slots_preserved": True, "positive_errno_preserved": True, "b11_b15_scope": "NOT_INCLUDED", "qemu": "NOT_RUN", "full_feature_suite": "NOT_RUN", } (artifacts / "B09-result.json").write_text( json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="ascii", ) print( f"B09 PASS moved={len(moved_functions)} " f"slots={len(vnode_slots)}+{len(fifo_slots)}/{len(vfs_slots)} hash=2" ) PY then : else pre15_dut_fail 'B09 vnode ownership, VOP slots, or vget graph check failed' fi sha256sum "$artifacts/B09-vop-slots.json" \ "$artifacts/B09-vget-callgraph.txt" "$artifacts/B09-result.json" \ > "$artifacts/SHA256SUMS" printf 'B09 PASS vnode adapter ownership and FreeBSD lifecycle preserved\n'