From eaa9f4bf081010db5cdc933d0e92139080c7b997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eren=20Ar=C4=B1?= Date: Sat, 19 Sep 2026 16:14:28 +0300 Subject: [PATCH 1/6] research: add fail-closed repeat manifest projection --- .../research/project-repeat-manifest-v1.py | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 scripts/research/project-repeat-manifest-v1.py diff --git a/scripts/research/project-repeat-manifest-v1.py b/scripts/research/project-repeat-manifest-v1.py new file mode 100644 index 0000000..ae4984e --- /dev/null +++ b/scripts/research/project-repeat-manifest-v1.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +"""Create a single-profile repeat-execution projection of a frozen manifest. + +The frozen research manifest remains the source of truth. This helper changes +only its top-level required_profiles list so BPFCompat can execute one sampled +profile without requiring the other nine profiles to be present in the repeat +matrix. The source Git blob is verified before projection and projection +metadata is emitted for archival provenance. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import tempfile +from pathlib import Path + +PROFILE_ID_RE = re.compile(r"^[A-Za-z0-9._-]+$") +PROFILE_ITEM_RE = re.compile(r"^ - ([A-Za-z0-9._-]+)\s*$") + + +def sha256_bytes(data: bytes) -> str: + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def git_blob_sha1(data: bytes) -> str: + header = f"blob {len(data)}\0".encode() + return hashlib.sha1(header + data).hexdigest() + + +def locate_required_profiles(text: str) -> tuple[list[str], int, int, list[str], str]: + lines = text.splitlines(keepends=True) + key_indexes = [ + i for i, line in enumerate(lines) + if line.rstrip("\r\n") == "required_profiles:" + ] + if len(key_indexes) != 1: + raise ValueError( + f"expected exactly one top-level required_profiles block, found {len(key_indexes)}" + ) + + key_index = key_indexes[0] + newline = "\n" + if lines[key_index].endswith("\r\n"): + newline = "\r\n" + + profiles: list[str] = [] + end = key_index + 1 + while end < len(lines): + raw = lines[end].rstrip("\r\n") + match = PROFILE_ITEM_RE.fullmatch(raw) + if match: + profiles.append(match.group(1)) + end += 1 + continue + + # A blank/comment or the next top-level key belongs to the suffix. + if raw.strip() == "" or raw.startswith("#") or not raw.startswith((" ", "\t")): + break + + # Any other indented content means the frozen block is not in the + # simple list form this fail-closed projection understands. + raise ValueError( + f"unsupported content inside required_profiles block at line {end + 1}: {raw!r}" + ) + + if not profiles: + raise ValueError("required_profiles block is empty") + + if len(set(profiles)) != len(profiles): + raise ValueError("required_profiles contains duplicate profile IDs") + + return lines, key_index, end, profiles, newline + + +def project_manifest( + source: Path, + profile_id: str, + expected_git_blob: str, + out: Path, + metadata_out: Path, +) -> dict[str, object]: + if not PROFILE_ID_RE.fullmatch(profile_id): + raise ValueError(f"unsafe profile id: {profile_id!r}") + + data = source.read_bytes() + actual_git_blob = git_blob_sha1(data) + expected = expected_git_blob.removeprefix("git-blob:") + if actual_git_blob != expected: + raise ValueError( + "source manifest Git blob mismatch: " + f"expected {expected}, got {actual_git_blob}" + ) + + text = data.decode("utf-8") + lines, key_index, end, profiles, newline = locate_required_profiles(text) + if profile_id not in profiles: + raise ValueError( + f"profile {profile_id!r} is not required by frozen manifest" + ) + + projected_lines = ( + lines[: key_index + 1] + + [f" - {profile_id}{newline}"] + + lines[end:] + ) + projected = "".join(projected_lines).encode("utf-8") + + # Re-parse the projection to prove the transformation did exactly what it + # intended at the required_profiles boundary. + _, _, _, projected_profiles, _ = locate_required_profiles( + projected.decode("utf-8") + ) + if projected_profiles != [profile_id]: + raise ValueError("projected manifest did not produce a singleton profile list") + + out.parent.mkdir(parents=True, exist_ok=True) + metadata_out.parent.mkdir(parents=True, exist_ok=True) + out.write_bytes(projected) + + metadata: dict[str, object] = { + "schema_version": "bpfcompat.research.repeat-manifest-projection.v1", + "projection": "required_profiles_singleton", + "source_path": source.as_posix(), + "source_git_blob": actual_git_blob, + "source_sha256": sha256_bytes(data), + "profile_id": profile_id, + "source_required_profiles": profiles, + "projected_required_profiles": [profile_id], + "projected_path": out.as_posix(), + "projected_sha256": sha256_bytes(projected), + } + metadata_out.write_text( + json.dumps(metadata, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + return metadata + + +def self_test() -> int: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "source.yaml" + out = root / "projected.yaml" + meta = root / "projection.json" + source.write_text( + "name: demo\n" + "programs:\n" + " - name: p\n" + "required_profiles:\n" + " - ubuntu-20.04-5.4\n" + " - ubuntu-22.04-5.15\n" + "metadata:\n" + " owner: demo\n", + encoding="utf-8", + ) + blob = git_blob_sha1(source.read_bytes()) + metadata = project_manifest( + source, + "ubuntu-22.04-5.15", + blob, + out, + meta, + ) + projected = out.read_text(encoding="utf-8") + assert " - ubuntu-22.04-5.15\n" in projected + assert " - ubuntu-20.04-5.4\n" not in projected + assert "metadata:\n owner: demo\n" in projected + assert metadata["source_git_blob"] == blob + assert metadata["projected_required_profiles"] == ["ubuntu-22.04-5.15"] + + try: + project_manifest(source, "debian-12-6.1", blob, out, meta) + except ValueError as exc: + assert "not required by frozen manifest" in str(exc) + else: + raise AssertionError("missing-profile projection unexpectedly succeeded") + + try: + project_manifest(source, "ubuntu-22.04-5.15", "0" * 40, out, meta) + except ValueError as exc: + assert "Git blob mismatch" in str(exc) + else: + raise AssertionError("wrong source blob unexpectedly succeeded") + + print("[project-repeat-manifest-v1] self-test PASS") + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--self-test", action="store_true") + ap.add_argument("--source") + ap.add_argument("--profile-id") + ap.add_argument("--expected-git-blob") + ap.add_argument("--out") + ap.add_argument("--metadata-out") + args = ap.parse_args() + + if args.self_test: + return self_test() + + required = { + "--source": args.source, + "--profile-id": args.profile_id, + "--expected-git-blob": args.expected_git_blob, + "--out": args.out, + "--metadata-out": args.metadata_out, + } + missing = [flag for flag, value in required.items() if not value] + if missing: + ap.error("missing required arguments: " + ", ".join(missing)) + + try: + project_manifest( + Path(args.source), + args.profile_id, + args.expected_git_blob, + Path(args.out), + Path(args.metadata_out), + ) + except (OSError, UnicodeError, ValueError) as exc: + raise SystemExit(f"repeat manifest projection failed: {exc}") from exc + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From b51c88cd6ca83567271aa899a33b61daec7deb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eren=20Ar=C4=B1?= Date: Sat, 19 Sep 2026 16:14:54 +0300 Subject: [PATCH 2/6] research: project frozen manifests for sampled repeat profiles --- scripts/research/run-repeat-v1.sh | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/scripts/research/run-repeat-v1.sh b/scripts/research/run-repeat-v1.sh index 6cee8c8..08aa3ca 100644 --- a/scripts/research/run-repeat-v1.sh +++ b/scripts/research/run-repeat-v1.sh @@ -5,6 +5,7 @@ BUNDLE="${1:-dist/research-corpus-v1}" REPORTS="${2:-reports/research-repeat-v1}" SAMPLE="${3:-research/repeat/v1/stability-sample.json}" PLAN="research/corpus/v1/study-plan.json" +PROJECTION_SCRIPT="scripts/research/project-repeat-manifest-v1.py" fail() { echo "[research-repeat-v1] $*" >&2 @@ -14,6 +15,7 @@ fail() { [[ -x "$BUNDLE/bin/bpfcompat-linux-amd64" ]] || fail "missing materialized BPFCompat CLI" [[ -x "$BUNDLE/bin/bpfcompat-validator-static-linux-amd64" ]] || fail "missing materialized validator" [[ -s "$SAMPLE" && -s "$PLAN" ]] || fail "missing repeat sample or study plan" +[[ -s "$PROJECTION_SCRIPT" ]] || fail "missing repeat manifest projection helper" bash scripts/research/verify-materialization-v1.sh "$BUNDLE" bash scripts/research/verify-profile-lock-v1.sh @@ -22,7 +24,7 @@ export BPFCOMPAT_VALIDATOR_BIN="$PWD/$BUNDLE/bin/bpfcompat-validator-static-linu export BPFCOMPAT_VALIDATOR_SHA256="4ae1d5b838be07e6e7c304d753389a239c19eb92f6ba3bd77657e5c9583b9d04" BPF="$PWD/$BUNDLE/bin/bpfcompat-linux-amd64" -mkdir -p "$REPORTS/raw" "$REPORTS/logs" "$REPORTS/matrices" "$REPORTS/normalized" +mkdir -p "$REPORTS/raw" "$REPORTS/logs" "$REPORTS/matrices" "$REPORTS/manifests" "$REPORTS/normalized" jq -n \ --arg workflow_source_commit "$(git rev-parse HEAD)" \ @@ -30,6 +32,7 @@ jq -n \ --arg materialization_sha256 "sha256:$(sha256sum "$BUNDLE/materialization.json" | awk '{print $1}')" \ --arg study_plan_sha256 "sha256:$(sha256sum "$PLAN" | awk '{print $1}')" \ --arg profile_lock_sha256 "sha256:$(sha256sum research/corpus/v1/profile-identities.json | awk '{print $1}')" \ + --arg projection_script_sha256 "sha256:$(sha256sum "$PROJECTION_SCRIPT" | awk '{print $1}')" \ --arg cli_sha256 "sha256:$(sha256sum "$BUNDLE/bin/bpfcompat-linux-amd64" | awk '{print $1}')" \ --arg validator_sha256 "sha256:$(sha256sum "$BUNDLE/bin/bpfcompat-validator-static-linux-amd64" | awk '{print $1}')" \ --arg cilium_loader_sha256 "sha256:$(sha256sum "$BUNDLE/loaders/ebpf-go-loader" | awk '{print $1}')" \ @@ -42,6 +45,7 @@ jq -n \ materialization_sha256:$materialization_sha256, study_plan_sha256:$study_plan_sha256, profile_lock_sha256:$profile_lock_sha256, + manifest_projection_script_sha256:$projection_script_sha256, bpfcompat_cli_sha256:$cli_sha256, validator_sha256:$validator_sha256, loaders:{ @@ -102,6 +106,7 @@ while IFS= read -r tuple_json; do manifest="$(jq -r '.manifest // empty' <<<"$case_json")" command_binary="$(jq -r '.command_binary // empty' <<<"$case_json")" command="$(jq -r '.command // empty' <<<"$case_json")" + manifest_git_blob="$(jq -r '.manifest_git_blob // empty' <<<"$case_json")" matrix="$REPORTS/matrices/$tuple_id.yaml" cat > "$matrix" < Date: Sat, 19 Sep 2026 16:15:13 +0300 Subject: [PATCH 3/6] research: regression-test repeat manifest projections --- .github/workflows/research-repeat-v1.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/research-repeat-v1.yml b/.github/workflows/research-repeat-v1.yml index c1098a8..3dd095f 100644 --- a/.github/workflows/research-repeat-v1.yml +++ b/.github/workflows/research-repeat-v1.yml @@ -10,6 +10,7 @@ on: - "research/corpus/v1/**" - "scripts/research/run-repeat-v1.sh" - "scripts/research/analyze-repeat-v1.py" + - "scripts/research/project-repeat-manifest-v1.py" - "scripts/research/materialize-v1.sh" - "scripts/research/verify-materialization-v1.sh" - "scripts/research/verify-profile-lock-v1.sh" @@ -37,7 +38,9 @@ jobs: set -euo pipefail bash -n scripts/research/run-repeat-v1.sh python3 -m py_compile scripts/research/analyze-repeat-v1.py + python3 -m py_compile scripts/research/project-repeat-manifest-v1.py python3 scripts/research/analyze-repeat-v1.py --self-test + python3 scripts/research/project-repeat-manifest-v1.py --self-test jq -e . research/repeat/v1/stability-sample.json >/dev/null - name: Verify sample bindings @@ -50,7 +53,8 @@ jobs: sample=json.loads(Path("research/repeat/v1/stability-sample.json").read_text()) plan=json.loads(Path("research/corpus/v1/study-plan.json").read_text()) lock=json.loads(Path("research/corpus/v1/profile-identities.json").read_text()) - cases={x["id"] for x in plan["cases"]} + case_map={x["id"]: x for x in plan["cases"]} + cases=set(case_map) profiles={x["id"] for x in lock["profiles"]} ids=set() for row in sample["tuples"]: @@ -58,6 +62,10 @@ jobs: ids.add(row["id"]) assert row["case_id"] in cases, row assert row["profile_id"] in profiles, row + case=case_map[row["case_id"]] + if case["mode"] in {"load_only", "load_attach"}: + assert case.get("manifest"), row + assert case.get("manifest_git_blob"), row shard=Path("research/data/v1/processed/executions")/f"{row['case_id']}.jsonl" matches=[json.loads(x) for x in shard.read_text().splitlines() if x.strip() and json.loads(x)["logical_profile_id"]==row["profile_id"]] From fe778ec09a0db8d84ee420d4610c1c6c247f70e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eren=20Ar=C4=B1?= Date: Sat, 19 Sep 2026 16:15:23 +0300 Subject: [PATCH 4/6] research: document repeat-only manifest projection --- research/repeat/v1/README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/research/repeat/v1/README.md b/research/repeat/v1/README.md index 9f75555..7e279a7 100644 --- a/research/repeat/v1/README.md +++ b/research/repeat/v1/README.md @@ -17,6 +17,15 @@ Each tuple is repeated three times in one workflow collection, for 21 planned attempts. Frozen v1 artifacts, loaders, validation semantics, and profile definitions are reused. +For libbpf-backed cases, the frozen v1 manifests list all ten study profiles in +`required_profiles`. The repeat study executes only one sampled profile per +tuple, so the runner creates a **repeat-only manifest projection** whose sole +change is narrowing `required_profiles` to that sampled profile. Before doing +so it verifies the frozen source manifest Git-blob identity from +`study-plan.json`. The projected manifest and a metadata record containing its +SHA-256, source Git blob, and projection rule are retained in the workflow +artifact. Program, attach, and validation semantics are not rewritten. + ## Interpretation The repeat analyzer distinguishes two failure modes: @@ -41,6 +50,7 @@ After this protocol lands on `main`: gh workflow run research-repeat-v1.yml --repo Kernel-Guard/bpfcompat --ref main ``` -The workflow publishes raw repeat reports, logs, a repeat provenance record, +The workflow publishes raw repeat reports, logs, repeat-only manifest +projections and their provenance metadata, a repeat provenance record, `repeat-executions.jsonl`, `stability-summary.json`, and generated `RESULTS.md` as a staging Actions artifact. From b1b06de599dc243c78ffcca1fd43ac967a0fc88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eren=20Ar=C4=B1?= Date: Sat, 19 Sep 2026 16:15:59 +0300 Subject: [PATCH 5/6] research: bind repeat samples to frozen manifest blobs --- .github/workflows/research-repeat-v1.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/research-repeat-v1.yml b/.github/workflows/research-repeat-v1.yml index 3dd095f..1574b7b 100644 --- a/.github/workflows/research-repeat-v1.yml +++ b/.github/workflows/research-repeat-v1.yml @@ -47,9 +47,13 @@ jobs: run: | set -euo pipefail python3 - <<'PY' + import hashlib import json from pathlib import Path + def git_blob_sha1(data): + return hashlib.sha1(f"blob {len(data)}\\0".encode() + data).hexdigest() + sample=json.loads(Path("research/repeat/v1/stability-sample.json").read_text()) plan=json.loads(Path("research/corpus/v1/study-plan.json").read_text()) lock=json.loads(Path("research/corpus/v1/profile-identities.json").read_text()) @@ -66,6 +70,10 @@ jobs: if case["mode"] in {"load_only", "load_attach"}: assert case.get("manifest"), row assert case.get("manifest_git_blob"), row + manifest=Path(case["manifest"]) + data=manifest.read_bytes() + assert git_blob_sha1(data)==case["manifest_git_blob"], row + assert f" - {row['profile_id']}" in data.decode(), row shard=Path("research/data/v1/processed/executions")/f"{row['case_id']}.jsonl" matches=[json.loads(x) for x in shard.read_text().splitlines() if x.strip() and json.loads(x)["logical_profile_id"]==row["profile_id"]] From 8c49776e643625e7d4e6cd195e71750cef58d62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eren=20Ar=C4=B1?= Date: Sat, 19 Sep 2026 16:16:44 +0300 Subject: [PATCH 6/6] research: fix preflight Git blob hashing --- .github/workflows/research-repeat-v1.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/research-repeat-v1.yml b/.github/workflows/research-repeat-v1.yml index 1574b7b..05ddca2 100644 --- a/.github/workflows/research-repeat-v1.yml +++ b/.github/workflows/research-repeat-v1.yml @@ -52,7 +52,8 @@ jobs: from pathlib import Path def git_blob_sha1(data): - return hashlib.sha1(f"blob {len(data)}\\0".encode() + data).hexdigest() + header=b"blob " + str(len(data)).encode() + bytes([0]) + return hashlib.sha1(header + data).hexdigest() sample=json.loads(Path("research/repeat/v1/stability-sample.json").read_text()) plan=json.loads(Path("research/corpus/v1/study-plan.json").read_text())