mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
fix(update-tx): explicit null schema stamp reads corrupt, not legacy-unstamped
F3 adversarial fix-round 2, claim 1 (HIGH): read_update_tx_strict used a
plain .get(), so a marker carrying an explicit _schema_version: null was
indistinguishable from the accepted pre-7.0 UNSTAMPED form and read valid —
rollback would then interpret and destructively act on a damaged stamp. A
dict.get sentinel now distinguishes key ABSENCE (legacy, valid) from a stored
null (corrupt, like every other non-integer stamp; no writer ever stamps
null). Pin: null-stamped marker reads ("corrupt", {}) and the direct
rollback entry point refuses typed with the marker byte-identical, HEAD
unmoved and dirty owner work untouched; None joined the invalid-stamp loop.
Protected surface supervisor/update_merge.py: delta sanctioned by this
fix-round and minimal (one sentinel + docstring).
This commit is contained in:
parent
d1d131dfe1
commit
9daf100bfd
2 changed files with 46 additions and 9 deletions
|
|
@ -53,6 +53,10 @@ UPDATE_TX_MARKER_NAME = "ouroboros-update-tx.json"
|
|||
# the stamp is one additive key its reader ignores.
|
||||
UPDATE_TX_SCHEMA_VERSION = 1
|
||||
|
||||
# ``dict.get`` sentinel distinguishing a MISSING ``_schema_version`` key (the
|
||||
# accepted pre-7.0 unstamped form) from an explicit stored ``null`` (corrupt).
|
||||
_SCHEMA_STAMP_ABSENT = object()
|
||||
|
||||
|
||||
def managed_update_constitution_present(ref: str = "HEAD") -> bool:
|
||||
"""Whether *ref* keeps the non-empty regular BIBLE.md blob required by P4."""
|
||||
|
|
@ -217,12 +221,14 @@ def read_update_tx_strict() -> Tuple[str, Dict[str, Any]]:
|
|||
``"future"``. A marker that exists but is unreadable/invalid is ``corrupt`` — callers
|
||||
MUST fail closed (block mutative update/commit ops) rather than treat it as ``absent``.
|
||||
|
||||
Schema admission (F14 N−1 shim): an UNSTAMPED marker is the accepted pre-7.0
|
||||
form and reads ``valid`` — the boot finalizer must drive a transaction the
|
||||
N−1 updater recorded. An integer stamp above ``UPDATE_TX_SCHEMA_VERSION`` is
|
||||
``future`` (recorded by a newer release; the raw tx is returned as evidence
|
||||
but no caller may interpret or overwrite it); a non-integer stamp is
|
||||
``corrupt``."""
|
||||
Schema admission (F14 N−1 shim): an UNSTAMPED marker — the key ABSENT —
|
||||
is the accepted pre-7.0 form and reads ``valid``: the boot finalizer must
|
||||
drive a transaction the N−1 updater recorded. An integer stamp above
|
||||
``UPDATE_TX_SCHEMA_VERSION`` is ``future`` (recorded by a newer release;
|
||||
the raw tx is returned as evidence but no caller may interpret or
|
||||
overwrite it); ANY present non-integer stamp — an explicit ``null``
|
||||
included — is ``corrupt`` (no writer ever stamps ``null``, so it is a
|
||||
damaged stamp, not the legacy unstamped form)."""
|
||||
import json
|
||||
|
||||
path = _update_tx_marker_path()
|
||||
|
|
@ -236,8 +242,8 @@ def read_update_tx_strict() -> Tuple[str, Dict[str, Any]]:
|
|||
return "corrupt", {}
|
||||
from ouroboros.contracts.schema_versions import SCHEMA_VERSION_KEY
|
||||
|
||||
stamp = raw.get(SCHEMA_VERSION_KEY)
|
||||
if stamp is not None:
|
||||
stamp = raw.get(SCHEMA_VERSION_KEY, _SCHEMA_STAMP_ABSENT)
|
||||
if stamp is not _SCHEMA_STAMP_ABSENT:
|
||||
if isinstance(stamp, bool) or not isinstance(stamp, int) or stamp < 1:
|
||||
return "corrupt", {}
|
||||
if stamp > UPDATE_TX_SCHEMA_VERSION:
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ def test_invalid_stamps_are_corrupt_and_newer_stamps_are_future(tmp_path, monkey
|
|||
repo, head = _init_repo(tmp_path)
|
||||
_point_at(monkeypatch, tmp_path, repo, head)
|
||||
marker = update_merge._update_tx_marker_path()
|
||||
for bad_stamp in ("1", True, 0, -3):
|
||||
for bad_stamp in ("1", True, 0, -3, None):
|
||||
atomic_write_json(marker, {SCHEMA_VERSION_KEY: bad_stamp, "phase": "x"})
|
||||
assert update_merge.read_update_tx_strict()[0] == "corrupt", bad_stamp
|
||||
atomic_write_json(
|
||||
|
|
@ -97,6 +97,37 @@ def test_invalid_stamps_are_corrupt_and_newer_stamps_are_future(tmp_path, monkey
|
|||
assert status == "future" and tx["phase"] == "x" # raw evidence returned
|
||||
|
||||
|
||||
def test_explicit_null_stamp_is_corrupt_and_rollback_refuses_untouched(tmp_path, monkeypatch):
|
||||
"""Adversarial fix-round 2, claim 1: an explicit ``_schema_version: null``
|
||||
is a DAMAGED stamp, not the legacy unstamped form (only key ABSENCE is) —
|
||||
it reads ``corrupt`` and the direct rollback entry point refuses typed
|
||||
BEFORE any marker write or destructive reset/checkout/clean."""
|
||||
from ouroboros.utils import atomic_write_json
|
||||
|
||||
repo, head = _init_repo(tmp_path)
|
||||
_point_at(monkeypatch, tmp_path, repo, head)
|
||||
cur = _git(repo, "rev-parse", "HEAD").stdout.strip()
|
||||
marker = update_merge._update_tx_marker_path()
|
||||
atomic_write_json(
|
||||
marker,
|
||||
{
|
||||
SCHEMA_VERSION_KEY: None, "phase": "rolling_back",
|
||||
"pre_update_sha": cur, "pre_update_branch": head,
|
||||
},
|
||||
trailing_newline=True,
|
||||
)
|
||||
before = marker.read_bytes()
|
||||
assert update_merge.read_update_tx_strict() == ("corrupt", {})
|
||||
|
||||
dirty = repo / "uncommitted.txt"
|
||||
dirty.write_text("owner work the rollback must not clean away\n")
|
||||
ok, msg = update_merge.rollback_managed_update("null_stamp_probe")
|
||||
assert ok is False and "pre_update_sha" in msg # refuses on the empty corrupt tx
|
||||
assert marker.read_bytes() == before # byte-identical, never re-phased
|
||||
assert _git(repo, "rev-parse", "HEAD").stdout.strip() == cur
|
||||
assert dirty.read_text() == "owner work the rollback must not clean away\n"
|
||||
|
||||
|
||||
# ------------------------------------ N−1 fixtures through the boot finalizer
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue