From 3f1fe093059e1c6e3e6b198174a668888b06d130 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:14:18 -0500 Subject: [PATCH] =?UTF-8?q?fix(contract):=20SHARED=5FPRIMITIVES=20is=20pro?= =?UTF-8?q?file-dependent=20=E2=80=94=20unblock=20both=20onchip=20variants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both FP_ONCHIP_MUL variants failed to link at master 21f2ce5: ld65: Error: src/lib_contract_asserts.s(166): libs/nistcurves shared-primitive ownership claim moved — re-derive the archive member drops in tools/integration/build_nistcurves_p256.sh before touching this assert That is half the release matrix, and `make package` aborted before writing dist/build-info.txt, so packaging could not run at all. Root cause: LIB_NISTCURVES_SHARED_PRIMITIVES is the same exported symbol in both builds, but the REU and FP_ONCHIP_MUL archives ship different lib_manifest* objects, so its VALUE is profile-dependent. Measured with od65 on the staged archives at both pins: profile v0.6.0 v0.9.1 default (REU) $0007 $0007 FP_ONCHIP_MUL $0007 $0005 <- split FP_ONCHIP_MUL + comb $0007 $0005 <- split $0007 -> $0005 is the SPEC 8.2 reu_mul bit dropping out, upstream at v0.8.0 (c64-nist-curves #78). It is correct behaviour rather than drift: the onchip profile has no REU multiply path, so owning nothing there is the truthful claim, and a profile switch drops the bit from SHARED_PRIMITIVES and SHARED_CONSUMES alike. The `= APP_OWNED` ($0007) pin was correct when measured at the v0.6.0 pin; the v0.9.1 bump carried the split in. The archive surgery needed no re-derivation, and that was verified rather than assumed before changing the constant. The SPEC 8 primitive bodies each archive actually exports are the same set at v0.6.0 and v0.9.1 — REU ships only reu_mul_init/reu_mul_tables_init (never pulled; boot.s:31 exports its own), onchip ships sqtab_lo/sqtab_hi (absolute equates baked to $BC00, not bodies) plus reu_fetch_mul_row, og_common, og_src_ld. Neither ships sqtab_init, ct_mul_8x8, mul_8x8, poly_prod_* or smc_*. Provider tables read from the link maps confirm every primitive body in every profile comes from c64-https (poly1305.o / data.o / boot.o), and `od65 --dump-imports` across all linked objects shows nobody imports reu_fetch_mul_row in the onchip build — so its two definitions ($4200 library-exported, $0B15 boot-local) never compete. Fix is the expected value only, kept as an exact equality pin per profile so a future split still trips it. Both error messages are rewritten: the old text sent the reader to re-derive the wrapper's member drops, which is the wrong remedy for the failure it actually fires on and cost real debugging time. Each arm now names the real cause, states the expected value for that profile, says which lib_manifest to read, and says to touch the wrapper only if the archive's exported primitive bodies changed too. Evidence — all five variants link from clean, every PRG hash byte-identical to the images verified before the v0.9.1 merge, so the assert costs zero bytes: uci-reu 66e37037deb9b295 62,977 B uci-onchip 518ee446e57af1c5 62,977 B ip65-reu d522e68469b8fc09 47,105 B ip65-onchip 132685878cae506d 47,105 B uci-comb 2982865332107f69 62,977 B tools/test_ecdsa_kat_oracle.py 6/6 tools/test_x509.py 11/11 make package completes, 4/4 variants result=OK Negative-tested in final form: flipping the onchip arm back to $0007 reproduces the original failure as a hard ld65 error with no PRG, now carrying the corrected message. Co-Authored-By: Claude Opus 5 (1M context) --- src/lib_contract_asserts.s | 86 +++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/src/lib_contract_asserts.s b/src/lib_contract_asserts.s index bd49e5f..f00636b 100644 --- a/src/lib_contract_asserts.s +++ b/src/lib_contract_asserts.s @@ -49,19 +49,29 @@ ; src/crypto/poly1305.s:21,24,213; mul_tables_init in ; src/crypto/shared/mul_tables.s:32; reu_mul_init in src/boot.s:31; ; mul_dma_lo/hi in src/data.s:123) — but the library's manifest still -; exports LIB_NISTCURVES_SHARED_PRIMITIVES = $0007, i.e. it claims to -; own all three. The disjointness assert would therefore FAIL, and -; correctly so: we resolve the double-ownership by *deleting archive -; members* (tools/integration/build_nistcurves_p256.sh drops -; mul_8x8.o and data_shared.o) rather than by the contract's -; `SHARED_*` deferral switches, so the shipped mask describes the -; upstream archive and not the one we link. The fix is to rebuild -; lib_manifest.o with those switches — the wrapper already does -; exactly this for zp_config.o — and is sequenced in c64-https#70. -; Until then the tripwire below pins the value we measured. -; -; §5 LIB_NISTCURVES_SHARED_CONSUMES (contract v0.5.0): absent at the -; v0.6.0 pin. Nothing to import. +; claims ownership of primitives we provide ($0007 on the REU archive, +; $0005 on the FP_ONCHIP_MUL one at the v0.9.1 pin). The disjointness +; assert would therefore FAIL, and correctly so: we resolve the +; double-ownership by *deleting archive members* +; (tools/integration/build_nistcurves_p256.sh drops mul_8x8.o and +; data_shared.o, and rebuilds mul_8x8_onchip.o under the SHARED_* +; switches) rather than by declaring the deferral in the manifest, so +; the shipped mask describes the upstream archive and not the one we +; link. After our surgery the linked archive owns NONE of the three. +; The clean fix is to rebuild the lib_manifest object with those +; switches — the wrapper already does exactly this for zp_config — +; and is sequenced in c64-https#70. It is deliberately not done here: +; it changes what the manifest reports for every consumer-side §5 +; check at once, which is not a change to make under release +; pressure. Until then the tripwire below pins the values we measured. +; +; §5 LIB_NISTCURVES_SHARED_CONSUMES (contract v0.5.0): present from the +; v0.9.0 pin ($0007 REU / $0005 onchip, mirroring PRIMITIVES). Not +; imported yet — the §8.0 coverage assert it enables +; (`CONSUMES & ~(APP_OWNED | LIB_OWNED) = 0`) is part of the same #70 +; work as the deferral switches above, and asserting on a mask that +; describes the pre-surgery archive would encode the same mismatch +; twice. .include "constants.inc" @@ -156,14 +166,50 @@ APP_OWNED = LIB_SHARED_PRIMITIVES_SQTAB | LIB_SHARED_PRIMITIVES_REU_MUL | LIB_SH ; See the header note: the contract's disjointness assert is not ; writable here yet. What IS checkable is that upstream's ownership ; claim has not moved out from under the archive surgery in -; tools/integration/build_nistcurves_p256.sh. Measured $0007 on both -; the REU and FP_ONCHIP_MUL variants at the v0.6.0 pin. -; -; If this fires, upstream changed which §8 primitives it owns. Re-derive -; which archive members the wrapper must drop (or which `SHARED_*` -; deferral switches it must pass) BEFORE updating the expected value. +; tools/integration/build_nistcurves_p256.sh. +; +; THE MASK IS PROFILE-DEPENDENT. This is the same exported symbol in +; both builds, but the REU and FP_ONCHIP_MUL archives ship different +; `lib_manifest*` objects, so its VALUE differs by profile and one +; expected constant cannot describe both. Measured with od65 on the +; staged archives: +; +; profile v0.6.0 v0.9.1 +; default (REU) $0007 $0007 +; FP_ONCHIP_MUL $0007 $0005 <- split +; FP_ONCHIP_MUL + comb $0007 $0005 <- split +; +; $0007 -> $0005 is the §8.2 `reu_mul` bit dropping out, upstream at +; **v0.8.0** (c64-nist-curves #78). It is correct behaviour, not drift: +; the onchip profile has no REU multiply path, so owning nothing there +; is the truthful claim, and the CHANGELOG records that a profile switch +; drops the bit from `SHARED_PRIMITIVES` and `SHARED_CONSUMES` alike. +; An earlier revision of this file pinned $0007 for both profiles — +; correct when measured at v0.6.0, and the reason every onchip build +; failed to link at the v0.9.1 pin. +; +; THE ARCHIVE SURGERY IS UNAFFECTED. Checked rather than assumed, since +; the wrong remedy here is expensive: the §8 primitive bodies each +; archive actually exports are the SAME SET at v0.6.0 and v0.9.1 — +; REU ships only `reu_mul_init`/`reu_mul_tables_init` (never pulled; +; src/boot.s:31 exports its own, so ld65 has no undefined symbol to +; resolve from the member), onchip ships `sqtab_lo`/`sqtab_hi` (absolute +; equates baked to LIB_SHARED_SQTAB_BASE=$BC00, not bodies) plus +; `reu_fetch_mul_row`, `og_common`, `og_src_ld`. Neither ships +; `sqtab_init`, `ct_mul_8x8`, `mul_8x8`, `poly_prod_*` or `smc_*`. +; So `build_nistcurves_p256.sh`'s member drops need no re-derivation +; for this change, and none was made. .import LIB_NISTCURVES_SHARED_PRIMITIVES -.assert LIB_NISTCURVES_SHARED_PRIMITIVES = APP_OWNED, lderror, "libs/nistcurves shared-primitive ownership claim moved — re-derive the archive member drops in tools/integration/build_nistcurves_p256.sh before touching this assert" + +.ifdef USE_NISTCURVES_ONCHIP + ; FP_ONCHIP_MUL, and the comb variant which sets the same -D. + NISTCURVES_EXPECTED_PRIMITIVES = LIB_SHARED_PRIMITIVES_SQTAB | LIB_SHARED_PRIMITIVES_CT_MUL_8X8 + .assert LIB_NISTCURVES_SHARED_PRIMITIVES = NISTCURVES_EXPECTED_PRIMITIVES, lderror, "libs/nistcurves FP_ONCHIP_MUL archive: SHARED_PRIMITIVES is not $0005 (sqtab|ct_mul_8x8). This mask is PROFILE-DEPENDENT - the onchip archive drops the reu_mul bit that the REU archive keeps, so read the onchip lib_manifest, not the REU one. If upstream moved it again, update the USE_NISTCURVES_ONCHIP arm in src/lib_contract_asserts.s; only re-derive the wrapper's member drops if the archive's exported primitive BODIES changed too." +.else + ; Default REU profile: all three. + NISTCURVES_EXPECTED_PRIMITIVES = LIB_SHARED_PRIMITIVES_SQTAB | LIB_SHARED_PRIMITIVES_REU_MUL | LIB_SHARED_PRIMITIVES_CT_MUL_8X8 + .assert LIB_NISTCURVES_SHARED_PRIMITIVES = NISTCURVES_EXPECTED_PRIMITIVES, lderror, "libs/nistcurves REU archive: SHARED_PRIMITIVES is not $0007 (sqtab|reu_mul|ct_mul_8x8). This mask is PROFILE-DEPENDENT - the onchip archive legitimately reports $0005, so check which profile this build selected. If upstream moved the REU value, update the .else arm in src/lib_contract_asserts.s; only re-derive the wrapper's member drops if the archive's exported primitive BODIES changed too." +.endif ; =====================================================================