473 lines
16 KiB
Bash
Executable File
473 lines
16 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
|
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
|
input=$gate_dir/P15-052-input.json
|
|
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
|
base=
|
|
output=
|
|
|
|
while test "$#" -gt 0; do
|
|
case "$1" in
|
|
--base)
|
|
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
|
base=$2
|
|
shift 2
|
|
;;
|
|
--output)
|
|
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
|
output=$2
|
|
shift 2
|
|
;;
|
|
*)
|
|
printf 'unknown argument: %s\n' "$1" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
|
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
|
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; }
|
|
test -d "$freebsd_src/sys" || {
|
|
printf 'missing FreeBSD source tree: %s\n' "$freebsd_src" >&2
|
|
exit 2
|
|
}
|
|
for tool in git python3 sha256sum; do
|
|
command -v "$tool" >/dev/null 2>&1 || {
|
|
printf 'missing required host tool: %s\n' "$tool" >&2
|
|
exit 2
|
|
}
|
|
done
|
|
case "$output" in
|
|
/*) ;;
|
|
*) output=$PWD/$output ;;
|
|
esac
|
|
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
|
mkdir -p "$output"
|
|
|
|
python3 - "$root" "$input" "$base" "$output" "$freebsd_src" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(sys.argv[1])
|
|
INPUT = Path(sys.argv[2])
|
|
REQUESTED_BASE = sys.argv[3]
|
|
OUTPUT = Path(sys.argv[4])
|
|
FREEBSD_SRC = Path(sys.argv[5])
|
|
SPEC = json.loads(INPUT.read_text(encoding="ascii"))
|
|
|
|
U32_MAX = (1 << 32) - 1
|
|
U48_MAX = (1 << 48) - 1
|
|
U64_MAX = (1 << 64) - 1
|
|
I64_MAX = (1 << 63) - 1
|
|
|
|
|
|
def git(*args: str) -> str:
|
|
return subprocess.check_output(
|
|
["git", "-C", str(ROOT), *args], text=True
|
|
).strip()
|
|
|
|
|
|
def sha256(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def source_at(commit: str, path: str) -> str:
|
|
completed = subprocess.run(
|
|
["git", "-C", str(ROOT), "show", f"{commit}:{path}"],
|
|
check=False,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
if completed.returncode != 0:
|
|
raise SystemExit(f"cannot read {path} at {commit}: {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 match is None:
|
|
raise SystemExit(f"function not found: {name}")
|
|
start = source.rfind("\n\n", 0, match.start()) + 2
|
|
brace = source.find("{", match.end())
|
|
if brace < 0:
|
|
raise SystemExit(f"function body not found: {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] + "\n"
|
|
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 line_number(source: str, needle: str) -> int:
|
|
if source.count(needle) != 1:
|
|
raise SystemExit(
|
|
f"source needle must occur exactly once ({source.count(needle)}): {needle}"
|
|
)
|
|
return source.count("\n", 0, source.index(needle)) + 1
|
|
|
|
|
|
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-052":
|
|
raise SystemExit("invalid P15-052 gate input")
|
|
resolved = git("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
|
if resolved != SPEC["required_base"]:
|
|
raise SystemExit(
|
|
f"P15-052 must replay {SPEC['required_base']}, got {resolved}"
|
|
)
|
|
|
|
sources = {
|
|
path: source_at(resolved, path) for path in SPEC["source_sha256"]
|
|
}
|
|
source_hashes = {
|
|
path: sha256(text.encode("utf-8")) for path, text in sources.items()
|
|
}
|
|
for path, expected in SPEC["source_sha256"].items():
|
|
if source_hashes[path] != expected:
|
|
raise SystemExit(f"frozen source identity mismatch: {path}")
|
|
|
|
function_bodies: dict[str, str] = {}
|
|
for key, counts in SPEC["function_errno_counts"].items():
|
|
path, function = key.rsplit(":", 1)
|
|
body = extract_function(sources[path], function)
|
|
function_bodies[key] = body
|
|
for errno_name, expected_count in counts.items():
|
|
actual_count = body.count(f"return ({errno_name});")
|
|
if actual_count != expected_count:
|
|
raise SystemExit(
|
|
f"{key} {errno_name} count changed: {actual_count} != {expected_count}"
|
|
)
|
|
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", body):
|
|
raise SystemExit(f"negative errno entered FreeBSD function: {key}")
|
|
|
|
param_path = FREEBSD_SRC / "sys/amd64/include/param.h"
|
|
types_path = FREEBSD_SRC / "sys/sys/_types.h"
|
|
geom_path = FREEBSD_SRC / "sys/geom/geom.h"
|
|
for path in (param_path, types_path, geom_path):
|
|
if not path.is_file():
|
|
raise SystemExit(f"missing FreeBSD contract source: {path}")
|
|
param_source = param_path.read_text(encoding="utf-8")
|
|
types_source = types_path.read_text(encoding="utf-8")
|
|
geom_source = geom_path.read_text(encoding="utf-8")
|
|
page_match = re.search(r"^#define\s+PAGE_SHIFT\s+(\d+)\b", param_source, re.MULTILINE)
|
|
if page_match is None:
|
|
raise SystemExit("PAGE_SHIFT is absent from amd64 param.h")
|
|
page_shift = int(page_match.group(1))
|
|
if page_shift != 12:
|
|
raise SystemExit(f"unexpected audited amd64 PAGE_SHIFT: {page_shift}")
|
|
if "typedef\t__int64_t\t__off_t;" not in types_source:
|
|
raise SystemExit("FreeBSD off_t source is not the audited signed 64-bit type")
|
|
if "off_t\t\t\tmediasize;" not in geom_source:
|
|
raise SystemExit("GEOM mediasize is not the audited off_t field")
|
|
|
|
freebsd_head = subprocess.check_output(
|
|
["git", "-C", str(FREEBSD_SRC), "rev-parse", "HEAD"], text=True
|
|
).strip()
|
|
freebsd_contract = {
|
|
"head": freebsd_head,
|
|
"page_shift": page_shift,
|
|
"off_t": "signed-64",
|
|
"geom_mediasize": "off_t",
|
|
"files": {
|
|
str(path.relative_to(FREEBSD_SRC)): sha256(path.read_bytes())
|
|
for path in (param_path, types_path, geom_path)
|
|
},
|
|
}
|
|
|
|
# These are maxima after the current super/inode decode and mount-time media
|
|
# checks, not unconstrained C-structure values.
|
|
inode_slot_min = 32
|
|
inode_isize_max = 64
|
|
xattr_isize_max = 4 + 4 * ((1 << 16) - 2)
|
|
inode_off_max = I64_MAX - inode_slot_min
|
|
idx_base_max = inode_off_max + inode_isize_max + xattr_isize_max
|
|
entry_size_max = 8
|
|
chunkbits_min = 9
|
|
chunkbits_max = page_shift + 31
|
|
chunk_idx_max = I64_MAX >> chunkbits_min
|
|
blkaddr_max = U48_MAX
|
|
physical_max = blkaddr_max << page_shift
|
|
chunk_off_max = (1 << chunkbits_max) - 1
|
|
prefix_record_max = 256
|
|
prefix_off_max = U32_MAX * 4 + 255 * (3 + 2 + prefix_record_max)
|
|
xattr_base_max = U32_MAX << page_shift
|
|
xattr_relative_max = U32_MAX * 4
|
|
|
|
reachability: dict[str, tuple[bool, str, dict[str, int]]] = {
|
|
"data.chunk.inode_plus_isize": (
|
|
inode_off_max > U64_MAX - inode_isize_max,
|
|
"inode_off is bounded by mounted backing size minus one compact inode",
|
|
{"inode_off_max": inode_off_max, "inode_isize_max": inode_isize_max},
|
|
),
|
|
"data.chunk.isize_plus_xattr": (
|
|
inode_off_max + inode_isize_max > U64_MAX - xattr_isize_max,
|
|
"inode offset plus maximum 64-byte inode and 16-bit xattr body stays below UINT64_MAX",
|
|
{"metadata_base_max": inode_off_max + inode_isize_max, "xattr_isize_max": xattr_isize_max},
|
|
),
|
|
"data.chunk.align": (
|
|
idx_base_max > U64_MAX - (entry_size_max - 1),
|
|
"validated backing and bounded inode/xattr sizes leave alignment headroom",
|
|
{"idx_base_max": idx_base_max, "alignment_slack": entry_size_max - 1},
|
|
),
|
|
"data.chunk.index_multiply": (
|
|
chunk_idx_max > (U64_MAX - idx_base_max) // entry_size_max,
|
|
"size <= OFF_MAX and chunkbits >= 9 bound the index product",
|
|
{"idx_base_max": idx_base_max, "chunk_idx_max": chunk_idx_max, "entry_size_max": entry_size_max},
|
|
),
|
|
"data.chunk.image_size_shift": (
|
|
I64_MAX > U64_MAX,
|
|
"mount validates blocks << blkszbits <= GEOM off_t mediasize",
|
|
{"validated_image_bytes_max": I64_MAX},
|
|
),
|
|
"data.chunk.physical_shift": (
|
|
blkaddr_max > (U64_MAX >> page_shift),
|
|
"decoded chunk block address is at most 48 bits and blkszbits is at most PAGE_SHIFT",
|
|
{"blkaddr_max": blkaddr_max, "blkszbits_max": page_shift},
|
|
),
|
|
"data.chunk.physical_plus_offset": (
|
|
chunk_off_max > U64_MAX - physical_max,
|
|
"48-bit block address and maximum validated chunkbits leave addition headroom",
|
|
{"physical_max": physical_max, "chunk_off_max": chunk_off_max},
|
|
),
|
|
"xattr.backing_size.shift": (
|
|
I64_MAX > U64_MAX,
|
|
"mounted primary backing bytes are already bounded by GEOM off_t mediasize",
|
|
{"validated_backing_bytes_max": I64_MAX},
|
|
),
|
|
"xattr.metadata.align": (
|
|
prefix_off_max > U64_MAX - 3,
|
|
"32-bit prefix start, 8-bit count, and bounded records cannot approach UINT64_MAX",
|
|
{"prefix_off_max": prefix_off_max},
|
|
),
|
|
"xattr.metadata.header_add": (
|
|
False,
|
|
"the preceding off <= UINT64_MAX-3 check and 4-byte roundup imply aligned off <= UINT64_MAX-3, so adding uint16_t cannot overflow",
|
|
{"accepted_input_max": U64_MAX - 3, "aligned_off_max": U64_MAX - 3, "raw_len_size": 2},
|
|
),
|
|
"xattr.shared.base_shift": (
|
|
U32_MAX > (U64_MAX >> page_shift),
|
|
"xattr_blkaddr is 32-bit and blkszbits is at most PAGE_SHIFT",
|
|
{"xattr_blkaddr_max": U32_MAX, "blkszbits_max": page_shift},
|
|
),
|
|
"xattr.shared.base_plus_relative": (
|
|
xattr_relative_max > U64_MAX - xattr_base_max,
|
|
"32-bit xattr block address and 32-bit shared ID cannot overflow the 64-bit sum",
|
|
{"xattr_base_max": xattr_base_max, "relative_max": xattr_relative_max},
|
|
),
|
|
}
|
|
|
|
ledger = []
|
|
for target in SPEC["targets"]:
|
|
target_id = target["id"]
|
|
if target_id not in reachability:
|
|
raise SystemExit(f"target has no reachability proof: {target_id}")
|
|
body = function_bodies[f"{target['path']}:{target['function']}"]
|
|
if body.count(target["needle"]) != 1:
|
|
raise SystemExit(f"target source is absent or ambiguous: {target_id}")
|
|
reachable, reason, bounds = reachability[target_id]
|
|
ledger.append(
|
|
{
|
|
**target,
|
|
"line": line_number(sources[target["path"]], target["needle"]),
|
|
"source_unique": True,
|
|
"target_marker_reached": reachable,
|
|
"current_freebsd_return": "+EOVERFLOW",
|
|
"candidate_freebsd_return": "+EINTEGRITY",
|
|
"linux_semantic_return": "-EFSCORRUPTED (no direct checked counterpart)",
|
|
"source_class": "on-disk contradiction",
|
|
"replay_observation": reason,
|
|
"provenance_bounds": bounds,
|
|
}
|
|
)
|
|
|
|
data_source = sources["repo-pre-15/src/data.c"]
|
|
xattr_source = sources["repo-pre-15/src/xattr.c"]
|
|
linux_xattr = sources["src-linux/xattr.c"]
|
|
control_anchors = (
|
|
(xattr_source, "if (off > backing_size || (uint64_t)len > backing_size - off)\n\t\treturn (EINTEGRITY);"),
|
|
(xattr_source, "if (off > INT64_MAX)\n\t\t\treturn (EOVERFLOW);"),
|
|
(xattr_source, "error = EOPNOTSUPP;"),
|
|
(data_source, "return (ENXIO);"),
|
|
(data_source, "return (EIO);"),
|
|
(data_source, "if (len == 0) {\n\t\treturn (0);\n\t}"),
|
|
(linux_xattr, "ret = -EOPNOTSUPP;"),
|
|
(linux_xattr, "ret = -ENOMEM;"),
|
|
(linux_xattr, "return PTR_ERR(it->kaddr);"),
|
|
(linux_xattr, "return -EFSCORRUPTED;"),
|
|
)
|
|
for source, anchor in control_anchors:
|
|
if anchor not in source:
|
|
raise SystemExit(f"errno preservation anchor is absent: {anchor}")
|
|
|
|
abi_reachable = False
|
|
abi_reason = (
|
|
"erofs_xattr_read_backing checks off <= backing_size before off > INT64_MAX; "
|
|
"for a mounted primary backing, backing_size <= GEOM signed off_t mediasize"
|
|
)
|
|
controls = []
|
|
for control in SPEC["preservation_controls"]:
|
|
record: dict[str, Any] = dict(control)
|
|
record["source_anchors_verified"] = True
|
|
if control["id"] == "abi.off_gt_int64":
|
|
record["target_marker_reached"] = abi_reachable
|
|
record["replay_observation"] = abi_reason
|
|
controls.append(record)
|
|
|
|
unreachable = [record["id"] for record in ledger if not record["target_marker_reached"]]
|
|
status = "GO" if not unreachable else "STOP"
|
|
stop_reasons = [
|
|
{
|
|
"id": target_id,
|
|
"reason": next(
|
|
record["replay_observation"] for record in ledger if record["id"] == target_id
|
|
),
|
|
}
|
|
for target_id in unreachable
|
|
]
|
|
|
|
ledger_document = {
|
|
"schema": 1,
|
|
"gate": "G06",
|
|
"candidate": "P15-052",
|
|
"status": status,
|
|
"resolved_base": resolved,
|
|
"targets": ledger,
|
|
"preservation_controls": controls,
|
|
"freebsd_contract": freebsd_contract,
|
|
"prototype_identity": SPEC["prototype_identity"],
|
|
"prototype_disposition": (
|
|
"READY arithmetic vectors inject unconstrained internal or local values; "
|
|
"they are not independently reachable on-disk fixtures"
|
|
),
|
|
}
|
|
(OUTPUT / "branch-ledger.json").write_text(
|
|
json.dumps(ledger_document, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
|
)
|
|
|
|
tsv = [
|
|
"id\tfunction\tfield\tfreebsd_now\tfreebsd_candidate\tlinux_semantic\tmarker\tobservation"
|
|
]
|
|
for record in ledger:
|
|
tsv.append(
|
|
"\t".join(
|
|
(
|
|
record["id"],
|
|
record["function"],
|
|
record["field"],
|
|
record["current_freebsd_return"],
|
|
record["candidate_freebsd_return"],
|
|
record["linux_semantic_return"],
|
|
"reached" if record["target_marker_reached"] else "not-reached",
|
|
record["replay_observation"],
|
|
)
|
|
)
|
|
)
|
|
(OUTPUT / "branch-ledger.tsv").write_text("\n".join(tsv) + "\n", encoding="ascii")
|
|
|
|
control_tsv = ["id\tfreebsd\tlinux\tresult\tmarker"]
|
|
for record in controls:
|
|
control_tsv.append(
|
|
"\t".join(
|
|
(
|
|
record["id"],
|
|
record["freebsd"],
|
|
record["linux"],
|
|
record["result"],
|
|
(
|
|
"not-reached"
|
|
if record.get("target_marker_reached") is False
|
|
else "preserved"
|
|
),
|
|
)
|
|
)
|
|
)
|
|
(OUTPUT / "preservation-ledger.tsv").write_text(
|
|
"\n".join(control_tsv) + "\n", encoding="ascii"
|
|
)
|
|
(OUTPUT / "source-sha256.json").write_text(
|
|
json.dumps(source_hashes, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
|
)
|
|
(OUTPUT / "freebsd-contract.json").write_text(
|
|
json.dumps(freebsd_contract, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
|
)
|
|
|
|
result = {
|
|
"schema": 1,
|
|
"gate": "G06",
|
|
"candidate": "P15-052",
|
|
"status": status,
|
|
"requested_base": REQUESTED_BASE,
|
|
"resolved_base": resolved,
|
|
"target_count": len(ledger),
|
|
"target_markers_reached": len(ledger) - len(unreachable),
|
|
"target_markers_not_reached": len(unreachable),
|
|
"source_unique_count": sum(record["source_unique"] for record in ledger),
|
|
"preservation_control_count": len(controls),
|
|
"abi_marker_reached": abi_reachable,
|
|
"stop_reasons": stop_reasons,
|
|
"b20": "STOP-NO-SOURCE",
|
|
"b21": "NOT_RUN",
|
|
"qemu": "NOT_RUN",
|
|
"qemu_reason": "host-proven target reachability failure",
|
|
"full_feature_suite": "NOT_RUN",
|
|
}
|
|
(OUTPUT / "result.json").write_text(
|
|
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
|
)
|
|
|
|
hash_lines = []
|
|
for path in sorted(OUTPUT.iterdir()):
|
|
if path.is_file() and path.name != "SHA256SUMS":
|
|
hash_lines.append(f"{sha256(path.read_bytes())} {path.name}")
|
|
(OUTPUT / "SHA256SUMS").write_text("\n".join(hash_lines) + "\n", encoding="ascii")
|
|
print(json.dumps(result, sort_keys=True))
|
|
if status != "GO":
|
|
raise SystemExit(1)
|
|
PY
|