Retire all archive member surgery — SPEC §6.1 satisfied (closes #119) - #122
Conversation
Pins: libs/nistcurves v0.11.0 -> v0.11.2, libs/x25519 v0.11.1 -> v0.11.2. c64-https no longer edits any archive member set. The §8.0 APP_OWNED shape is requested through CONTRACT_DEFINES (SPEC §6.2) instead: -D SHARED_SQTAB_INIT -D SHARED_REU_MUL_INIT -D SHARED_REU_MUL_FETCH -D SHARED_CT_MUL_8X8 -D LIB_NO_BARE_EXPORTS=1 -D LIB_SHARED_SQTAB_BASE=0xBC00 Three drops (mul_8x8.o, data_shared.o, reu_mul_init.o) and the generated glue TU for mul_8x8_onchip.o are all gone. WHAT THAT BUYS The manifest now ATTESTS the deferral rather than us resolving it by deletion: SHARED_PRIMITIVES=$0000, SHARED_CONSUMES=$0007. So the §8.0 disjointness and coverage asserts — recorded in lib_contract_asserts.s as "not writable today" since the clause landed — are live, and profile-independently, since the old REU-vs-onchip mask split was a property of which bodies each archive shipped and neither ships any now. OWNERSHIP MOVES, ALL TOWARD WHAT §8.0 ALWAYS IMPLIED poly_prod_lo/hi we provide the §8.3 ct_mul_8x8 body, so we own its product scratch. poly1305.s exports in every profile; it used to import under onchip. sqtab_lo/hi we provide sqtab_init, so we own the table. data.s defines it in every profile and sqtab_reserved is gone. The sibling still reads via its own equates derived from LIB_SHARED_SQTAB_BASE (sqtab_hi = base+$0200), so the labels MUST land at $BC00/$BE00 — the Makefile check now asserts sqtab_lo rather than the retired placeholder. ABI import prefixed LIB_NISTCURVES_ABI_VERSION; the bare name is suppressed by the export gate. Both are silent-failure surfaces: a second definition of either links clean and yields wrong arithmetic with no diagnostic. The KATs are the evidence, not the link. TWO UPSTREAM FIXES MADE THIS POSSIBLE v0.11.1 made SHARED_CT_MUL_8X8 assemble against the on-chip TU (c64-nist-curves#123, filed from here) — before it, the glue TU was the only route. v0.11.2 added the knob-staleness guard: a changed CONTRACT_DEFINES used to reuse stale objects and exit 0 with a different archive than requested. An earlier attempt at this change, before that guard, produced a comb image that would not boot; the cause looked like these ownership moves and was actually mixed objects. Anything inexplicable after a define change: check the pin is >= v0.11.2 first. VERIFICATION ECDSA KAT oracle 6/6 on REU, onchip AND comb builds (3 valid accepted, 3 invalid rejected each) — comb is the one that exercises the poly_prod rendezvous through og_common at boot. REU-profile PRGs byte-identical to the previous release (7a02e213d014, b574b344cdca); onchip/comb changed as expected. make package-verify: 11/11 artifact checks, RELEASE ARTIFACTS VERIFIED. Closes#119. §13 (#70) remains open and deferred. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000
commented
Aug 16, 2026
Evaluated against SPEC v0.10.6, with the v0.11.2 upstream source and both release notes in hand. The migration itself is right: this is exactly the §6.1→§6.4→§6.6 repair the clause chain was written for, and the evidence bar (KATs on all three profiles including comb's Verified
Fix before merge: |
… the zp_config rebuild Three findings from the contract-side review of #122. All three verified against the v0.11.2 source before acting. 1. SHARED_CONSUMES IS NOT PROFILE-INDEPENDENT (comment was wrong) The §8.0 block claimed CONSUMES = $0007 "PROFILE-INDEPENDENT". Measured with od65 on the staged archives: archive PRIMITIVES CONSUMES lib-p256-verify (REU) $0000 $0007 lib-p256-verify-onchip $0000 $0005 lib-p256-comb-onchip $0000 $0005 Only PRIMITIVES is profile-independent. Under FP_ONCHIP_MUL the manifest zeroes the reu_mul bit in BOTH masks because that build genuinely does not read the primitive, and upstream hard-asserts it. That is §8.0's three-state table working: a deferral switch drops a bit from ownership only; a profile gate drops it from ownership AND consumption. No assert change needed — all three pass either way, since $0005 & ~$0007 = 0. The comment now carries the table so the next person to od65 an onchip archive does not "discover" a contradiction. 2. THE COVERAGE ASSERT CANNOT FIRE TODAY (scope was oversold) APP_OWNED is $0007, covering every §8.0 bit allocated so far, and CONSUMES is drawn from those same bits — so CONSUMES & ~(APP_OWNED | PRIMITIVES) is identically zero. The comment sold it as catching "a deferral requested but not honoured"; it cannot. That case is covered by the PRIMITIVES = 0 assert (defines missing from the manifest TU) or by a duplicate-external link error (defines missing from the code TUs), and the poly_prod rendezvous is caught by neither — only by the comb/onchip KAT. Kept, because it is the clause's canonical form and arms itself when a fourth primitive is allocated. 3. THE ZP_CONFIG REBUILD WAS THE SURVIVING TAIL OF #119 "No member set is edited" was true, but the wrapper still rebuilt zp_config*.o out-of-band and re-archived — passing only the ZP overrides and NOT CONTRACT_DEFINES. Since zp_config.s gates its bare exports on `.ifndef LIB_NO_BARE_EXPORTS`, that one member re-exported the bare `zp_*` names every other member was built to suppress. One archive, two configurations (SPEC §6.2) — and the resurrected names are exactly the #83 ZP collision family the gate exists to prevent. Dormant with one contract library in the link; live the day a second joins. Fixed by the sanctioned route: the three overrides move to CONTRACT_ZP_DEFINES (upstream since nist#104, covered by v0.11.2's staleness stamp). That retires the rebuild, the member-name discovery case block, the staging directory AND the re-archive step. The archive we link is now byte-for-byte the one upstream's make produced, which is what makes §6.1's "no copying intermediates around" true by construction. The od65 post-check is kept and strengthened: it verifies the three slots from the emitted object (canonical spellings only — the bare aliases vanish under the gate, and a guard that can go vacuous is worse than none) and adds a new check that the bare zp_ptr2 is NOT re-exported. It reads od65's hex field directly rather than the parenthesised decimal. EVIDENCE All five PRGs byte-identical to the pre-fix build (7a02e213d014, b574b344cdca, 6acfa0ef9cfd, d59254702819, dcde8eb8f150) — the bare export was unreferenced, so removing it changes no shipped byte. The comb and onchip KATs already run at exactly those hashes (6/6 each), so their evidence carries; re-running identical bytes would prove nothing new. make package-verify: 31/31 logic, 11/11 artifact, RELEASE ARTIFACTS VERIFIED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000
commented
Aug 16, 2026
Thank you — all three findings were correct. Verified each against the v0.11.2 source before acting rather than taking them on trust, and pushed 1. |
| archive | PRIMITIVES | CONSUMES |
|---|---|---|
lib-p256-verify (REU) | $0000 | $0007 |
lib-p256-verify-onchip | $0000 | $0005 |
lib-p256-comb-onchip | $0000 | $0005 |
The comment now carries that table plus the reason — a deferral switch drops a bit from ownership only, a profile gate drops it from ownership and consumption — and says outright not to "correct" $0005 to $0007 on an onchip archive. No assert changed; all three pass either way since $0005 & ~$0007 = 0. The PR description above is corrected too.
2. Coverage assert — scope note added
You're right that it's a tautology today, and the comment oversold it. It now states plainly that it cannot fire while APP_OWNED = $0007 covers every allocated bit, that the failure it sounds like it catches is actually covered by the PRIMITIVES = 0 assert or by a duplicate-external link error, and — the part that mattered most to get on the page — that the poly_prod rendezvous is caught by neither, only by the comb/onchip KAT. Kept the assert for the reason you give: canonical form, and it arms itself when a fourth primitive lands.
3. zp_config tail — folded in rather than deferred
Confirmed the defect before fixing: the rebuilt zp_config_p256verify.o did export the bare zp_ptr2 while every other member in the same archive suppressed it.
Routing the three overrides through CONTRACT_ZP_DEFINES works exactly as you said, and retired more than the rebuild — the member-name discovery case block, the staging directory and the re-archive step all went with it. The archive we link is now byte-for-byte the one upstream's make produced, which is the version of §6.1 that needs no inspection to believe.
The od65 post-check is kept and strengthened: canonical spellings only (the bare aliases vanish under the gate, and a guard that can go vacuous under a build-tightening flag is worse than none), plus a new check that the bare zp_ptr2 is not re-exported — the exact regression you found, now guarded. It also reads od65's hex field rather than the parenthesised decimal; the first version of that check compared a hex string against a decimal one and failed loudly on a correct build, which is at least the right direction to fail in.
I also fixed the stale header comment you flagged — it claimed we "cannot pass -D overrides" and "build upstream with its defaults", both untrue since the knobs landed.
Evidence for the follow-up
All five PRGs are byte-identical to the pre-fix build (7a02e213d014, b574b344cdca, 6acfa0ef9cfd, d59254702819, dcde8eb8f150) — the bare export was unreferenced, so removing it changes no shipped byte. The comb and onchip KATs already ran at exactly those hashes, so that evidence carries; re-running identical bytes would prove nothing new. make package-verify: 31/31 logic, 11/11 artifact, RELEASE ARTIFACTS VERIFIED, no skips.
So #119 closes without a successor issue. §13/#70 remains open and untouched.
JC-000
commented
Aug 16, 2026
Re-verified
One non-blocking nit on the new member discovery: Nothing further from my side — #119 closes clean with no successor, and I'll refresh the contract repo's consumers.md row once this merges. |
Follow-up nit from the contract-side review of #122, and it was my regression: the pre-existing code discovered the ZP member with `ar65 t` on the archive, and when I removed the staging step I replaced that with `ls "$LIB_BUILD"/zp_config_*.o | head -1`. All three profiles build from the same libs/nistcurves tree, so several variants' ZP objects coexist there. Reproduced: after a comb build followed by a REU build, ls zp_config_*.o | head -1 -> zp_config_p256comb.o REU archive actually ships -> zp_config_p256verify.o so the post-check was reading an object the archive being linked does not contain. It passed anyway — v0.11.2's staleness stamp guarantees every object in the directory carries the same knob string, so the values were right — but a guard that inspects a different artifact than the one shipped is the wrong shape whatever it happens to return, and a variant-gated slot could someday differ between variants' ZP TUs. Now derived from the archive being linked: ZP_MEMBER="$( ar65 t "$UPSTREAM_ARCHIVE" | grep '^zp_config' )" with explicit zero-match and multi-match failures rather than a silent head -1, plus a check that the named member exists in the build dir. Listing an archive is not extracting it, so §6.1 stays clean. PRG-neutral, as build tooling should be: all five images byte-identical (7a02e213d014, b574b344cdca, 6acfa0ef9cfd, d59254702819, dcde8eb8f150), so the comb and onchip KAT evidence carries unchanged. make package-verify: 31/31 logic, 11/11 artifact, RELEASE ARTIFACTS VERIFIED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000
commented
Aug 16, 2026
Taken, in Reproduced before fixing, since "may not be the member shipped" deserved confirming rather than assuming. After a comb build followed by a REU build in the same tree: So the check was reading an object the linked archive does not contain. It passed anyway, exactly for the reason you gave — the staleness stamp means every object in the directory carries the same knob string — but you're right that it's the wrong shape regardless of what it returns. Now derived from the archive being linked: ZP_MEMBER="$("$AR65" t "$UPSTREAM_ARCHIVE"| grep '^zp_config')"with explicit zero-match and multi-match failures instead of a silent PRG-neutral, as build tooling should be — all five images byte-identical, so the comb and on-chip KAT evidence carries unchanged. Thanks for all three rounds — the |
Closes#119. c64-https no longer edits any archive member set — SPEC §6.1 is satisfied.
Pins move to
libs/nistcurvesv0.11.2 andlibs/x25519v0.11.2.What changed
The §8.0 APP_OWNED shape is now requested through
CONTRACT_DEFINES(SPEC §6.2) rather than achieved by deleting members:Gone: the three member drops (
mul_8x8.o,data_shared.o,reu_mul_init.o) and the generated glue TU that.included pristine library source formul_8x8_onchip.o.The payoff
The manifest now attests the deferral instead of us resolving it by deletion. That makes the §8.0 disjointness and coverage asserts live, having been recorded in
lib_contract_asserts.sas "not writable today" since the clause landed.Measured on the staged archives — note that only
PRIMITIVESis profile-independent:PRIMITIVESCONSUMESlib-p256-verify(REU)$0000$0007lib-p256-verify-onchip$0000$0005lib-p256-comb-onchip$0000$0005Under
FP_ONCHIP_MULthe manifest zeroes the reu_mul bit in both masks, because that build genuinely does not read the primitive — a deferral switch drops a bit from ownership only, a profile gate drops it from ownership and consumption. All three asserts pass either way ($0005 & ~$0007 = 0).The coverage assert is a tautology today and is documented as such:
APP_OWNED = $0007covers every allocated bit, so it cannot fire. It is kept as the clause's canonical form, and it arms itself when a fourth primitive is allocated. It does not guard the poly_prod rendezvous — only the comb/onchip KAT does.Ownership moves
All three go the direction §8.0 always implied — the provider of a primitive owns its state:
poly_prod_lo/hict_mul_8x8body)sqtab_lo/hisqtab_init)sqtab_reservedplaceholder retiredLIB_NISTCURVES_ABI_VERSIONBoth buffer moves are silent-failure surfaces: a second definition of either links clean and produces wrong arithmetic with no diagnostic anywhere. The KATs are the evidence, not the link.
sqtabadditionally has a hard address constraint — the sibling reads through its own equates derived fromLIB_SHARED_SQTAB_BASE, derivingsqtab_hi = base+$0200, so the labels must land at$BC00/$BE00. The Makefile's post-link check now assertssqtab_lorather than the retired placeholder.Two upstream fixes made this possible
SHARED_CT_MUL_8X8assemble against the on-chip TU (c64-nist-curves#123, filed from this work). Before it, the glue TU was the only route.CONTRACT_DEFINESused to reuse stale objects and exit 0 with a different archive than requested. An earlier attempt at this change, before that guard, produced a comb image that would not boot — and the cause looked like these ownership moves when it was actually mixed objects. Worth knowing: anything inexplicable after a define change, check the pin is ≥ v0.11.2 before debugging anything else.Verification
poly_prodrendezvous throughog_commonduring its boot precompute, which is exactly the silent-failure path.7a02e213d014,b574b344cdca); on-chip and comb changed as expected.make package-verify: 31/31 gate logic tests, 11/11 artifact checks,RELEASE ARTIFACTS VERIFIED, no skips, clean provenance.Also in this PR (from contract-side review)
The
zp_configrebuild was the surviving tail of #119: it re-archived one member built withoutCONTRACT_DEFINES, so that member re-exported the barezp_*names every other member suppressed — one archive, two configurations (§6.2), and the resurrected names are the #83 ZP collision family. Routing the overrides throughCONTRACT_ZP_DEFINESretired the rebuild, the member-name discovery block, the staging directory and the re-archive step together. The archive we link is now byte-for-byte upstream's. PRG-neutral: all five hashes unchanged.Still open
§13 net ABI (#70) — deferred, unchanged by this.
🤖 Generated with Claude Code