Skip to content

fix(anchors): verify_rfc3161 keeps the never-raise rule it prescribes to others - #142

Merged
b7n0de merged 2 commits into
mainfrom
fix/rfc3161-type-floor
Aug 16, 2026
Merged

fix(anchors): verify_rfc3161 keeps the never-raise rule it prescribes to others#142
b7n0de merged 2 commits into
mainfrom
fix/rfc3161-type-floor

Conversation

@b7n0de

@b7n0de b7n0de commented Aug 16, 2026

Copy link
Copy Markdown
Owner

The contradiction this closes

register_anchor_type tells third-party authors that a verifier "MUST be fail-closed … never raise for an ordinary bad proof". This first-party implementation did not hold its own rule. frozen and rp_trust are consumed with .get(...), so a non-dict raised a raw AttributeError out of a surface whose job is to return a verdict.

The self-gate run recorded it as F3 on 2026-07-31. Re-measured against main today, sixteen days later:

rp_trust=123 -> AttributeError: 'int' object has no attribute 'get'
frozen=123   -> AttributeError: 'int' object has no attribute 'get'

Why it survived sixteen days, which is the more useful half

Two axes of the never-raise family property were blind to it at the same time:

Axis Why it missed this Status
module anchors_rfc3161 was not in _MODULES; the property never entered the module closed by the coverage guard in #141
argument the property fuzzes only the primary parameter, and both affected arguments are keyword-only — even inside the population it does not reach them still open (the self-gate's F2)

So this PR adds a test that covers this one surface directly, rather than relying on a sweep that provably does not reach it. Relying on it would be closing the instance while believing the class was closed.

The floor, not a wider except

Same reasoning as the evalcard / prereg floors (L1-01). An except AttributeError would close this one shape, let the next type-confusion sibling through, and swallow a genuine internal AttributeError on top. BundleFormatError is in the family's accepted set, so the surface still decides instead of crashing.

A measurement precondition worth stating

Without proofbundle[anchors] installed, verify_rfc3161 returns at its optional-import guard before these lines. A probe run that way reads green for a reason that has nothing to do with the defence it names — the class this repository's own fixture manifest calls vacuous_seam_passes_for_a_reason_other_than_the_defence_it_names. The new tests therefore skip honestly when the extra is absent, instead of passing vacuously. That distinction cost a wrong reading earlier today before it was caught.

Bidirectional evidence

With the floor Floor temporarily removed
new tests 5 green FAILED (failures=1, errors=8)
failure message frozen=123 still raises a raw AttributeError: 'int' object has no attribute 'get'

The failure reproduces the original finding verbatim, so the green is the fix rather than a coincidence. Two of the five tests exist for the other direction: the documented rp_trust=None default and a valid mapping must still reach a verdict, so the floor cannot have broken the surface it protects.

Full suite Ran 2035 tests, OK (skipped=10) · ruff clean · mypy: no issues in 63 source files.

Honest severity, unchanged from the finding

This surface is not exported at package level, and verify_anchor wraps every verifier in except Exception, so nothing leaked over the public path. The contradiction was the point — the project's own implementation not keeping the rule it prescribes to others.

Not part of the 3.8.0 release candidate (#140), whose delta is a single file. This targets main on its own.

kraxo and others added 2 commits August 16, 2026 14:11
… to others

`register_anchor_type` documents that a verifier "MUST be fail-closed … never
raise for an ordinary bad proof". This first-party implementation did not hold
its own rule: `frozen` and `rp_trust` are consumed with `.get(...)`, so a
non-dict raised a raw `AttributeError` out of a verdict-returning surface.

Recorded by the self-gate run as F3 on 2026-07-31. Re-measured against main
today, sixteen days later, and it still reproduced:

  rp_trust=123 -> AttributeError: 'int' object has no attribute 'get'
  frozen=123   -> AttributeError: 'int' object has no attribute 'get'

WHY IT SURVIVED SO LONG, and this is the part worth keeping. Two axes of the
never-raise family property were blind to it at once:

  MODULE axis   -- `anchors_rfc3161` was not in `_MODULES`, so the property
                   never entered the module at all. Closed separately by the
                   coverage guard in PR #141.
  ARGUMENT axis -- the property fuzzes only the PRIMARY parameter, and both
                   affected arguments are keyword-only. Even inside the
                   population the property does not reach them. That is the
                   finding the self-gate recorded as F2 and it stays OPEN.

So this test file covers this one surface directly instead of pretending the
general sweep does. A fix that relied on the sweep would be closing the instance
while believing it closed the class.

THE FLOOR, NOT A WIDER EXCEPT, for the same reason evalcard and prereg carry
theirs (L1-01): an `except AttributeError` would close this one shape, let the
next type-confusion sibling through, and swallow a genuine internal
AttributeError on top. `BundleFormatError` is in the family's accepted set, so
the surface still DECIDES instead of crashing.

MEASUREMENT PRECONDITION, learned the hard way today: without
`proofbundle[anchors]` the function returns at its optional-import guard BEFORE
these lines, and a probe reads green for a reason unrelated to the defence it
names -- the class the fixture manifest calls
`vacuous_seam_passes_for_a_reason_other_than_the_defence_it_names`. The tests
therefore SKIP honestly when the extra is absent rather than pass vacuously.

BIDIRECTIONAL EVIDENCE. With the floor: 5 green. Without it (temporarily
removed): FAILED failures=1 errors=8, and the failure message reproduces the
original finding verbatim -- "frozen=123 still raises a raw AttributeError:
'int' object has no attribute 'get'". Two of the five tests exist for the other
direction: the documented `rp_trust=None` default and a valid mapping must still
reach a verdict, so the floor cannot have broken the surface it protects.

full suite Ran 2035 tests OK skipped=10 · ruff clean · mypy: no issues in 63
source files.

HONEST SEVERITY, unchanged from the finding: this surface is not exported at
package level and `verify_anchor` wraps every verifier in `except Exception`, so
nothing leaked over the public path. The contradiction was the point -- the
project's own implementation not keeping the rule it prescribes to third-party
authors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The counter-read rejected the previous commit on my own doubt #2, which I had
written down and then not acted on -- the third time today that the same shape
came back to me from outside.

`isinstance(x, dict)` programs to an implementation. The following logic calls
`.get(...)`, so the question is whether the argument is a Mapping, not whether
it is exactly a dict. `MappingProxyType`, `OrderedDict` and any dict-like object
were being refused for no reason.

MEASURED before changing it, so one wrong check would not be swapped for
another: every use of `frozen` and `rp` in this function is `.get(...)` -- six
call sites, no subscript, no dict-only method, no mutation. `.get` belongs to
the `Mapping` protocol, so `collections.abc.Mapping` is not merely more
permissive here, it is exactly the right predicate.

A floor that rejects valid input is a defect of its own. It is quieter than the
crash it replaced, which makes it worse to find, not better.

BIDIRECTIONAL EVIDENCE for the correction itself: with `Mapping` 6 tests green;
with the old `dict` check restored, FAILED errors=1 --
"BundleFormatError: frozen must be a mapping, got mappingproxy (fail-closed)".
The new test therefore discriminates; without that counter-check it would be a
test that measures nothing.

full suite Ran 2036 tests OK skipped=10 · ruff clean · mypy: no issues in 63
source files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@b7n0de
b7n0de merged commit 79556fa into main Aug 16, 2026
22 checks passed
@b7n0de
b7n0de deleted the fix/rfc3161-type-floor branch August 16, 2026 15:47
b7n0de pushed a commit that referenced this pull request Aug 16, 2026
…llen gegen meine

Zwei Dateien kollidierten, beide weil #141/#142 dieselbe Arbeit schon gelandet hatten, waehrend ich
sie lokal noch einmal machte. Aufgeloest zugunsten von main, nicht aus Hoeflichkeit, sondern
gemessen:

1. `_ACCEPTED`: ich hatte `OSError` aufgenommen. main nahm nur `FileNotFoundError` — und ihr
   Kommentar haelt fest, dass der ERSTE Versuch dort ebenfalls `OSError` war und von einer
   Gegenlesung als REJECT gefangen wurde. `OSError` ist die Basisklasse von `PermissionError`,
   `TimeoutError`, `BrokenPipeError`; sie alle stillschweigend zu akzeptieren ist ein FAIL-OPEN auf
   genau der Achse, die diese Eigenschaft verteidigt. Ich habe denselben Fehler ein zweites Mal
   gemacht, mit derselben Selbstbegruendung. Mains Fassung uebernommen.

2. `emit.load_signer`: mains Typboden ist aequivalent zu meinem und besser dokumentiert.

WAS AUS MEINER ARBEIT BLEIBT, weil es main NICHT hat:
- `tests/test_load_signer_fd_hazard.py` belegt, dass der Deskriptor NICHT GELESEN wird (er ist nach
  dem Aufruf noch offen), nicht bloss dass etwas geworfen wurde. Gegen mains load_signer gruen.
- Die Unterpaket-Luecke: mains Deckungs-Waechter nutzt `_SRC.glob("*.py")`, also nur die oberste
  Ebene. Gemessen auf dem zusammengefuehrten Baum: 90 Flaechen in 40 Modulen, und
  `experimental.enclave` ist NICHT dabei — obwohl es ausgeliefert wird, dokumentierter Importpfad
  und CLI-Unterbefehl ist. Folgt als eigener Commit.

Batterie auf dem zusammengefuehrten Baum: 2161 passed, 9 skipped, 3 xfailed, 228 subtests, ruff
clean. Der eine rote Test bleibt der fehlende Vor-Tag-Eintrag.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant