update
This commit is contained in:
Executable
+464
@@ -0,0 +1,464 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user