381 lines
12 KiB
Bash
Executable File
381 lines
12 KiB
Bash
Executable File
#!/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 <errno.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
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"
|