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