Files
erofs-freebsd-out-tree/tests/pre15/gates/P15-052.sh
T
2026-08-18 09:20:44 +02:00

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