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

376 lines
13 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"
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 <sys/extattr.h>\n",
"#include <sys/extattr.h>\n#include <sys/fnv_hash.h>\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'