diff --git a/backend/tests/test_delivery.py b/backend/tests/test_delivery.py index 24995c9..66b9a9a 100644 --- a/backend/tests/test_delivery.py +++ b/backend/tests/test_delivery.py @@ -178,6 +178,32 @@ def test_frozen_copy_reuses_verified_records_and_checks_full_destination(tmp_pat assert scanned == [target, target] +def test_frozen_copy_rejects_destination_io_mismatch_before_acceptance(tmp_path, monkeypatch): + # Simulated I/O mismatch in an ordinary owned fixture; Root protection and + # actual disk/error isolation are still separate VM acceptance. + monkeypatch.setattr(installer.preflight, "protected", lambda *args, **kwargs: None) + source, target = tmp_path / "source", tmp_path / "target" + source.mkdir() + (source / "backend").mkdir() + payload = source / "backend/ordinary.txt" + payload.write_bytes(b"frozen input") + payload.chmod(0o444) + (source / "backend").chmod(0o555) + records = manifest.inventory(source) + original = installer._copy_verified + + def simulate_io_mismatch(source, target, expected, mode, **options): + assert options["readback"] is False + original(source, target, expected, mode, **options) + target.chmod(0o600) + target.write_bytes(b"different bytes") + target.chmod(mode) + + monkeypatch.setattr(installer, "_copy_verified", simulate_io_mismatch) + with pytest.raises(policy.DeliveryError, match="installed image readback differs"): + installer.frozen_copy(source, target, verified_records=records) + + def test_kernel_observation_keeps_cumulative_counters_and_rejects_duplicate_rows(): raw = ( "usage_usec 17\nuser_usec 11\nsystem_usec 6\nnr_periods 9\nnr_throttled 2\nthrottled_usec 3" diff --git a/scripts/release/install.py b/scripts/release/install.py index acee5ae..97d5d06 100644 --- a/scripts/release/install.py +++ b/scripts/release/install.py @@ -89,6 +89,10 @@ def sync_directory(path: Path) -> None: def copy_verified(source: Path, target: Path, expected: str, mode: int) -> None: + _copy_verified(source, target, expected, mode, readback=True) + + +def _copy_verified(source: Path, target: Path, expected: str, mode: int, *, readback: bool) -> None: preflight.protected(source) sha256(expected) if target.exists() or target.is_symlink(): @@ -117,7 +121,7 @@ def copy_verified(source: Path, target: Path, expected: str, mode: int) -> None: ) os.fchmod(outgoing, mode) os.fsync(outgoing) - if digest(target) != expected: + if readback and digest(target) != expected: raise DeliveryError("installed bytes differ; incomplete release retained") finally: os.close(incoming) @@ -215,11 +219,14 @@ def frozen_copy( for name in sorted(directories, key=lambda name: (name.count("/"), name)): (target / name).mkdir(mode=0o700) for record in records: - copy_verified( + # The complete sealed tree is read back immediately below. Avoid + # hashing each destination both here and again in that inventory. + _copy_verified( source / record["path"], target / record["path"], record["sha256"], int(record["mode"], 8), + readback=False, ) for name in sorted(directories, key=lambda name: name.count("/"), reverse=True): (target / name).chmod(0o555)