diff --git a/CHANGELOG.md b/CHANGELOG.md index 66652e8..23ff629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Guard the six-script R2 Fractal resync with immutable revisions, exact blobs and executable modes; expire the exception on the next oracle repin (#115). + ## [0.6.0] - 2026-11-10 ### Added diff --git a/flake.nix b/flake.nix index d7474c8..302f540 100644 --- a/flake.nix +++ b/flake.nix @@ -67,6 +67,7 @@ # Inspect every Git-source entry, including names cleanSource excludes. src = self; MERGEMASTER_PYTHON_ORACLE = "${python-oracle}"; + MERGEMASTER_FRACTAL_FIXTURE = "${fractal-fixture}"; } '' python3 -B "$src/tests/test-oracle-scripts.py" python3 -B "$src/tests/oracle_scripts.py" diff --git a/scripts/PROVENANCE b/scripts/PROVENANCE index aa042a4..d4e5b6f 100644 --- a/scripts/PROVENANCE +++ b/scripts/PROVENANCE @@ -1,6 +1,17 @@ # Source: compoundingtech/fractal # Commit: e83871ec919f95b3b3e2a9a1cd211c2ac37babda # Scripts copied as-is except the documented upstream patches below; Git blob SHA-1 hashes (not file SHA-1 hashes). +# R2 resync exception; exit: https://github.com/compoundingtech/mergemaster/issues/115 +# The original extraction Commit above is historical; this resync starts at the current fixture revision. +# Pending requires oracle bytes; the target fixture activates exact upstream bytes and modes. +# resync-original e2721e52463428b4ff655addee129cecd2839763 +# resync-rev 787e8ed592dff34d2bdc2a2b0b584482da567e7f +# resync-blob scripts/evidence-sign ea0ab64b5efc631c624cc974208bf05234839433 +# resync-blob scripts/evidence-sign-hardening-test 7da656fdc8f2b4b499fa099c93efa34884434004 +# resync-blob scripts/evidence-verify-test 6b9d6c5f403fae4fcb1a516562e06f8d5cfc0cc7 +# resync-blob scripts/evidence-wrap-test 711847812a5d703fa9f2056468970f89a0f57c53 +# resync-blob scripts/lane-definition-closure 5a844628e821bb51e6ab3fc2b38088a37c3c47c4 +# resync-blob scripts/st-test feb0d16374a2dfb78af20357166f56225df46774 # scripts/fractal-test-test: apply the fixture maintenance-isolation patch from # https://github.com/compoundingtech/fractal/pull/4398 (5f995f26e5). # The source_blob column remains the original import; mergemaster_blob includes applied patches. diff --git a/tests/oracle_scripts.py b/tests/oracle_scripts.py index 86e0134..b7e905a 100644 --- a/tests/oracle_scripts.py +++ b/tests/oracle_scripts.py @@ -1,8 +1,38 @@ -"""Require copied scripts to match the immutable Python oracle, in both directions.""" +"""Byte/mode oracle guard with the bounded PROVENANCE resync exception.""" +import hashlib +import json import os from pathlib import Path +import re EXEMPT = {"PROVENANCE", "test-scripts"} +ORIGINAL = "e2721e52463428b4ff655addee129cecd2839763" +TARGET = "787e8ed592dff34d2bdc2a2b0b584482da567e7f" + + +def blob(data): + return hashlib.sha1(b"blob " + str(len(data)).encode() + b"\0" + data).hexdigest() + + +def table(text): + revisions, original, rows = [], [], {} + for line in text.splitlines(): + if line.startswith("# resync-original "): + original.append(line.removeprefix("# resync-original ")) + elif line.startswith("# resync-rev "): + revisions.append(line.removeprefix("# resync-rev ")) + elif line.startswith("# resync-blob "): + path, oid = line.removeprefix("# resync-blob ").split() + if not re.fullmatch(r"scripts/[A-Za-z0-9._-]+", path) or path in rows: + raise ValueError("invalid/duplicate resync path") + if not re.fullmatch(r"[0-9a-f]{40}", oid) or path[8:] in EXEMPT: + raise ValueError("invalid resync blob") + rows[path] = oid + if len(revisions) != 1 or len(original) != 1 or not rows: + raise ValueError("resync requires one target, one original revision and paths") + if ORIGINAL == TARGET or original[0] != ORIGINAL or revisions[0] != TARGET: + raise ValueError("resync revision differs from approved constants") + return rows def snapshot(path): @@ -11,12 +41,25 @@ def snapshot(path): return path.read_bytes(), path.stat().st_mode & 0o111 -def check(src, oracle): +def check(src, oracle, fixture=None, fixture_rev=ORIGINAL): + rows = table((src / "scripts/PROVENANCE").read_text()) + if fixture_rev not in (ORIGINAL, TARGET): + raise ValueError("fixture revision differs from approved constants") + for path, oid in rows.items(): + local, old = snapshot(src / path), snapshot(oracle / path) + if blob(old[0]) == oid: + raise ValueError(f"{path}: expired resync exception; remove it (#115)") + if fixture_rev == TARGET: + upstream = snapshot(fixture / path) + if blob(upstream[0]) != oid or local != upstream or local[0] == old[0]: + raise ValueError(f"{path}: resync bytes/mode/blob mismatch") + elif local != old: + raise ValueError(f"{path}: pending resync must still equal oracle") for path in sorted((src / "scripts").rglob("*")): if path.is_dir() and not path.is_symlink(): continue relative = path.relative_to(src).as_posix() - if relative[8:] in EXEMPT: + if relative[8:] in EXEMPT or relative in rows: continue if snapshot(path) != snapshot(oracle / relative): raise ValueError(f"{relative}: differs from pinned oracle") @@ -29,4 +72,8 @@ def check(src, oracle): if __name__ == "__main__": - check(Path(os.environ["src"]), Path(os.environ["MERGEMASTER_PYTHON_ORACLE"])) + src = Path(os.environ["src"]) + lock = json.loads((src / "flake.lock").read_text()) + node = lock["nodes"][lock["nodes"]["root"]["inputs"]["fractal-fixture"]] + check(src, Path(os.environ["MERGEMASTER_PYTHON_ORACLE"]), + Path(os.environ["MERGEMASTER_FRACTAL_FIXTURE"]), node["locked"]["rev"]) diff --git a/tests/test-oracle-scripts.py b/tests/test-oracle-scripts.py index d669141..875fce4 100644 --- a/tests/test-oracle-scripts.py +++ b/tests/test-oracle-scripts.py @@ -2,49 +2,68 @@ from pathlib import Path import unittest -from oracle_scripts import EXEMPT, check +from oracle_scripts import EXEMPT, ORIGINAL, TARGET, blob, check -class OracleGuard(unittest.TestCase): +class ResyncGuard(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) - self.src, self.oracle = [Path(self.temp.name) / p for p in ("src", "oracle")] - for root in (self.src, self.oracle): + self.src, self.oracle, self.fixture = [Path(self.temp.name) / p for p in ("src", "oracle", "fixture")] + for root in (self.src, self.oracle, self.fixture): (root / "scripts").mkdir(parents=True) - self.write(root, "one", b"old") + self.old, self.target = ORIGINAL, TARGET + self.oid = blob(b"new") + self.write(self.src, "one", b"new") + self.write(self.oracle, "one", b"old") + self.write(self.fixture, "one", b"new") + self.manifest() def write(self, root, name, data, mode=0o755): path = root / "scripts" / name path.write_bytes(data) path.chmod(mode) - def run_check(self): - check(self.src, self.oracle) + def manifest(self): + (self.src / "scripts/PROVENANCE").write_text( + f"# resync-original {self.old}\n# resync-rev {self.target}\n# resync-blob scripts/one {self.oid}\n") + + def run_check(self, rev=None): + check(self.src, self.oracle, self.fixture, rev or self.target) def test_match(self): self.run_check() - def test_repository_owned_exemptions(self): - for name in EXEMPT: - self.write(self.src, name, b"local") - self.write(self.oracle, name, b"oracle") - self.run_check() + def test_pending_is_oracle_only(self): + with self.assertRaises(ValueError): + self.run_check(self.old) + self.write(self.src, "one", b"old") + self.run_check(self.old) + with self.assertRaises(ValueError): + self.run_check("c" * 40) - def test_local_mismatch(self): - for data in (b"edited", b"new"): + def test_local_mismatch_and_oracle_copy(self): + for data in (b"edited", b"old"): self.write(self.src, "one", data) with self.assertRaises(ValueError): self.run_check() - def test_executable_modes(self): - self.write(self.src, "one", b"old", 0o644) + def test_fixture_and_table_must_agree(self): + self.write(self.fixture, "one", b"edited") + with self.assertRaises(ValueError): + self.run_check() + self.write(self.fixture, "one", b"new") + self.oid = blob(b"edited") + self.manifest() with self.assertRaises(ValueError): self.run_check() - def test_oracle_script_deletion_is_refused(self): + def test_modes_and_missing_paths(self): + self.write(self.src, "one", b"new", 0o644) + with self.assertRaises(ValueError): + self.run_check() (self.src / "scripts/one").unlink() - with self.assertRaisesRegex(ValueError, "missing oracle script"): + with self.assertRaises(ValueError): self.run_check() def test_unlisted_stays_oracle(self): @@ -55,21 +74,55 @@ def test_unlisted_stays_oracle(self): with self.assertRaises(ValueError): self.run_check() - def test_unlisted_addition_is_refused(self): - for name in ("not-in-oracle", "parity-drift", "parity-drift-test"): - with self.subTest(name=name): - self.write(self.src, name, b"new") - with self.assertRaisesRegex(ValueError, "not a regular script"): - self.run_check() - (self.src / "scripts" / name).unlink() + def test_expiry_is_byte_identical_even_if_modes_differ(self): + self.write(self.oracle, "one", b"new", 0o644) + with self.assertRaisesRegex(ValueError, "expired"): + self.run_check() + + def test_manifest_cannot_retarget_approved_revisions(self): + for original, target in [("c" * 40, "c" * 40), (ORIGINAL, "c" * 40), + (TARGET, TARGET), (ORIGINAL, ORIGINAL)]: + self.old, self.target = original, target + self.manifest() + with self.assertRaisesRegex(ValueError, "approved constants"): + self.run_check(target) - def test_symlink_is_not_a_script(self): + def test_duplicate_or_malformed_rows(self): + path = self.src / "scripts/PROVENANCE" + original = path.read_text() + for row in [f"# resync-blob scripts/one {self.oid}", + "# resync-blob scripts/../escape " + self.oid, + "# resync-blob scripts/two invalid", + "# resync-blob scripts/two"]: + path.write_text(original + row + "\n") + with self.assertRaises(ValueError): + self.run_check() + + def test_listed_symlink_is_not_a_script(self): path = self.src / "scripts/one" path.unlink() - path.symlink_to(self.oracle / "scripts/one") + path.symlink_to(self.fixture / "scripts/one") with self.assertRaisesRegex(ValueError, "not a regular script"): self.run_check() + def test_repository_owned_exemptions(self): + for name in EXEMPT - {"PROVENANCE"}: + self.write(self.src, name, b"local") + self.write(self.oracle, name, b"oracle") + self.run_check() + + def test_oracle_script_deletion_is_refused(self): + self.write(self.oracle, "two", b"old") + with self.assertRaisesRegex(ValueError, "missing oracle script"): + self.run_check() + + def test_unlisted_addition_is_refused(self): + for name in ("not-in-oracle", "parity-drift", "parity-drift-test"): + self.write(self.src, name, b"new") + with self.assertRaisesRegex(ValueError, "not a regular script"): + self.run_check() + (self.src / "scripts" / name).unlink() + if __name__ == "__main__": unittest.main()