diff --git a/README.md b/README.md index 5e8126dd58..096e2e2fd7 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,6 @@ This is a pre-prototype, not a working OS. The repository is intentionally rigor Reviewed checkpoints: Stage A v12 `4d2d361168d7b861c4a67df717139fa2357b8b8b`; retained v12 Stage B `13c80d2170fa796a16b9b60dfd3fb9995611deb7`; hardened operational validation/tests `bda378b76c7c06100dbd33c9e664d43ad31ff312`; inert Stage C scaffold `3e7a83554161b2d1f93d73f2a737873a50d42d1b`; inert Node1 protocol/recovery checkpoint `270129bce818377a1bc80f3fc02c1e9eb54ec2d9`; native Phase 1 checkpoint `6f0822a5a40ae24d41ba8805f374b0a7ccb8f7ca`. Earlier inactive/unfrozen statements in the frozen v12 and v13 candidate packets describe their preregistration checkpoints; they are not a reason to rewrite frozen packets or repeat a retained ceremony. Memory-heavy parser tests run sequentially under a 7 GiB address-space limit after earlier uncapped runs exhausted the development VM. -Start with [CORE.md](CORE.md), then [DECISIONS.md](DECISIONS.md), [BOOTSTRAP-1.md](BOOTSTRAP-1.md), and the current [D-011 v14 packet](decisions/d011_v14/D-011-PACKET.md). The original [architecture handoff](AOS-0_Architecture_Handoff.md) is archival context. +Start with [CORE.md](CORE.md), then [DECISIONS.md](DECISIONS.md) and [BOOTSTRAP-1.md](BOOTSTRAP-1.md); the governing boundary decision is [ADR-011](decisions/ADR-011-boundary-and-abi.md) (D-011 RESOLVED: TRUSTED_RUNTIME) and the governance decision is [ADR-012](decisions/ADR-012-promotion-evidence-governance.md). The [D-011 v14 packet](decisions/d011_v14/D-011-PACKET.md) is superseded historical implementation context, not current governance. The original [architecture handoff](AOS-0_Architecture_Handoff.md) is archival context. Licensed under [GNU GPLv3](LICENSE). diff --git a/decisions/d012/m_policy_generations.py b/decisions/d012/m_policy_generations.py index ba51efa872..a6240d7684 100644 --- a/decisions/d012/m_policy_generations.py +++ b/decisions/d012/m_policy_generations.py @@ -1,8 +1,9 @@ """M-A — D-012 policy-generations model (G-3). Grammar revision 4; -correction generation rev9 (rev6: registry lock; rev7/rev8: closed -type-exact injective tagged encoding + deep-copy insulation + -snapshot-derived fork detection; rev9: non-finite floats rejected and -single-snapshot intake — see the note at SCHEMA). +correction generation rev10 (rev6: registry lock; rev7/rev8: closed +type-exact injective tagged encoding + snapshot-derived fork detection; +rev9: non-finite floats rejected; rev10: NO copy protocol runs during +intake — one controlled traversal produces the owned canonical form, +cycles/depth typed FAIL_CLOSED — see the note at SCHEMA). Corrections: - Registry-owned records are stored as frozen immutable structures; no public @@ -27,7 +28,6 @@ Transition table (packet G-3 rev4): unknown dependency / unrecorded compatibility -> FAIL_CLOSED / INVALIDATE """ -import copy import math import threading @@ -53,25 +53,55 @@ class GenerationError(Exception): self.detail = detail -def _frozen(obj, path="value"): - """Closed, type-exact, INJECTIVE canonical encoding for registry-owned - generation data. Only exact built-in types are accepted (no subclasses): - None, bool, int, float, str, and dict/list/tuple containers thereof with - str dict keys. Every value is encoded with an explicit type tag, so - distinct policy structures remain distinct after freezing — {"a": 1} and - [("a", 1)] differ, as do True/1/1.0 and list/tuple — so a changed body - can never be misread as unchanged (rev7 verifier Finding H1). Sets, - bytes, subclasses, and any other type are typed FAIL_CLOSED.""" +_MAX_CANONICAL_DEPTH = 64 + + +def _frozen(obj, path="value", depth=0, _active=None): + """One controlled traversal producing the owned frozen canonical form. + Exact-type checks precede ANY object behavior: no caller-controlled + copy protocol (__deepcopy__, __reduce__, pickling) ever executes during + intake, so a hook cannot normalize an unsupported value into the + grammar or reenter the registry mid-intake (rev9-verifier Finding 1). + Every value is encoded with an explicit type tag, so distinct policy + structures remain distinct — {"a": 1} vs [("a", 1)], True vs 1 vs 1.0, + list vs tuple — and a changed body can never be misread as unchanged. + Cycles and excessive nesting are typed FAIL_CLOSED. The result holds + only exact-type immutable scalars and freshly built tagged tuples, so + no caller-owned mutable object is retained.""" + if depth > _MAX_CANONICAL_DEPTH: + raise GenerationError(FAIL_CLOSED, + f"policy value nesting too deep in {path}") + if _active is None: + _active = set() t = type(obj) if t is dict: - for k in obj: - if type(k) is not str: - raise GenerationError(FAIL_CLOSED, f"non-string key in {path}") - return ("dict", tuple(sorted((k, _frozen(v, path)) for k, v in obj.items()))) + if id(obj) in _active: + raise GenerationError(FAIL_CLOSED, f"cyclic policy value in {path}") + _active.add(id(obj)) + try: + for k in obj: + if type(k) is not str: + raise GenerationError(FAIL_CLOSED, f"non-string key in {path}") + return ("dict", tuple(sorted((k, _frozen(v, path, depth + 1, _active)) + for k, v in obj.items()))) + finally: + _active.discard(id(obj)) if t is list: - return ("list", tuple(_frozen(v, path) for v in obj)) + if id(obj) in _active: + raise GenerationError(FAIL_CLOSED, f"cyclic policy value in {path}") + _active.add(id(obj)) + try: + return ("list", tuple(_frozen(v, path, depth + 1, _active) for v in obj)) + finally: + _active.discard(id(obj)) if t is tuple: - return ("tuple", tuple(_frozen(v, path) for v in obj)) + if id(obj) in _active: + raise GenerationError(FAIL_CLOSED, f"cyclic policy value in {path}") + _active.add(id(obj)) + try: + return ("tuple", tuple(_frozen(v, path, depth + 1, _active) for v in obj)) + finally: + _active.discard(id(obj)) if t in (str, int, float, bool, type(None)): if t is float and not math.isfinite(obj): # NaN/±inf are not reflexively comparable: distinct NaNs compare @@ -118,39 +148,42 @@ class Registry: raise GenerationError(FAIL_CLOSED, "bad or duplicate generation id") if not _ident(evaluator): raise GenerationError(FAIL_CLOSED, "evaluator identity required") - # rev8 verifier Finding 3: single owned snapshot — the caller's - # composite is materialized exactly once (deep-copied), and - # validation + freezing operate only on that owned copy; there is no - # second traversal of caller-owned data. Snapshot/copy failures are - # typed FAIL_CLOSED. (Concurrent mutation DURING materialization can - # still yield a hybrid but internally consistent snapshot of some - # interleaving; callers supplying concurrently mutated trees must - # synchronize — recorded as a caller obligation.) - try: - owned = copy.deepcopy({"id": gen_id, "parent": parent, - "scope": scope, "evaluator": evaluator, - "rules": rules}) - except Exception as exc: - raise GenerationError(FAIL_CLOSED, - f"generation snapshot unavailable: {exc}") - payload = _frozen(owned) + # rev9-verifier Finding 1: NO generic copy protocol runs during + # intake. One controlled traversal (_frozen) exact-type-checks + # every node BEFORE touching it and produces the owned frozen + # canonical form — a caller __deepcopy__/__reduce__ hook can neither + # normalize an unsupported value into the grammar nor reenter the + # registry mid-intake. Nothing caller-owned and mutable is + # retained. (Concurrent mutation of caller containers DURING the + # read-only traversal can still yield a hybrid but internally + # consistent snapshot of some interleaving; callers supplying + # concurrently mutated trees must synchronize — caller obligation.) + payload = _frozen({"id": gen_id, "parent": parent, "scope": scope, + "evaluator": evaluator, "rules": rules}) if parent is not None: - if not _ident(parent) or parent not in self.__records: + # parent/scope are derived from the owned frozen form + payload_parent = _field(payload, "parent")[1] + payload_scope = _field(payload, "scope") + if not _ident(payload_parent) or payload_parent not in self.__records: raise GenerationError(FAIL_CLOSED, "unknown dependency: parent") - if parent in self.__revoked: + if payload_parent in self.__revoked: raise GenerationError(REJECT, "parent revoked") - # fork detection derives ONLY from the validated owned snapshot + # fork detection derives ONLY from the owned snapshot # (rev7 verifier Finding H2): caller-owned objects are never - # re-read after the payload snapshot is taken - payload_scope = _field(payload, "scope") - siblings = [c for c in self.__children.get(parent, ()) + # re-read after the snapshot exists + siblings = [c for c in self.__children.get(payload_parent, ()) if _field(self.__records[c], "scope") == payload_scope] if siblings: raise GenerationError(REJECT, "fork: second child of one parent, same scope") + # defense in depth (rev9-verifier Finding 1): no caller code has run + # since the initial duplicate check, but the precondition is + # re-verified immediately before the commit + if gen_id in self.__records: + raise GenerationError(FAIL_CLOSED, "duplicate generation id at commit") object.__setattr__(self, "_Registry__records", {**self.__records, gen_id: payload}) if parent is not None: object.__setattr__(self, "_Registry__children", - {**self.__children, parent: self.__children.get(parent, ()) + (gen_id,)}) + {**self.__children, payload_parent: self.__children.get(payload_parent, ()) + (gen_id,)}) return dict(payload[1]) def revoke(self, gen_id): diff --git a/decisions/d012/test_d012_models.py b/decisions/d012/test_d012_models.py index 4493ca14c0..c5a230fb6a 100644 --- a/decisions/d012/test_d012_models.py +++ b/decisions/d012/test_d012_models.py @@ -3,13 +3,14 @@ precedence, evaluator binding, M-A/M-B composition (admission decisions compose into M-B), mandatory currentness with typed failure, atomic transitions, durable crash cuts, and the global replay domain. -Correction generation rev9: no-mutation postconditions after typed +Correction generation rev10: no-mutation postconditions after typed REJECT (rev6); reentrant recovery/expiry mid-currentness rejections and the cross-registry final-point linearizability probes over every commit path (rev6/rev7); required lock protocol, closed injective value grammar, closed time domain, and snapshot-derived fork detection (rev7/rev8); immutable expiry ceiling, non-finite float rejection, and single-snapshot -pair intake (rev9). Schema constants remain grammar revision 4. +pair intake (rev9); no-copy-protocol intake with cycle/depth guards +(rev10). Schema constants remain grammar revision 4. """ import sys, unittest, pathlib @@ -622,18 +623,19 @@ class AdmissionReplayTests(unittest.TestCase): def test_fork_decision_derives_from_stored_snapshot(self): """rev7 verifier Finding H2: the fork decision corresponds to the - scope actually STORED, not a second read of the caller's (possibly + scope actually STORED, not a re-read of the caller's (possibly concurrently mutated) object. The probe mutates the caller's scope - in the window between the owned snapshot and the fork comparison — - exactly the H2 race — and requires the fork to be detected.""" - original_deepcopy = m_policy_generations.copy.deepcopy + immediately after the owned canonical form exists — exactly the H2 + window — and requires the fork to be detected.""" + original_frozen = m_policy_generations._frozen scope = {"tier": "X"} mutated = {"flag": False} - def spying_deepcopy(obj, *args, **kwargs): - result = original_deepcopy(obj, *args, **kwargs) - # simulate the caller mutating its scope object right after the - # owned snapshot is taken, before the fork comparison runs - if isinstance(result, dict) and "evaluator" in result: + def spying_frozen(obj, *args, **kwargs): + result = original_frozen(obj, *args, **kwargs) + # mutate the caller's object right after the owned snapshot is + # produced, before the fork comparison runs + if (isinstance(result, tuple) and len(result) == 2 + and result[0] == "dict" and "evaluator" in dict(result[1])): mutated["flag"] = True scope["tier"] = "Y" return result @@ -641,16 +643,65 @@ class AdmissionReplayTests(unittest.TestCase): gens.add("G0", None, scope, "EVAL-1", {"rule": 1}) # root, stored scope X gens.add("G1", "G0", scope, "EVAL-1", {"rule": 1}) # first child, scope X self.assertEqual(scope["tier"], "X") # setup did not need the spy - m_policy_generations.copy.deepcopy = spying_deepcopy + m_policy_generations._frozen = spying_frozen try: with _expect(GenerationError, REJECT): gens.add("G2", "G0", scope, "EVAL-1", {"rule": 1}) # second child, stored scope X: fork finally: - m_policy_generations.copy.deepcopy = original_deepcopy + m_policy_generations._frozen = original_frozen self.assertTrue(mutated["flag"]) # the race window was exercised with _expect(GenerationError, FAIL_CLOSED): gens.record("G2") # the forked child was not stored + def test_copy_protocols_never_execute_during_intake(self): + """rev9-verifier Finding 1: caller-controlled copy behavior + (__deepcopy__/__reduce__) must never run before closed-grammar + rejection — a hook can neither normalize an unsupported value into + the grammar nor reenter add() to create/overwrite a generation.""" + gens = GenRegistry() + hook = {"calls": 0} + class HookValue: + def __deepcopy__(self, memo): + hook["calls"] += 1 + try: # a reentrant add must not happen + gens.add("G1", None, "scope-X", "EVAL-1", {"rule": 1}) + except GenerationError: + pass + return {"normalized": 1} # and must not normalize + with _expect(GenerationError, FAIL_CLOSED): + gens.add("G1", None, HookValue(), "EVAL-1", {"rule": 1}) + self.assertEqual(hook["calls"], 0) # no copy protocol executed + with _expect(GenerationError, FAIL_CLOSED): + gens.record("G1") # nothing was stored, nothing overwritten + # and a hook in the rules position is equally rejected + with _expect(GenerationError, FAIL_CLOSED): + gens.add("G1", None, "scope-X", "EVAL-1", {"rule": HookValue()}) + self.assertEqual(hook["calls"], 0) + with _expect(GenerationError, FAIL_CLOSED): + gens.record("G1") + + def test_cyclic_and_over_deep_policy_values_fail_closed(self): + """rev9-verifier Finding 1: cyclic and excessively nested policy + values are typed FAIL_CLOSED, never a bare RecursionError.""" + gens = GenRegistry() + cyc_list = [] + cyc_list.append(cyc_list) + with _expect(GenerationError, FAIL_CLOSED): + gens.add("G1", None, cyc_list, "EVAL-1", {"rule": 1}) + cyc_dict = {} + cyc_dict["self"] = cyc_dict + with _expect(GenerationError, FAIL_CLOSED): + gens.add("G1", None, "scope-X", "EVAL-1", cyc_dict) + deep = current = [] + for _ in range(80): + nxt = [] + current.append(nxt) + current = nxt + with _expect(GenerationError, FAIL_CLOSED): + gens.add("G1", None, "scope-X", "EVAL-1", {"d": deep}) + with _expect(GenerationError, FAIL_CLOSED): + gens.record("G1") # nothing was stored + def test_reentrant_later_commit_blocks_outer_and_preserves_monotonic(self): """rev7 verifier Finding H3 (1): a currentness callback that reenters and commits an operation at a later instant, then recovers, must