#!/usr/bin/env python3 """Independently verify B19b fixtures, Bloom semantics, and source scope.""" from __future__ import annotations import argparse import hashlib import json from pathlib import Path import re import statistics import struct import subprocess SUPER = 1024 MAGIC = 0xE0F5E1E2 class Reject(RuntimeError): def __init__(self, errno_name: str, point: str): super().__init__(f"{errno_name} at {point}") self.errno_name = errno_name self.point = point def sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def write_json(path: Path, value: object) -> None: path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii") def rotl32(value: int, count: int) -> int: return ((value << count) | (value >> (32 - count))) & 0xFFFFFFFF def xxh32(data: bytes, seed: int) -> int: prime1 = 2654435761 prime2 = 2246822519 prime3 = 3266489917 prime4 = 668265263 prime5 = 374761393 cursor = 0 def round32(accumulator: int, lane: int) -> int: accumulator = (accumulator + lane * prime2) & 0xFFFFFFFF return (rotl32(accumulator, 13) * prime1) & 0xFFFFFFFF if len(data) >= 16: accumulator1 = (seed + prime1 + prime2) & 0xFFFFFFFF accumulator2 = (seed + prime2) & 0xFFFFFFFF accumulator3 = seed & 0xFFFFFFFF accumulator4 = (seed - prime1) & 0xFFFFFFFF limit = len(data) - 16 while cursor <= limit: accumulator1 = round32(accumulator1, int.from_bytes(data[cursor:cursor + 4], "little")) accumulator2 = round32(accumulator2, int.from_bytes(data[cursor + 4:cursor + 8], "little")) accumulator3 = round32(accumulator3, int.from_bytes(data[cursor + 8:cursor + 12], "little")) accumulator4 = round32(accumulator4, int.from_bytes(data[cursor + 12:cursor + 16], "little")) cursor += 16 value = ( rotl32(accumulator1, 1) + rotl32(accumulator2, 7) + rotl32(accumulator3, 12) + rotl32(accumulator4, 18) ) & 0xFFFFFFFF else: value = (seed + prime5) & 0xFFFFFFFF value = (value + len(data)) & 0xFFFFFFFF while cursor + 4 <= len(data): value = (value + int.from_bytes(data[cursor:cursor + 4], "little") * prime3) & 0xFFFFFFFF value = (rotl32(value, 17) * prime4) & 0xFFFFFFFF cursor += 4 while cursor < len(data): value = (value + data[cursor] * prime5) & 0xFFFFFFFF value = (rotl32(value, 11) * prime1) & 0xFFFFFFFF cursor += 1 value ^= value >> 15 value = (value * prime2) & 0xFFFFFFFF value ^= value >> 13 value = (value * prime3) & 0xFFFFFFFF value ^= value >> 16 return value & 0xFFFFFFFF class Reader: def __init__(self, path: Path, spec: dict): self.data = path.read_bytes() self.spec = spec self.calls = 0 self.bytes = 0 self.blocks_read: set[int] = set() header = self.read(SUPER, 144, "super") if struct.unpack_from(" len(self.data): raise Reject("EINTEGRITY", "super.bounds") def read(self, offset: int, length: int, point: str) -> bytes: limit = self.limit if hasattr(self, "limit") else len(self.data) if offset < 0 or length < 0 or offset > limit or length > limit - offset: raise Reject("EINTEGRITY", f"{point}.bounds") self.calls += 1 self.bytes += length if length: self.blocks_read.update(range(offset // 4096, (offset + length - 1) // 4096 + 1)) return self.data[offset:offset + length] def reset_reads(self) -> None: self.calls = 0 self.bytes = 0 self.blocks_read.clear() def inode(self, nid: int) -> dict[str, int]: offset = (self.meta_blkaddr << self.block_bits) + (nid << 5) raw = self.read(offset, 64, "inode") inode_format, xattr_count = struct.unpack_from("> 1) & 7, "size": size, "start_block": struct.unpack_from(" bytes: if inode["layout"] == 2: physical = inode["offset"] + inode["inode_size"] + inode["xattr_size"] + logical elif inode["layout"] == 0: physical = (inode["start_block"] << self.block_bits) + logical else: raise Reject("EINTEGRITY", "directory.layout") return self.read(physical, length, "directory.data") def directory_entries(self, inode: dict[str, int]) -> list[tuple[bytes, int]]: entries = [] logical = 0 while logical < inode["size"]: length = min(self.block_size, inode["size"] - logical) data = self.inode_data(inode, logical, length) if len(data) < 12: raise Reject("EINTEGRITY", "directory.header") first_name = struct.unpack_from(" len(data): raise Reject("EINTEGRITY", "directory.name-offset") count = first_name // 12 for index in range(count): offset = index * 12 nid = struct.unpack_from(" end or end > len(data): raise Reject("EINTEGRITY", "directory.name-bounds") entries.append((data[start:end].split(b"\0", 1)[0], nid)) logical += length return entries def resolve(self, path: str) -> dict[str, int]: inode = self.inode(self.root_nid) for component in path.strip("/").encode().split(b"/"): matches = [nid for name, nid in self.directory_entries(inode) if name == component] if len(matches) != 1: raise Reject("ENOATTR", "path.lookup") inode = self.inode(matches[0]) return inode def stats(self, scanned: bool) -> dict[str, int | bool]: return { "bytes": self.bytes, "calls": self.calls, "provider_blocks": len(self.blocks_read), "scanned": scanned, } def lookup(self, inode: dict[str, int], name: bytes, candidate: bool) -> tuple[str, bytes | None, dict]: self.reset_reads() body_offset = inode["offset"] + inode["inode_size"] usable = self.feature_compat & self.spec["format"]["feature_compat"] and self.filter_reserved == 0 if candidate and usable: if inode["xattr_size"] < 12: raise Reject("EINTEGRITY", "ibody.header") if inode["xattr_size"] == 12: raise Reject("EOPNOTSUPP", "ibody.header-only") header = self.read(body_offset, 12, "ibody.header") shared_count = header[4] if 12 + shared_count * 4 > inode["xattr_size"]: raise Reject("EINTEGRITY", "ibody.shared-count") name_filter = struct.unpack_from(" len(body): raise Reject("EINTEGRITY", "ibody.shared-count") cursor = header_size while cursor < len(body): if cursor + 4 > len(body): raise Reject("EINTEGRITY", "inline.header") name_length, name_index, value_length = struct.unpack_from(" len(body) - cursor: raise Reject("EINTEGRITY", "inline.bounds") actual = body[cursor + 4:cursor + 4 + name_length] if b"\0" in actual: raise Reject("EINTEGRITY", "inline.name-nul") if name_index == 1 and actual == name: start = cursor + 4 + name_length return "PASS", body[start:start + value_length], self.stats(True) cursor += total for index in range(shared_count): shared_id = struct.unpack_from(" dict: try: reader = Reader(path, spec) inode = reader.resolve(spec["fixture"]["target"]) status, value, reads = reader.lookup(inode, name, candidate) return {"errno": 0 if status == "PASS" else spec["errno"][status], "reads": reads, "status": status, "value_hex": None if value is None else value.hex()} except Reject as error: return {"errno": spec["errno"].get(error.errno_name, -1), "point": error.point, "status": error.errno_name, "value_hex": None} def function(source: str, name: str) -> str: match = re.search(rf"\n{name}\([^;]*?\n\{{", source, re.DOTALL) if match is None: raise SystemExit(f"missing source function: {name}") start = match.start() + 1 brace = source.index("{", match.start()) depth = 0 for index in range(brace, len(source)): if source[index] == "{": depth += 1 elif source[index] == "}": depth -= 1 if depth == 0: return source[start:index + 1] raise SystemExit(f"unterminated source function: {name}") def audit_source(root: Path, dut: Path, spec: dict) -> dict: header = (dut / "src/erofs_fs.h").read_text(encoding="utf-8") internal = (dut / "src/internal.h").read_text(encoding="utf-8") xattr = (dut / "src/xattr.c").read_text(encoding="utf-8") linux_header = (root / "src-linux/erofs_fs.h").read_text(encoding="utf-8") linux_xattr = (root / "src-linux/xattr.c").read_text(encoding="utf-8") for marker in ( "#define EROFS_XATTR_FILTER_BITS", "#define EROFS_XATTR_FILTER_DEFAULT", "#define EROFS_XATTR_FILTER_SEED", ): if marker not in header or marker not in linux_header: raise SystemExit(f"Bloom format marker missing: {marker}") if "erofs_sb_has_xattr_filter_v1" not in internal or "xattr_filter_reserved == 0" not in internal: raise SystemExit("FreeBSD unknown-filter fallback helper is incomplete") hash_body = function(xattr, "erofs_xxh32") filter_name = function(xattr, "erofs_xattr_filter_name") filter_negative = function(xattr, "erofs_xattr_filter_negative") getxattr = function(xattr, "erofs_getxattr") if "static uint32_t\nerofs_xxh32" not in xattr: raise SystemExit("xxh32 is not file-local and namespaced") for marker in ("le32dec(cursor)", "UINT32_C(2654435761)", "hash ^= hash >> 16"): if marker not in hash_body: raise SystemExit(f"xxh32 source marker missing: {marker}") for marker in ( "EXTATTR_NAMESPACE_USER", "EXTATTR_NAMESPACE_SYSTEM", "EROFS_XATTR_INDEX_USER", "EROFS_XATTR_INDEX_POSIX_ACL_ACCESS", "EROFS_XATTR_INDEX_POSIX_ACL_DEFAULT", "EROFS_XATTR_INDEX_TRUSTED", "EROFS_XATTR_INDEX_SECURITY", ): if marker not in filter_name: raise SystemExit(f"FreeBSD namespace mapping marker missing: {marker}") for marker in ( "erofs_sb_has_xattr_filter_v1(sbi)", "header_size > vi->xattr_isize", "erofs_xxh32(filter_name, filter_name_len", "erofs_put_metabuf(&buf)", ): if marker not in filter_negative: raise SystemExit(f"fast-negative integrity marker missing: {marker}") if any(marker in filter_negative.lower() for marker in ("malloc", "mtx_", "cv_", "cache")): raise SystemExit("B19b introduced cache/allocation/locking into the fast-negative") if getxattr.index("erofs_xattr_filter_negative") > getxattr.index("erofs_xattr_load_body"): raise SystemExit("fast-negative runs after the full xattr body read") if "if (filter_negative)\n\t\treturn (ENOATTR);" not in getxattr: raise SystemExit("getxattr does not restrict the shortcut to proven negatives") if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", header + internal + xattr): raise SystemExit("Linux negative errno entered the FreeBSD implementation") for marker in ( "xxh32(name, strlen(name)", "EROFS_XATTR_FILTER_SEED + index", "vi->xattr_name_filter & (1U << hashbit)", "!sbi->xattr_filter_reserved", ): if marker not in linux_xattr: raise SystemExit(f"Linux comparison anchor changed: {marker}") changed = subprocess.run( ["git", "-C", str(root), "diff", "--name-only", spec["gate"]["commit"], "--", "repo-pre-15/src"], check=True, text=True, stdout=subprocess.PIPE, ).stdout.splitlines() expected = ["repo-pre-15/src/erofs_fs.h", "repo-pre-15/src/internal.h", "repo-pre-15/src/xattr.c"] if sorted(changed) != expected: raise SystemExit(f"B19b source write set differs: {changed}") return { "freebsd_errno": "positive", "freebsd_namespace": "extattr user/system mapping retained", "hash_symbol": "file-local erofs_xxh32", "linux_semantics": "xxh32(name suffix, seed + index), inverse 32-bit filter", "source_write_set": expected, "status": "PASS", } def verify(fixtures: Path, spec: dict, root: Path, dut: Path) -> dict: hashes = {path.name: sha256(path) for path in sorted(fixtures.glob("*.erofs"))} if hashes != spec["fixture_sha256"]: raise SystemExit(f"fixture hashes differ: {hashes}") cases = [ ("hit", "valid.erofs", b"attr00", "PASS", True), ("miss", "valid.erofs", spec["fixture"]["miss"].encode(), "ENOATTR", False), ("false-positive", "valid.erofs", spec["fixture"]["collision"].encode(), "ENOATTR", True), ("unknown-filter", "unknown-filter.erofs", spec["fixture"]["miss"].encode(), "ENOATTR", True), ("feature-off", "feature-off.erofs", spec["fixture"]["miss"].encode(), "ENOATTR", True), ("corrupt-shared-count", "corrupt-shared-count.erofs", spec["fixture"]["miss"].encode(), "EINTEGRITY", None), ("corrupt-shared-id", "corrupt-shared-id.erofs", b"attr00", "EINTEGRITY", None), ] results = [] for identifier, image, name, expected_status, expected_scan in cases: baseline = lookup(fixtures / image, name, False, spec) candidate = lookup(fixtures / image, name, True, spec) if baseline["status"] != expected_status or candidate["status"] != expected_status: raise SystemExit(f"{identifier} status differs: {baseline}, {candidate}") if baseline["errno"] < 0 or candidate["errno"] < 0: raise SystemExit(f"{identifier} returned negative errno") if expected_scan is not None and candidate["reads"]["scanned"] != expected_scan: raise SystemExit(f"{identifier} scan decision differs") results.append({"baseline": baseline, "candidate": candidate, "id": identifier, "name": name.decode()}) baseline_calls = [] candidate_calls = [] baseline_blocks = [] candidate_blocks = [] for _ in range(spec["thresholds"]["samples"]): baseline = lookup(fixtures / "valid.erofs", spec["fixture"]["miss"].encode(), False, spec)["reads"] candidate = lookup(fixtures / "valid.erofs", spec["fixture"]["miss"].encode(), True, spec)["reads"] baseline_calls.append(baseline["calls"]) candidate_calls.append(candidate["calls"]) baseline_blocks.append(baseline["provider_blocks"]) candidate_blocks.append(candidate["provider_blocks"]) call_reduction = 100 * (statistics.median(baseline_calls) - statistics.median(candidate_calls)) / statistics.median(baseline_calls) block_reduction = 100 * (statistics.median(baseline_blocks) - statistics.median(candidate_blocks)) / statistics.median(baseline_blocks) threshold = spec["thresholds"]["minimum_provider_metadata_read_reduction_percent"] if call_reduction < threshold or block_reduction < threshold: raise SystemExit("B19b source model no longer meets the measured benefit gate") return { "benchmark": { "baseline_calls": baseline_calls, "baseline_provider_blocks": baseline_blocks, "call_reduction_percent": call_reduction, "candidate_calls": candidate_calls, "candidate_provider_blocks": candidate_blocks, "provider_block_reduction_percent": block_reduction, "threshold_percent": threshold, }, "cases": results, "fixture_sha256": hashes, "source": audit_source(root, dut, spec), "status": "PASS", "test": spec["test"], } def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--dut", type=Path, required=True) parser.add_argument("--fixtures", type=Path, required=True) parser.add_argument("--report", type=Path, required=True) parser.add_argument("--root", type=Path, required=True) parser.add_argument("--spec", type=Path, required=True) args = parser.parse_args() spec = json.loads(args.spec.read_text(encoding="ascii")) if spec.get("schema") != 1 or spec.get("batch") != "B19b": raise SystemExit("invalid B19b oracle spec") report = verify(args.fixtures, spec, args.root, args.dut) write_json(args.report, report) print(json.dumps({"case_count": len(report["cases"]), "status": "PASS", "test": report["test"]}, sort_keys=True)) if __name__ == "__main__": main()