465 lines
17 KiB
Python
Executable File
465 lines
17 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
|
|
|
|
EINTEGRITY = "EINTEGRITY"
|
|
PASS = "PASS"
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", required=True, type=Path)
|
|
parser.add_argument("--dut", required=True, type=Path)
|
|
parser.add_argument("--baseline", required=True)
|
|
parser.add_argument("--spec", required=True, type=Path)
|
|
parser.add_argument("--report", required=True, type=Path)
|
|
return parser.parse_args()
|
|
|
|
|
|
def committed(root: Path, baseline: str, path: str) -> str:
|
|
completed = subprocess.run(
|
|
["git", "-C", str(root), "show", f"{baseline}:{path}"],
|
|
check=False,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
if completed.returncode != 0:
|
|
raise SystemExit(f"cannot read B10 baseline {path}: {completed.stderr}")
|
|
return completed.stdout
|
|
|
|
|
|
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 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}")
|
|
|
|
|
|
def function_position(source: str, name: str) -> int:
|
|
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
|
if not match:
|
|
raise SystemExit(f"missing function position: {name}")
|
|
return match.start()
|
|
|
|
|
|
def source_checks(root: Path, dut: Path, baseline: str) -> dict[str, object]:
|
|
src = dut / "src"
|
|
current = {
|
|
name: (src / name).read_text(encoding="utf-8")
|
|
for name in ("dir.c", "namei.c", "internal.h", "erofs_vnops.c")
|
|
}
|
|
base = {
|
|
name: committed(root, baseline, f"repo-pre-15/src/{name}")
|
|
for name in current
|
|
}
|
|
if current["dir.c"] == base["dir.c"] or current["namei.c"] == base["namei.c"]:
|
|
raise SystemExit("B10 did not change both declared directory sources")
|
|
if current["internal.h"] != base["internal.h"]:
|
|
raise SystemExit("B10 added directory-wide state outside its write set")
|
|
if current["erofs_vnops.c"] != base["erofs_vnops.c"]:
|
|
raise SystemExit("B10 changed the FreeBSD VOP adapter")
|
|
|
|
completed = subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
str(root),
|
|
"diff",
|
|
"--name-only",
|
|
baseline,
|
|
"--",
|
|
"repo-pre-15/src",
|
|
],
|
|
check=True,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
changed_sources = set(completed.stdout.splitlines())
|
|
expected_sources = {
|
|
"repo-pre-15/src/dir.c",
|
|
"repo-pre-15/src/namei.c",
|
|
}
|
|
if changed_sources != expected_sources:
|
|
raise SystemExit(f"unexpected B10 source write set: {sorted(changed_sources)}")
|
|
|
|
dir_source = current["dir.c"]
|
|
namei_source = current["namei.c"]
|
|
validator = extract_function(dir_source, "erofs_validate_dirblock")
|
|
fill = extract_function(dir_source, "erofs_fill_dentries")
|
|
readdir = extract_function(dir_source, "erofs_readdir_block")
|
|
previous = extract_function(dir_source, "erofs_previous_dirname")
|
|
read_block = extract_function(namei_source, "erofs_read_dirblock")
|
|
neighbors = extract_function(namei_source, "erofs_validate_dirblock_neighbors")
|
|
find_block = extract_function(namei_source, "erofs_find_target_block")
|
|
namei = extract_function(namei_source, "erofs_namei")
|
|
lookup = extract_function(namei_source, "erofs_lookup")
|
|
|
|
require_order(
|
|
validator,
|
|
["erofs_dirent_namelen", "erofs_dirname_order", "EINTEGRITY"],
|
|
"block lexical validator",
|
|
)
|
|
require_order(
|
|
fill,
|
|
["erofs_dirent_name", "erofs_nid_is_valid", "bzero", "d_fileno", "erofs_uiodir"],
|
|
"readdir publication boundary",
|
|
)
|
|
require_order(
|
|
readdir,
|
|
[
|
|
"GENERIC_MINDIRSIZ",
|
|
"(size_t)INT_MAX",
|
|
"SIZE_MAX / sizeof(*cookiebuf)",
|
|
"(int)cookie_count",
|
|
],
|
|
"cookie clamp",
|
|
)
|
|
if "uio->uio_resid / 8" in readdir or "MAX(1" in readdir:
|
|
raise SystemExit("legacy unchecked cookie sizing remains")
|
|
require_order(
|
|
readdir,
|
|
[
|
|
"erofs_validate_dirblock",
|
|
"erofs_previous_dirname",
|
|
"erofs_dirname_order",
|
|
"erofs_fill_dentries",
|
|
],
|
|
"linear readdir order validation",
|
|
)
|
|
if previous.count("erofs_read_data") != 1 or previous.count("erofs_brelse") != 1:
|
|
raise SystemExit("readdir predecessor ownership is not one acquire/release")
|
|
require_order(read_block, ["erofs_read_data", "erofs_validate_dirblock"], "lookup block read")
|
|
for marker in ("block - 1", "block + 1", "erofs_dirblock_order"):
|
|
if marker not in neighbors:
|
|
raise SystemExit(f"bounded neighbor validation is missing: {marker}")
|
|
if find_block.count("while (head <= back)") != 1 or re.search(r"\bfor\s*\(", find_block):
|
|
raise SystemExit("lookup is no longer a single binary block search")
|
|
require_order(
|
|
find_block,
|
|
["erofs_read_dirblock", "erofs_validate_dirblock_neighbors", "erofs_dirnamecmp"],
|
|
"touched-block lookup validation",
|
|
)
|
|
require_order(namei, ["erofs_nid_is_valid", "*nid = found_nid", "*d_type"], "lookup NID publication")
|
|
require_order(
|
|
lookup,
|
|
["erofs_namei", "(cnp->cn_flags & ISDOTDOT) == 0", "vn_vget_ino", "erofs_vget"],
|
|
"FreeBSD self-NID lock boundary",
|
|
)
|
|
if lookup.count("vn_vget_ino") != 1 or lookup.count("erofs_vget") != 1:
|
|
raise SystemExit("FreeBSD dotdot/ordinary vnode acquisition changed shape")
|
|
for marker in ("vref(dvp)", "cache_enter(dvp, NULL, cnp)", "cache_enter(dvp, vp, cnp)"):
|
|
if marker not in lookup:
|
|
raise SystemExit(f"FreeBSD lookup behavior is missing: {marker}")
|
|
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", dir_source + namei_source):
|
|
raise SystemExit("negative Linux errno entered B10")
|
|
|
|
dir_order = [
|
|
"erofs_validate_dirblock",
|
|
"erofs_fill_dentries",
|
|
"erofs_readdir_block",
|
|
]
|
|
if [function_position(dir_source, name) for name in dir_order] != sorted(
|
|
function_position(dir_source, name) for name in dir_order
|
|
):
|
|
raise SystemExit("dir core order does not retain validator-before-Linux-core shape")
|
|
namei_order = [
|
|
"find_target_dirent",
|
|
"erofs_find_target_block",
|
|
"erofs_namei",
|
|
"erofs_lookup",
|
|
]
|
|
if [function_position(namei_source, name) for name in namei_order] != sorted(
|
|
function_position(namei_source, name) for name in namei_order
|
|
):
|
|
raise SystemExit("namei core order diverges from the Linux reference shape")
|
|
|
|
return {
|
|
"changed_sources": sorted(changed_sources),
|
|
"dir_core_order": dir_order,
|
|
"namei_core_order": namei_order,
|
|
"full_directory_state_added": False,
|
|
"positive_errno": True,
|
|
"vfs_cache_lookup_preserved": "vfs_cache_lookup" in current["erofs_vnops.c"],
|
|
}
|
|
|
|
|
|
def validate_block(names: list[str]) -> None:
|
|
if not names or any(not name for name in names):
|
|
raise ValueError(EINTEGRITY)
|
|
encoded = [name.encode("ascii") for name in names]
|
|
if any(left >= right for left, right in zip(encoded, encoded[1:])):
|
|
raise ValueError(EINTEGRITY)
|
|
|
|
|
|
def validate_boundary(left: list[str], right: list[str]) -> None:
|
|
if left[-1].encode("ascii") >= right[0].encode("ascii"):
|
|
raise ValueError(EINTEGRITY)
|
|
|
|
|
|
def lookup_model(blocks: list[list[str]], target: str) -> tuple[str, int, list[int]]:
|
|
head = 0
|
|
back = len(blocks) - 1
|
|
candidate = None
|
|
reads = 0
|
|
touched = []
|
|
target_bytes = target.encode("ascii")
|
|
try:
|
|
while head <= back:
|
|
mid = head + (back - head) // 2
|
|
touched.append(mid)
|
|
validate_block(blocks[mid])
|
|
reads += 1
|
|
if mid > 0:
|
|
validate_block(blocks[mid - 1])
|
|
validate_boundary(blocks[mid - 1], blocks[mid])
|
|
reads += 1
|
|
if mid + 1 < len(blocks):
|
|
validate_block(blocks[mid + 1])
|
|
validate_boundary(blocks[mid], blocks[mid + 1])
|
|
reads += 1
|
|
first = blocks[mid][0].encode("ascii")
|
|
if target_bytes < first:
|
|
back = mid - 1
|
|
continue
|
|
candidate = mid
|
|
if target_bytes == first:
|
|
return PASS, reads, touched
|
|
head = mid + 1
|
|
if candidate is None:
|
|
return "ENOENT", reads, touched
|
|
names = [name.encode("ascii") for name in blocks[candidate]]
|
|
left = 1
|
|
right = len(names) - 1
|
|
while left <= right:
|
|
mid = left + (right - left) // 2
|
|
if names[mid] == target_bytes:
|
|
return PASS, reads, touched
|
|
if names[mid] < target_bytes:
|
|
left = mid + 1
|
|
else:
|
|
right = mid - 1
|
|
return "ENOENT", reads, touched
|
|
except ValueError:
|
|
return EINTEGRITY, reads, touched
|
|
|
|
|
|
def readdir_model(blocks: list[list[str]]) -> tuple[str, list[str]]:
|
|
output = []
|
|
previous = None
|
|
try:
|
|
for block in blocks:
|
|
validate_block(block)
|
|
if previous is not None:
|
|
validate_boundary(previous, block)
|
|
output.extend(block)
|
|
previous = block
|
|
except ValueError:
|
|
return EINTEGRITY, output
|
|
return PASS, output
|
|
|
|
|
|
def run_order_cases(spec: dict[str, object]) -> list[dict[str, object]]:
|
|
results = []
|
|
for case in spec["order_cases"]:
|
|
lookup_status, reads, touched = lookup_model(case["blocks"], case["target"])
|
|
readdir_status, raw = readdir_model(case["blocks"])
|
|
if lookup_status != case["expected_lookup"]:
|
|
raise SystemExit(f"lookup order oracle mismatch: {case['id']}")
|
|
if readdir_status != case["expected_readdir"]:
|
|
raise SystemExit(f"readdir order oracle mismatch: {case['id']}")
|
|
if "expected_raw" in case and raw != case["expected_raw"]:
|
|
raise SystemExit(f"raw directory order changed: {case['id']}")
|
|
results.append(
|
|
{
|
|
"id": case["id"],
|
|
"lookup": lookup_status,
|
|
"readdir": readdir_status,
|
|
"lookup_reads": reads,
|
|
"lookup_touched": touched,
|
|
"raw_entries": raw if readdir_status == PASS else None,
|
|
}
|
|
)
|
|
return results
|
|
|
|
|
|
def nid_is_valid(config: dict[str, object], case: dict[str, object]) -> bool:
|
|
uint64_max = (1 << 64) - 1
|
|
metabox_bit = config["metabox_bit"]
|
|
nid = case["nid"]
|
|
low = nid & (metabox_bit - 1)
|
|
if low > (uint64_max >> 5):
|
|
return False
|
|
offset = low << 5
|
|
in_metabox = bool(nid & metabox_bit)
|
|
if in_metabox:
|
|
if not case["metabox_feature"] or case["metabox_size"] is None:
|
|
return False
|
|
return offset <= case["metabox_size"] and config["compact_inode_size"] <= case["metabox_size"] - offset
|
|
primary = config["primary"]
|
|
if primary["blocks"] > (uint64_max >> primary["blkszbits"]):
|
|
return False
|
|
metadata = primary["meta_blkaddr"] << primary["blkszbits"]
|
|
if offset > uint64_max - metadata:
|
|
return False
|
|
offset += metadata
|
|
image_size = primary["blocks"] << primary["blkszbits"]
|
|
compact = config["compact_inode_size"]
|
|
return (
|
|
offset <= image_size
|
|
and compact <= image_size - offset
|
|
and offset <= primary["mediasize"]
|
|
and compact <= primary["mediasize"] - offset
|
|
)
|
|
|
|
|
|
def run_nid_cases(config: dict[str, object]) -> list[dict[str, object]]:
|
|
results = []
|
|
for case in config["cases"]:
|
|
valid = nid_is_valid(config, case)
|
|
lookup = EINTEGRITY if not valid else PASS
|
|
if valid and case["relation"] == "ordinary" and case["nid"] == case["current_nid"]:
|
|
lookup = EINTEGRITY
|
|
readdir = PASS if valid else EINTEGRITY
|
|
if valid != case["expected_valid"] or lookup != case["expected_lookup"] or readdir != case["expected_readdir"]:
|
|
raise SystemExit(f"NID oracle mismatch: {case['id']}")
|
|
results.append({"id": case["id"], "valid": valid, "lookup": lookup, "readdir": readdir})
|
|
return results
|
|
|
|
|
|
def run_cookie_cases(config: dict[str, object]) -> list[dict[str, int]]:
|
|
results = []
|
|
allocation_limit = config["size_max"] // 8
|
|
for case in config["cases"]:
|
|
count = 0
|
|
if case["resid"] > 0:
|
|
count = case["resid"] // config["min_dirent_size"]
|
|
count = min(count, config["int_max"], allocation_limit)
|
|
if count != case["expected"]:
|
|
raise SystemExit(f"cookie clamp oracle mismatch: {case['id']}")
|
|
results.append({"id": case["id"], "resid": case["resid"], "cookies": count})
|
|
return results
|
|
|
|
|
|
def complexity_result(config: dict[str, int]) -> dict[str, object]:
|
|
blocks = config["directory_bytes"] // config["block_size"]
|
|
if blocks != config["expected_blocks"]:
|
|
raise SystemExit("TC153 sparse directory block denominator changed")
|
|
max_iterations = blocks.bit_length()
|
|
if max_iterations != config["max_binary_iterations"]:
|
|
raise SystemExit("TC153 binary iteration bound mismatch")
|
|
scenarios = []
|
|
for target in (0, blocks // 2, blocks - 1):
|
|
head = 0
|
|
back = blocks - 1
|
|
iterations = 0
|
|
reads = 0
|
|
while head <= back:
|
|
mid = head + (back - head) // 2
|
|
iterations += 1
|
|
reads += 1 + int(mid > 0) + int(mid + 1 < blocks)
|
|
if mid == target:
|
|
break
|
|
if mid < target:
|
|
head = mid + 1
|
|
else:
|
|
back = mid - 1
|
|
if iterations > config["max_binary_iterations"] or reads > config["max_reads_with_neighbors"]:
|
|
raise SystemExit("TC153 lookup exceeded the logarithmic neighbor-read bound")
|
|
scenarios.append({"target_block": target, "iterations": iterations, "reads": reads})
|
|
return {
|
|
"directory_bytes": config["directory_bytes"],
|
|
"blocks": blocks,
|
|
"bound": "O(log n) selected blocks plus two neighbors per selection",
|
|
"scenarios": scenarios,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
spec = json.loads(args.spec.read_text(encoding="ascii"))
|
|
if spec.get("schema") != 1 or spec.get("batch") != "B10":
|
|
raise SystemExit("invalid B10 fixture identity")
|
|
if spec.get("candidates") != ["P15-012", "P15-054", "P15-055", "P15-078", "P15-080"]:
|
|
raise SystemExit("B10 candidate list changed")
|
|
|
|
report = {
|
|
"schema": 1,
|
|
"batch": "B10",
|
|
"source": source_checks(args.root, args.dut, args.baseline),
|
|
"order": run_order_cases(spec),
|
|
"nid": run_nid_cases(spec["nid"]),
|
|
"cookie": run_cookie_cases(spec["cookie"]),
|
|
"complexity": complexity_result(spec["complexity"]),
|
|
}
|
|
args.report.write_text(
|
|
json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
|
encoding="ascii",
|
|
)
|
|
print(f"TC163-directory-order PASS {len(report['order'])} cases")
|
|
print(f"TC173-readdir-boundaries PASS {len(report['nid']) + len(report['cookie'])} cases")
|
|
print("TC153-large-directory-block-index PASS O(log n)+bounded-neighbors")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|