test_mpack_oserror_integrity.py
python
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9
docs: expand cache plan with all seven testing tiers and do…
Sonnet 4.6
131 days ago
| 1 | """Tests for Bug 13: apply_mpack propagates unhandled OSError from write_commit. |
| 2 | |
| 3 | Root cause: apply_mpack's commit loop catches (KeyError, ValueError, TypeError) |
| 4 | but NOT OSError. write_commit raises OSError("Store integrity violation") when |
| 5 | an existing commit file contains a DIFFERENT commit_id than the incoming one — |
| 6 | i.e. an impostor file. When this condition exists in the local store, apply_mpack |
| 7 | propagates the unhandled OSError and the entire pull/push/clone call stack crashes |
| 8 | with an unhandled exception rather than logging CRITICAL and continuing. |
| 9 | |
| 10 | Concrete attack / failure scenario: |
| 11 | 1. Local store has commit file abc.msgpack containing impostor bytes |
| 12 | (different commit_id inside the file than the filename implies). |
| 13 | 2. A bundle arrives containing the legitimate abc commit. |
| 14 | 3. write_commit reads the existing file, detects commit_id mismatch, raises |
| 15 | OSError("Store integrity violation"). |
| 16 | 4. apply_mpack (before fix) propagates the OSError → muse pull / muse clone |
| 17 | crashes with an unhandled exception. |
| 18 | |
| 19 | Scope of tests |
| 20 | -------------- |
| 21 | - apply_mpack with impostor commit file does not crash (no uncaught OSError) |
| 22 | - apply_mpack logs CRITICAL when OSError is raised by write_commit |
| 23 | - apply_mpack continues processing remaining commits after OSError on one |
| 24 | - apply_mpack correctly reports commits_written for non-affected commits |
| 25 | - The impostor file is NOT overwritten by the bundle commit (safety) |
| 26 | - apply_mpack raises ValueError (not OSError) for commits with wrong hash |
| 27 | """ |
| 28 | from __future__ import annotations |
| 29 | |
| 30 | import datetime |
| 31 | import logging |
| 32 | import pathlib |
| 33 | |
| 34 | import msgpack |
| 35 | import pytest |
| 36 | |
| 37 | from muse.core.pack import MPackBundle, apply_mpack |
| 38 | from muse.core.snapshot import compute_commit_id, compute_snapshot_id |
| 39 | from muse.core.store import ( |
| 40 | CommitDict, |
| 41 | CommitRecord, |
| 42 | SnapshotRecord, |
| 43 | commit_path, |
| 44 | read_commit, |
| 45 | write_commit, |
| 46 | write_snapshot, |
| 47 | ) |
| 48 | |
| 49 | _TS = datetime.datetime(2024, 6, 15, 10, 0, 0, tzinfo=datetime.timezone.utc) |
| 50 | |
| 51 | |
| 52 | def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 53 | repo = tmp_path / "repo" |
| 54 | repo.mkdir() |
| 55 | (repo / ".muse").mkdir() |
| 56 | (repo / ".muse" / "commits").mkdir() |
| 57 | (repo / ".muse" / "snapshots").mkdir() |
| 58 | return repo |
| 59 | |
| 60 | |
| 61 | def _make_commit_record(message: str, parent: str | None = None) -> CommitRecord: |
| 62 | manifest = {f"{message}.py": "a" * 64} |
| 63 | snap_id = compute_snapshot_id(manifest) |
| 64 | parent_ids = [parent] if parent else [] |
| 65 | cid = compute_commit_id( |
| 66 | repo_id="test-repo", |
| 67 | parent_ids=parent_ids, |
| 68 | snapshot_id=snap_id, |
| 69 | message=message, |
| 70 | committed_at_iso=_TS.isoformat(), |
| 71 | author="tester",) |
| 72 | return CommitRecord( |
| 73 | commit_id=cid, |
| 74 | repo_id="test-repo", |
| 75 | created_on_branch="main", |
| 76 | snapshot_id=snap_id, |
| 77 | message=message, |
| 78 | committed_at=_TS, |
| 79 | author="tester", |
| 80 | parent_commit_id=parent, |
| 81 | parent2_commit_id=None, |
| 82 | ) |
| 83 | |
| 84 | |
| 85 | def _bundle_with_commits(commits: list[CommitRecord]) -> MPackBundle: |
| 86 | return MPackBundle(objects=[], snapshots=[], commits=[c.to_dict() for c in commits], tags=[]) |
| 87 | |
| 88 | |
| 89 | def _write_impostor_file(repo: pathlib.Path, path_commit_id: str, content_commit_id: str) -> None: |
| 90 | """Write a commit file at path_commit_id.msgpack that contains content_commit_id inside.""" |
| 91 | impostor_data = { |
| 92 | "commit_id": content_commit_id, |
| 93 | "repo_id": "impostor", |
| 94 | "branch": "main", |
| 95 | "snapshot_id": "0" * 64, |
| 96 | "message": "impostor", |
| 97 | "committed_at": _TS.isoformat(), |
| 98 | "parent_commit_id": None, |
| 99 | "parent2_commit_id": None, |
| 100 | "author": "attacker", |
| 101 | } |
| 102 | path = commit_path(repo, path_commit_id) |
| 103 | path.parent.mkdir(parents=True, exist_ok=True) |
| 104 | path.write_bytes(msgpack.packb(impostor_data, use_bin_type=True)) |
| 105 | |
| 106 | |
| 107 | # ────────────────────────────────────────────────────────────────────────────── |
| 108 | # Bug 13: unhandled OSError from write_commit |
| 109 | # ────────────────────────────────────────────────────────────────────────────── |
| 110 | |
| 111 | class TestApplyPackOsErrorIntegrity: |
| 112 | |
| 113 | def test_apply_pack_does_not_crash_on_store_integrity_violation(self, tmp_path: pathlib.Path) -> None: |
| 114 | """Bug 13: apply_mpack must not propagate OSError from write_commit.""" |
| 115 | repo = _make_repo(tmp_path) |
| 116 | c = _make_commit_record("good-commit") |
| 117 | |
| 118 | # Poison the store: write an impostor at c.commit_id's path |
| 119 | _write_impostor_file(repo, c.commit_id, "f" * 64) |
| 120 | |
| 121 | bundle = _bundle_with_commits([c]) |
| 122 | # Before the fix: apply_mpack would raise OSError("Store integrity violation") |
| 123 | # After the fix: apply_mpack must return normally (logs CRITICAL, skips commit) |
| 124 | result = apply_mpack(repo, bundle) # must not raise |
| 125 | assert result is not None |
| 126 | |
| 127 | def test_apply_pack_logs_critical_on_store_integrity_violation( |
| 128 | self, tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture |
| 129 | ) -> None: |
| 130 | """apply_mpack must log CRITICAL (not swallow silently) when OSError is raised.""" |
| 131 | repo = _make_repo(tmp_path) |
| 132 | c = _make_commit_record("critical-log-commit") |
| 133 | _write_impostor_file(repo, c.commit_id, "e" * 64) |
| 134 | |
| 135 | bundle = _bundle_with_commits([c]) |
| 136 | with caplog.at_level(logging.CRITICAL, logger="muse.core.pack"): |
| 137 | apply_mpack(repo, bundle) |
| 138 | |
| 139 | crits = [r for r in caplog.records if r.levelno >= logging.CRITICAL] |
| 140 | assert crits, "apply_mpack must log CRITICAL when write_commit raises OSError" |
| 141 | assert any("integrity" in r.message.lower() or "violation" in r.message.lower() |
| 142 | for r in crits) |
| 143 | |
| 144 | def test_apply_pack_continues_after_integrity_violation(self, tmp_path: pathlib.Path) -> None: |
| 145 | """Remaining commits must be processed after an integrity-violation skip.""" |
| 146 | repo = _make_repo(tmp_path) |
| 147 | bad = _make_commit_record("bad-commit") |
| 148 | good = _make_commit_record("good-commit-after") |
| 149 | |
| 150 | # Poison only the bad commit's file |
| 151 | _write_impostor_file(repo, bad.commit_id, "d" * 64) |
| 152 | |
| 153 | bundle = _bundle_with_commits([bad, good]) |
| 154 | result = apply_mpack(repo, bundle) |
| 155 | |
| 156 | # good commit must be written despite the preceding integrity violation |
| 157 | assert read_commit(repo, good.commit_id) is not None, ( |
| 158 | "apply_mpack must continue processing commits after an integrity violation" |
| 159 | ) |
| 160 | |
| 161 | def test_apply_pack_commits_written_excludes_integrity_violation(self, tmp_path: pathlib.Path) -> None: |
| 162 | """commits_written must not include commits that triggered OSError.""" |
| 163 | repo = _make_repo(tmp_path) |
| 164 | bad = _make_commit_record("integrity-violation") |
| 165 | good = _make_commit_record("normal-commit") |
| 166 | |
| 167 | _write_impostor_file(repo, bad.commit_id, "c" * 64) |
| 168 | |
| 169 | bundle = _bundle_with_commits([bad, good]) |
| 170 | result = apply_mpack(repo, bundle) |
| 171 | |
| 172 | # Only the good commit should be counted as written |
| 173 | assert result["commits_written"] == 1, ( |
| 174 | f"commits_written should be 1 (only good), got {result['commits_written']}" |
| 175 | ) |
| 176 | |
| 177 | def test_impostor_file_not_overwritten_by_apply_pack(self, tmp_path: pathlib.Path) -> None: |
| 178 | """apply_mpack must NOT replace the impostor file — that would hide the violation.""" |
| 179 | repo = _make_repo(tmp_path) |
| 180 | c = _make_commit_record("legit-commit") |
| 181 | impostor_id = "b" * 64 |
| 182 | _write_impostor_file(repo, c.commit_id, impostor_id) |
| 183 | |
| 184 | bundle = _bundle_with_commits([c]) |
| 185 | apply_mpack(repo, bundle) |
| 186 | |
| 187 | # The file still contains the impostor — write_commit's OSError branch |
| 188 | # does NOT overwrite (only write_commit with a valid incoming record |
| 189 | # and a corrupt EXISTING file overwrites via the except Exception branch). |
| 190 | # apply_mpack catches the OSError and skips — no write happens. |
| 191 | path = commit_path(repo, c.commit_id) |
| 192 | raw = msgpack.unpackb(path.read_bytes(), raw=False) |
| 193 | assert raw["commit_id"] == impostor_id, ( |
| 194 | "apply_mpack must not overwrite an impostor file — that would hide the " |
| 195 | "integrity violation from the user" |
| 196 | ) |
| 197 | |
| 198 | def test_apply_pack_valid_commit_no_integrity_violation(self, tmp_path: pathlib.Path) -> None: |
| 199 | """Regression: valid commits must still be written normally.""" |
| 200 | repo = _make_repo(tmp_path) |
| 201 | c = _make_commit_record("clean-commit") |
| 202 | bundle = _bundle_with_commits([c]) |
| 203 | result = apply_mpack(repo, bundle) |
| 204 | |
| 205 | assert result["commits_written"] == 1 |
| 206 | assert read_commit(repo, c.commit_id) is not None |
| 207 | |
| 208 | def test_apply_pack_wrong_hash_raises_valuerror_not_oserror(self, tmp_path: pathlib.Path) -> None: |
| 209 | """Commits with mismatched commit_id hash raise ValueError (caught separately).""" |
| 210 | repo = _make_repo(tmp_path) |
| 211 | # Bad record: commit_id doesn't match content hash |
| 212 | bad_dict = CommitDict( |
| 213 | commit_id="f" * 64, # wrong hash |
| 214 | repo_id="test-repo", |
| 215 | created_on_branch="main", |
| 216 | snapshot_id="a" * 64, |
| 217 | message="bad", |
| 218 | committed_at=_TS.isoformat(), |
| 219 | parent_commit_id=None, |
| 220 | parent2_commit_id=None, |
| 221 | author="tester", |
| 222 | ) |
| 223 | bundle = MPackBundle(objects=[], snapshots=[], commits=[bad_dict], tags=[]) |
| 224 | result = apply_mpack(repo, bundle) # must not raise |
| 225 | assert result["commits_written"] == 0 |
File History
2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9
docs: expand cache plan with all seven testing tiers and do…
Sonnet 4.6
131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c
docs: docstring sprint for-each-ref→hotspots — idiomatic ru…
Sonnet 4.6
patch
137 days ago