#!/bin/sh set -eu BASE=bd5a09054e5cf89efd4db82aadb051f20b06ebf7 FREEBSD_SRC=/work/build/freebsd-src OUTPUT= SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../../.." && pwd) INPUT=$SCRIPT_DIR/P15-032-input.json usage() { printf '%s\n' "usage: $0 --output ABSOLUTE_PATH [--base SHA] [--freebsd-src PATH]" >&2 exit 20 } while [ "$#" -gt 0 ]; do case "$1" in --output) [ "$#" -ge 2 ] || usage OUTPUT=$2 shift 2 ;; --base) [ "$#" -ge 2 ] || usage BASE=$2 shift 2 ;; --freebsd-src) [ "$#" -ge 2 ] || usage FREEBSD_SRC=$2 shift 2 ;; *) usage ;; esac done [ -n "$OUTPUT" ] || usage case "$OUTPUT" in /*) ;; *) printf '%s\n' "P15-032: --output must be absolute" >&2; exit 20 ;; esac if [ -d "$OUTPUT" ] && [ -n "$(find "$OUTPUT" -mindepth 1 -maxdepth 1 -print -quit)" ]; then printf '%s\n' "P15-032: --output must be empty" >&2 exit 20 fi mkdir -p "$OUTPUT" TMP=$(mktemp -d "${TMPDIR:-/tmp}/P15-032.XXXXXX") cleanup() { rm -rf "$TMP" } trap cleanup EXIT HUP INT TERM printf 'argv=%s\n' "$0 --output $OUTPUT --base $BASE --freebsd-src $FREEBSD_SRC" >"$OUTPUT/runner.argv" printf 'whole_gate_deadline_seconds=120\n' >"$OUTPUT/deadlines.txt" printf 'command_deadline_seconds=10\n' >>"$OUTPUT/deadlines.txt" printf 'compile_deadline_seconds=20\n' >>"$OUTPUT/deadlines.txt" printf 'model_deadline_seconds=10\n' >>"$OUTPUT/deadlines.txt" set +e /usr/bin/timeout 120s python3 - "$ROOT" "$FREEBSD_SRC" "$INPUT" "$OUTPUT" "$TMP" "$BASE" >"$OUTPUT/runner.stdout" 2>"$OUTPUT/runner.stderr" <<'PY' import datetime import hashlib import json import os from pathlib import Path import shlex import subprocess import sys root = Path(sys.argv[1]).resolve() freebsd = Path(sys.argv[2]).resolve() input_path = Path(sys.argv[3]).resolve() output = Path(sys.argv[4]).resolve() tmp = Path(sys.argv[5]).resolve() base = sys.argv[6] cfg = json.loads(input_path.read_text(encoding="utf-8")) command_timeout = cfg["timeouts_seconds"]["command"] commands = [] def run(argv, cwd, timeout=command_timeout, check=True): started = datetime.datetime.now(datetime.timezone.utc) cp = subprocess.run(argv, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout, check=False) commands.append({ "argv": argv, "cwd": str(cwd), "timeout_seconds": timeout, "started_utc": started.isoformat(), "exit": cp.returncode, "stdout": cp.stdout.decode("utf-8", "replace"), "stderr": cp.stderr.decode("utf-8", "replace"), }) if check and cp.returncode != 0: raise RuntimeError(f"command failed ({cp.returncode}): {shlex.join(argv)}") return cp def sha256(data): return hashlib.sha256(data).hexdigest() def bounded_read(path): data = path.read_bytes() if len(data) > 4 * 1024 * 1024: raise RuntimeError(f"refusing oversized contract file: {path}") return data def base_blob(path): return run(["git", "show", f"{base}:{path}"], root).stdout def line_of(text, marker): return text[:text.index(marker)].count("\n") + 1 started = datetime.datetime.now(datetime.timezone.utc) if base != cfg["frozen_base"]: raise RuntimeError("requested base does not match frozen base") head = run(["git", "rev-parse", "HEAD"], root).stdout.decode().strip() remote = run(["git", "rev-parse", "xdm/main"], root).stdout.decode().strip() if head != base or remote != base: raise RuntimeError(f"base mismatch: HEAD={head} xdm/main={remote} expected={base}") allowed = { "repo-pre-15/tests/pre15/gates/P15-032.sh", "repo-pre-15/tests/pre15/gates/P15-032-input.json", "repo-pre-15/docs/pre15-stage0/P15-032.md", } status_raw = run(["git", "status", "--porcelain=v1", "-z"], root).stdout entries = [entry for entry in status_raw.decode().split("\0") if entry] paths = set() for entry in entries: path = entry[3:] if " -> " in path: path = path.split(" -> ", 1)[1] paths.add(path) if not paths.issubset(allowed): raise RuntimeError(f"out-of-scope worktree paths: {sorted(paths - allowed)}") outer_head = run(["git", "rev-parse", "HEAD"], freebsd).stdout.decode().strip() if outer_head != cfg["freebsd_source"]["outer_git_head"]: raise RuntimeError(f"FreeBSD outer source identity mismatch: {outer_head}") contracts = [] source_hashes = {} for contract in cfg["contracts"]: kind = contract["root"] rel = contract["path"] if kind == "freebsd": data = bounded_read(freebsd / rel) elif kind == "repo_base": data = base_blob(rel) elif kind == "linux_reference": data = base_blob(rel) else: raise RuntimeError(f"unknown contract root: {kind}") actual = sha256(data) source_hashes[f"{kind}:{rel}"] = actual if actual != contract["sha256"]: raise RuntimeError(f"hash mismatch for {kind}:{rel}: {actual}") text = data.decode("utf-8", "replace") matches = [] for marker in contract.get("must_contain", []): if marker not in text: raise RuntimeError(f"missing contract marker in {rel}: {marker!r}") matches.append({"marker": marker, "line": line_of(text, marker)}) forbidden = [] for marker in contract.get("must_not_contain", []): present = marker in text forbidden.append({"marker": marker, "present": present}) if present: raise RuntimeError(f"forbidden bridge marker in {rel}: {marker!r}") contracts.append({"root": kind, "path": rel, "sha256": actual, "matches": matches, "negative_matches": forbidden}) events = cfg["required_events"] event_index = {name: index for index, name in enumerate(events)} required_order = [ ("check_self_identity", "first_io"), ("check_namespace_ancestors", "first_io"), ("trace_storage_ancestors", "first_io"), ("crhold_mount_cred", "first_io"), ("vop_set_text", "first_io"), ("register_upper_mount", "first_io"), ("io_complete", "drain_inflight"), ("drain_inflight", "unregister_upper_mount"), ("unregister_upper_mount", "vn_close"), ("vn_close", "crfree_mount_cred"), ("crfree_mount_cred", "release_last_reference"), ] for before, after in required_order: if event_index[before] >= event_index[after]: raise RuntimeError(f"invalid lifecycle order: {before} !< {after}") nodes = set() adj = {} for before, after in cfg["lock_edges"]: nodes.update((before, after)) adj.setdefault(before, []).append(after) visiting = set() visited = set() def visit(node): if node in visiting: raise RuntimeError(f"lock graph cycle at {node}") if node in visited: return visiting.add(node) for child in adj.get(node, []): visit(child) visiting.remove(node) visited.add(node) for node in sorted(nodes): visit(node) dot = ["digraph P15_032 {", " rankdir=LR;"] for before, after in cfg["lock_edges"]: dot.append(f' "{before}" -> "{after}";') dot.append("}") (output / "lockgraph.dot").write_text("\n".join(dot) + "\n", encoding="utf-8") invariants = cfg["required_invariants"] closeable = [name for name in invariants if name != "storage_ancestor_check_before_io"] controls = cfg["false_go_controls"] if set(controls) != set(closeable): raise RuntimeError("false-GO controls do not exactly cover closeable invariants") enum_lines = [f" INV_{name.upper()} = 1ULL << {index}," for index, name in enumerate(closeable)] required_mask = " |\n ".join(f"INV_{name.upper()}" for name in closeable) control_rows = ",\n".join( f' {{"{name}", INV_{name.upper()}}}' for name in controls ) model = f'''#include #include #include #include enum invariant {{ {os.linesep.join(enum_lines)} }}; struct scenario {{ uint64_t present; int storage_ancestry_identity_api; }}; struct control {{ const char *name; uint64_t bit; }}; static const uint64_t required = {required_mask}; static int visible_only_oracle(const struct scenario *scenario) {{ return ((scenario->present & required) == required ? 0 : EINVAL); }} static int rigorous_oracle(const struct scenario *scenario) {{ int error; error = visible_only_oracle(scenario); if (error != 0) return (error); if (!scenario->storage_ancestry_identity_api) return (EOPNOTSUPP); return (0); }} int main(void) {{ static const struct control controls[] = {{ {control_rows} }}; struct scenario candidate = {{ required, 0 }}; struct scenario mutation; size_t index; if (visible_only_oracle(&candidate) != 0) return (1); if (rigorous_oracle(&candidate) != EOPNOTSUPP) return (2); puts("hidden-md-edge: visible-only=GO rigorous=STOP:EOPNOTSUPP"); for (index = 0; index < sizeof(controls) / sizeof(controls[0]); index++) {{ mutation = candidate; mutation.present &= ~controls[index].bit; if (visible_only_oracle(&mutation) == 0 || rigorous_oracle(&mutation) == 0) return (3); printf("false-go-control:%s=REJECTED\\n", controls[index].name); }} puts("lifecycle-model=PASS"); return (0); }} ''' model_path = tmp / "P15-032-model.c" binary_path = tmp / "P15-032-model" model_path.write_text(model, encoding="utf-8") compile_cp = run(["/usr/bin/timeout", f"{cfg['timeouts_seconds']['compile']}s", "/usr/bin/cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-O2", "-o", str(binary_path), str(model_path)], tmp, timeout=cfg["timeouts_seconds"]["compile"] + 2) model_cp = run(["/usr/bin/timeout", f"{cfg['timeouts_seconds']['model']}s", str(binary_path)], tmp, timeout=cfg["timeouts_seconds"]["model"] + 2) (output / "owned-temp-model.c").write_bytes(model_path.read_bytes()) (output / "owned-temp-model.stdout").write_bytes(model_cp.stdout) (output / "owned-temp-model.stderr").write_bytes(model_cp.stderr) false_go = { "controls": [{"removed_invariant": name, "outcome": "REJECTED"} for name in controls], "adversarial_hidden_edge": { "topology": "regular vnode -> filesystem -> md GEOM provider -> private backing vnode", "visible_only_oracle": "GO", "rigorous_oracle": "STOP:EOPNOTSUPP", }, } (output / "false-go-controls.json").write_text(json.dumps(false_go, indent=2) + "\n") (output / "contracts.json").write_text(json.dumps(contracts, indent=2) + "\n") (output / "source-hashes.json").write_text(json.dumps(source_hashes, indent=2, sort_keys=True) + "\n") (output / "commands.json").write_text(json.dumps(commands, indent=2) + "\n") state = { "events": events, "required_order": required_order, "local_lifecycle": "PASS", "storage_ancestor_identity": "UNAVAILABLE", "typed_errno_if_detectable": cfg["typed_errno"]["cycle"], "typed_errno_for_unsupported_graph": cfg["typed_errno"]["unsupported_dependency_graph"], } (output / "state-model.json").write_text(json.dumps(state, indent=2) + "\n") result = { "unit": cfg["unit"], "batch": cfg["batch"], "classification": "GATE_STOP", "decision": "STOP", "exit": 10, "base": base, "head": head, "xdm_main": remote, "freebsd_git_head": outer_head, "freebsd_identity": cfg["freebsd_source"]["identity"], "started_utc": started.isoformat(), "finished_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(), "closeable_invariants": closeable, "blocking_condition": cfg["blocking_condition"], "source_changes_permitted": False, "tests_run": ["contract extraction", "owned-temp C lifecycle model", "adversarial false-GO controls"], "tests_omitted": ["D", "H", "K", "Q", "TC006", "TC179", "TC184", "full feature", "smoke"], } (output / "result.json").write_text(json.dumps(result, indent=2) + "\n") print("P15-032 STOP: no public identity-preserving VFS/GEOM-to-backing-vnode ancestry API") sys.exit(10) PY RC=$? set -e printf 'exit=%s\n' "$RC" >"$OUTPUT/runner.exit" case "$RC" in 10) printf '%s\n' GATE_STOP >"$OUTPUT/classification" ;; 124) printf '%s\n' RUNNER_TIMEOUT >"$OUTPUT/classification" ;; 0) printf '%s\n' GATE_GO >"$OUTPUT/classification" ;; *) printf '%s\n' RUNNER_FAILURE >"$OUTPUT/classification" ;; esac (cd "$OUTPUT" && find . -maxdepth 1 -type f ! -name manifest.sha256 -print | LC_ALL=C sort | xargs sha256sum) >"$OUTPUT/manifest.sha256" exit "$RC"