From 7ae01fddae8b505e6714d4f9b2980ea574a4773f Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:31:24 +0000 Subject: [PATCH 1/2] fix: import attested empty legacy gear offline --- tools/gear-ownership.py | 170 +++++++++++++++- tools/test_gear_ownership.py | 380 +++++++++++++++++++++++++++++++++++ 2 files changed, 548 insertions(+), 2 deletions(-) diff --git a/tools/gear-ownership.py b/tools/gear-ownership.py index fa7ec6d..e6c6bfc 100644 --- a/tools/gear-ownership.py +++ b/tools/gear-ownership.py @@ -3,6 +3,7 @@ import argparse import base64 import copy +from datetime import datetime, timezone from decimal import Decimal import hashlib import json @@ -16,6 +17,11 @@ import yaml +LEGACY_EMPTY_PRODUCERS = {"d864c30aa6c01453e38bc2539d0ccea889b846aa47e9050982026e894a2e8247": "Magic 0.5.2"} +LEGACY_EMPTY_CANONICAL = b"stations: {}\n" +LEGACY_EMPTY_STAGING = ".gear-legacy-empty-" +LEGACY_EMPTY_HISTORY = ("gear-ownership", "gear-returns") + class GearLoader(yaml.SafeLoader): def construct_mapping(self, node, deep=False): @@ -472,6 +478,151 @@ def backup_gear(root, backup): return original +def _legacy_empty_read(path, label): + info = os.lstat(path) + if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode): + raise ValueError(label + " must be a regular nonsymlink file") + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(descriptor) + if not stat.S_ISREG(before.st_mode): + raise ValueError(label + " must be a regular file") + chunks = [] + while chunk := os.read(descriptor, 65536): + chunks.append(chunk) + after = os.fstat(descriptor) + fields = ("st_dev", "st_ino", "st_size", "st_mode", "st_uid", "st_gid", "st_mtime_ns", "st_ctime_ns", "st_nlink") + if any(getattr(before, field) != getattr(after, field) for field in fields): + raise ValueError(label + " changed while being read") + return after, b"".join(chunks) + finally: + os.close(descriptor) + + +def _legacy_empty_state(root, legacy_jar, own_staging=None): + root = Path(root) + if not root.is_absolute() or Path(os.path.realpath(root)) != root or not stat.S_ISDIR(os.lstat(root).st_mode): + raise ValueError("--data must be an absolute, symlink-free directory") + if legacy_jar is None: + raise ValueError("legacy-empty import requires --legacy-jar") + _, jar_bytes = _legacy_empty_read(Path(legacy_jar), "legacy JAR") + producer = digest(jar_bytes) + if producer not in LEGACY_EMPTY_PRODUCERS: + raise ValueError("legacy JAR is not a reviewed empty-save producer") + for name in sorted(os.listdir(root)): + if own_staging is not None and name == own_staging.name: + continue + if name.startswith("gear-stations") and name != "gear-stations.yml": + raise ValueError("station sibling needs staff review") + if name.startswith("magic-") and name.endswith(".tmp") or name.startswith(LEGACY_EMPTY_STAGING): + raise ValueError("staging evidence needs staff review") + for name in LEGACY_EMPTY_HISTORY: + try: + os.lstat(root / name) + except FileNotFoundError: + continue + raise ValueError("ownership/return history exists (NOFOLLOW): " + name) + station = root / "gear-stations.yml" + try: + info, contents = _legacy_empty_read(station, "station") + except FileNotFoundError as error: + raise ValueError("station file is absent; legacy-empty import does not apply") from error + if info.st_nlink != 1: + raise ValueError("station file has other hard links") + record = { + "data": str(root), "producer": LEGACY_EMPTY_PRODUCERS[producer], "producer-sha256": producer, + "station": {"dev": info.st_dev, "ino": info.st_ino, "size": info.st_size, "sha256": digest(contents), + "mode": oct(stat.S_IMODE(info.st_mode)), "uid": info.st_uid, "gid": info.st_gid, + "mtime-ns": info.st_mtime_ns, "ctime-ns": info.st_ctime_ns, "nlink": info.st_nlink}, + "history-absent": list(LEGACY_EMPTY_HISTORY), "target-sha256": digest(LEGACY_EMPTY_CANONICAL), + } + if contents == LEGACY_EMPTY_CANONICAL and info.st_size == len(LEGACY_EMPTY_CANONICAL): + return record, None, "ALREADY_CANONICAL" + if contents != b"" or info.st_size != 0: + raise ValueError("station file is not exactly zero bytes; ordinary strict recovery applies") + return record, digest(json.dumps(record, sort_keys=True).encode()), "ELIGIBLE_REQUIRES_LINEAGE_ATTESTATION" + + +def legacy_empty_import(root, legacy_jar, apply=False, expect_baseline=None, attest=None, backup=None): + """Explicit stopped import; file state cannot establish the required historical lineage.""" + report, staged, staged_inode, replace_started = {}, None, None, False + try: + root = Path(root) + record, baseline, verdict = _legacy_empty_state(root, legacy_jar) + report.update(verdict=verdict, baseline=baseline, record=record) + if not apply or verdict == "ALREADY_CANONICAL": + return report + if attest is None or backup is None or not isinstance(expect_baseline, str) or not re.fullmatch(r"[0-9a-f]{64}", expect_baseline): + raise ValueError("apply requires --attest FILE, --expect-baseline HEX and a new --backup") + if baseline != expect_baseline: + raise ValueError("current state differs from the attested baseline") + _, attestation = _legacy_empty_read(Path(attest), "attestation") + if not attestation.strip() or not re.search(rb"(? Date: Wed, 14 Oct 2026 10:18:35 +0000 Subject: [PATCH 2/2] fix: preserve attested station ownership --- tools/gear-ownership.py | 8 +- tools/test_gear_ownership.py | 154 ++++++++++++++++++++++++++++++++++- 2 files changed, 159 insertions(+), 3 deletions(-) diff --git a/tools/gear-ownership.py b/tools/gear-ownership.py index e6c6bfc..5ce0ff8 100644 --- a/tools/gear-ownership.py +++ b/tools/gear-ownership.py @@ -585,6 +585,10 @@ def legacy_empty_import(root, legacy_jar, apply=False, expect_baseline=None, att if written <= 0: raise OSError("incomplete legacy-empty staging write") pending = pending[written:] + info = os.fstat(descriptor) + if (info.st_uid, info.st_gid) != (record["station"]["uid"], record["station"]["gid"]): + os.fchown(descriptor, record["station"]["uid"] if info.st_uid != record["station"]["uid"] else -1, + record["station"]["gid"] if info.st_gid != record["station"]["gid"] else -1) os.fchmod(descriptor, int(record["station"]["mode"], 8)) os.fsync(descriptor) finally: @@ -592,14 +596,14 @@ def legacy_empty_import(root, legacy_jar, apply=False, expect_baseline=None, att if _legacy_empty_state(root, legacy_jar, staged)[1] != baseline: raise ValueError("station or history changed before publication") info, contents = _legacy_empty_read(staged, "staging") - if (info.st_dev, info.st_ino) != staged_inode or info.st_nlink != 1 or contents != LEGACY_EMPTY_CANONICAL or stat.S_IMODE(info.st_mode) != int(record["station"]["mode"], 8): + if (info.st_dev, info.st_ino) != staged_inode or info.st_nlink != 1 or contents != LEGACY_EMPTY_CANONICAL or stat.S_IMODE(info.st_mode) != int(record["station"]["mode"], 8) or (info.st_uid, info.st_gid) != (record["station"]["uid"], record["station"]["gid"]): raise ValueError("staging changed before publication") # The final recheck/rename interval requires the stopped server and deployment lock. replace_started = True os.replace(staged, root / "gear-stations.yml") force(root) final, _, verdict = _legacy_empty_state(root, legacy_jar) - if verdict != "ALREADY_CANONICAL" or final["station"]["mode"] != record["station"]["mode"]: + if verdict != "ALREADY_CANONICAL" or any(final["station"][field] != record["station"][field] for field in ("mode", "uid", "gid")): raise ValueError("post-publication verification failed") report.update(verdict="IMPORTED", published=final["station"]) durable(backup_path / "IMPORT-RESULT.json", json.dumps(report, indent=2, sort_keys=True).encode()) diff --git a/tools/test_gear_ownership.py b/tools/test_gear_ownership.py index a62b43e..7c13d39 100644 --- a/tools/test_gear_ownership.py +++ b/tools/test_gear_ownership.py @@ -10,6 +10,7 @@ import time import stat import sys +from types import SimpleNamespace import unittest from unittest.mock import patch import uuid @@ -375,6 +376,15 @@ def test_native_world_and_java_integer_selector_constraints_are_mirrored(self): self.assertFalse(self.backup.exists()) +class LegacyEmptyProducerPolicyTests(unittest.TestCase): + def test_shipped_allowlist_is_exact_reviewed_preownership_producer(self): + unpatched_spec = importlib.util.spec_from_file_location("shipped_gear_ownership", Path(__file__).with_name("gear-ownership.py")) + shipped = importlib.util.module_from_spec(unpatched_spec) + unpatched_spec.loader.exec_module(shipped) + self.assertEqual({"d864c30aa6c01453e38bc2539d0ccea889b846aa47e9050982026e894a2e8247": "Magic 0.5.2"}, shipped.LEGACY_EMPTY_PRODUCERS) + self.assertNotIn("fb40f229b4b79fd1a6cd10c196d9a9a7d6dc40db4e97d4a7d6806b1f645002af", shipped.LEGACY_EMPTY_PRODUCERS) + + class LegacyEmptyImportTests(unittest.TestCase): # Exact named output of the retained actual 0.5.2 save probe; no external JAR needed by this suite. OLD_0_5_2_EMPTY_SAVE = b"" @@ -488,6 +498,145 @@ def test_already_canonical_replay_creates_no_backup_or_new_publication(self): self.assertEqual(0, code) self.assertEqual({"journals": []}, report) + @staticmethod + def ownership_view(info, **changed): + fields = {name: getattr(info, name) for name in dir(info) if name.startswith("st_")} + fields.update(changed) + return SimpleNamespace(**fields) + + def test_matching_ownership_never_calls_chown_including_mirrored_views(self): + owner = self.station.stat() + with patch.object(gear.os, "fchown", side_effect=PermissionError("matching bindfs ownership must not be changed")) as chown: + report = self.apply() + self.assertEqual("IMPORTED", report["verdict"], report) + chown.assert_not_called() + self.assertEqual((owner.st_uid, owner.st_gid), (report["published"]["uid"], report["published"]["gid"])) + self.assertEqual((owner.st_uid, owner.st_gid), (self.station.stat().st_uid, self.station.stat().st_gid)) + + def test_different_staging_owner_is_restored_before_mode_and_fsync(self): + real_open, real_stat, real_chown, real_chmod, real_fsync = os.open, os.fstat, os.fchown, os.fchmod, os.fsync + for uid_differs, gid_differs in [(True, False), (False, True), (True, True)]: + with self.subTest(uid_differs=uid_differs, gid_differs=gid_differs): + self.station.write_bytes(b"") + self.backup = self.base / ("recovery-" + str(uid_differs) + "-" + str(gid_differs)) + owner = self.station.stat() + inode, observed, events = [], [], [] + def opened(path, flags, *args): + fd = real_open(path, flags, *args) + if Path(path).name.startswith(gear.LEGACY_EMPTY_STAGING) and flags & os.O_CREAT: + info = real_stat(fd) + inode.append((info.st_dev, info.st_ino)) + return fd + def statted(fd): + info = real_stat(fd) + if inode and (info.st_dev, info.st_ino) == inode[0] and info.st_size == len(gear.LEGACY_EMPTY_CANONICAL) and not observed: + observed.append(True) + return self.ownership_view(info, st_uid=owner.st_uid + int(uid_differs), st_gid=owner.st_gid + int(gid_differs)) + return info + def chowned(fd, uid, gid): + self.assertEqual((owner.st_uid if uid_differs else -1, owner.st_gid if gid_differs else -1), (uid, gid)) + events.append("chown") + return real_chown(fd, uid, gid) + def chmodded(fd, mode): + events.append("chmod") + return real_chmod(fd, mode) + def synced(fd): + info = real_stat(fd) + if inode and (info.st_dev, info.st_ino) == inode[0]: events.append("fsync") + return real_fsync(fd) + with patch.object(gear.os, "open", side_effect=opened), patch.object(gear.os, "fstat", side_effect=statted), patch.object(gear.os, "fchown", side_effect=chowned), patch.object(gear.os, "fchmod", side_effect=chmodded), patch.object(gear.os, "fsync", side_effect=synced): + report = self.apply() + self.assertEqual("IMPORTED", report["verdict"], report) + self.assertEqual(["chown", "chmod", "fsync"], events) + self.assertEqual((owner.st_uid, owner.st_gid), (report["published"]["uid"], report["published"]["gid"])) + self.assertEqual(stat.S_IMODE(owner.st_mode), stat.S_IMODE(self.station.stat().st_mode)) + + def test_silently_ignored_chown_cannot_publish_disagreeing_ownership(self): + real_stat = os.fstat + for field in ["st_uid", "st_gid"]: + with self.subTest(field=field): + self.backup = self.base / ("ignored-" + field) + owner = self.station.stat() + good = self.prepared() + def statted(fd): + info = real_stat(fd) + if stat.S_ISREG(info.st_mode) and info.st_size == len(gear.LEGACY_EMPTY_CANONICAL): + return self.ownership_view(info, **{field: getattr(info, field) + 1}) + return info + with patch.object(gear.os, "fstat", side_effect=statted), patch.object(gear.os, "fchown", return_value=None) as chown: + self.refused(lambda: self.preview(**good), "staging changed") + chown.assert_called_once() + self.assertEqual((owner.st_uid if field == "st_uid" else -1, owner.st_gid if field == "st_gid" else -1), chown.call_args.args[1:]) + self.assertTrue((self.backup / "IMPORT-PLAN.json").exists()) + self.assertFalse((self.backup / "IMPORT-RESULT.json").exists()) + self.assertFalse(list(self.root.glob(gear.LEGACY_EMPTY_STAGING + "*"))) + + def test_unavailable_chown_refuses_without_replacing_original(self): + good = self.prepared() + real_stat = os.fstat + def statted(fd): + info = real_stat(fd) + if stat.S_ISREG(info.st_mode) and info.st_size == len(gear.LEGACY_EMPTY_CANONICAL) and stat.S_IMODE(info.st_mode) == 0o600: + return self.ownership_view(info, st_uid=info.st_uid + 1) + return info + with patch.object(gear.os, "fstat", side_effect=statted), patch.object(gear.os, "fchown", side_effect=PermissionError("ownership is not permitted")) as chown: + self.refused(lambda: self.preview(**good), "ownership is not permitted") + chown.assert_called_once() + self.assertTrue((self.backup / "IMPORT-PLAN.json").is_file()) + self.assertFalse((self.backup / "IMPORT-RESULT.json").exists()) + self.assertFalse(list(self.root.glob(gear.LEGACY_EMPTY_STAGING + "*"))) + + def test_staged_owner_or_group_disagreement_refuses_before_rename(self): + real_read = gear._legacy_empty_read + for field in ["st_uid", "st_gid"]: + with self.subTest(field=field): + import shutil + if self.backup.exists(): shutil.rmtree(self.backup) + self.station.write_bytes(b"") + good = self.prepared() + def read(path, label): + info, data = real_read(path, label) + if label == "staging": info = self.ownership_view(info, **{field: getattr(info, field) + 1}) + return info, data + with patch.object(gear, "_legacy_empty_read", side_effect=read), patch.object(gear.os, "replace", wraps=os.replace) as replace: + self.refused(lambda: self.preview(**good), "staging changed") + self.assertFalse(any(call.args[1] == self.station for call in replace.call_args_list)) + self.assertTrue((self.backup / "IMPORT-PLAN.json").is_file()) + self.assertFalse((self.backup / "IMPORT-RESULT.json").exists()) + self.assertFalse(list(self.root.glob(gear.LEGACY_EMPTY_STAGING + "*"))) + import shutil + shutil.rmtree(self.backup) + + def test_published_owner_or_group_disagreement_is_unconfirmed(self): + real_read, real_replace = gear._legacy_empty_read, os.replace + for field in ["st_uid", "st_gid"]: + with self.subTest(field=field): + import shutil + if self.backup.exists(): shutil.rmtree(self.backup) + self.station.write_bytes(b"") + good = self.prepared() + replaced = [] + def replace(source, target): + result = real_replace(source, target) + if target == self.station: replaced.append(True) + return result + def read(path, label): + info, data = real_read(path, label) + if replaced and label == "station": info = self.ownership_view(info, **{field: getattr(info, field) + 1}) + return info, data + with patch.object(gear, "_legacy_empty_read", side_effect=read), patch.object(gear.os, "replace", side_effect=replace): + report = self.preview(**good) + self.assertEqual("PUBLISHED_UNCONFIRMED", report["verdict"], report) + self.assertIn("post-publication verification failed", report["error"]) + self.assertIn("Keep Paper stopped", report["recovery"]) + self.assertEqual(gear.LEGACY_EMPTY_CANONICAL, self.station.read_bytes()) + self.assertTrue((self.backup / "IMPORT-PLAN.json").is_file()) + self.assertFalse((self.backup / "IMPORT-RESULT.json").exists()) + self.assertFalse(list(self.root.glob(gear.LEGACY_EMPTY_STAGING + "*"))) + import shutil + shutil.rmtree(self.backup) + self.station.write_bytes(b"") + def test_nonempty_or_noncanonical_documents_refuse_without_loosening_yaml(self): for data in [b"\n", b" \n\t\n", b"~\n", b"stations:\n", b"{}\n", b"stations: {}", b"stations: {}\nstations: {}\n", b"\xff"]: with self.subTest(data=data): @@ -569,9 +718,12 @@ def test_apply_needs_exact_baseline_and_a_nonempty_attestation_file(self): self.assertFalse(self.backup.exists()) def test_touch_and_same_bytes_old_writer_restarts_break_the_baseline(self): - for mutation in [lambda: os.utime(self.station, ns=(1, 1)), lambda: self.station.write_bytes(b""), lambda: (self.station.unlink(), self.station.write_bytes(b""))]: + for mutation in [lambda: os.utime(self.station, ns=(1, 1)), lambda: self.station.write_bytes(b""), lambda: (self.station.unlink(), self.station.write_bytes(b""), self.station.chmod(0o640))]: + os.utime(self.station, ns=(2, 2)) good = self.prepared() mutation() + self.assertNotEqual(2, self.station.stat().st_mtime_ns) + self.assertEqual(0o640, stat.S_IMODE(self.station.stat().st_mode)) self.refused(lambda: self.preview(**good), "attested baseline") self.assertFalse(self.backup.exists())