From 9dfe1783a85b47231cdb9688365e387b75b86d3d Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Sat, 15 Aug 2026 08:13:57 -0500 Subject: [PATCH 1/2] =?UTF-8?q?chore(contract):=20align=20to=20c64-lib-con?= =?UTF-8?q?tract=20v0.10.0=20=E2=80=94=20=C2=A76.6=20asserts=20+=20pin-ada?= =?UTF-8?q?ptive=20ZP=20spelling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contract SPEC.md on `main` moved v0.8.0 -> v0.10.0 (eleven versions). Two of the new clauses bind a consumer and were unimplemented here; one upcoming upstream change would have broken the build silently at the next pin move. §6.6 consumer footprint asserts (new, contract v0.10.0) ------------------------------------------------------- New zero-byte TU `src/contract_footprint_asserts.s` imports LIB_NISTCURVES_RESIDENT_BYTES + _COLD_BYTES and asserts them against the ld65-published region size. No cfg change was needed: every MEMORY area in both cfgs already carries `define = yes`. Stated honestly in the file: against a $4000 region this carries ~7 KB of slack while the region's real free space is 81 B (__CRYPTO_HOT_LAST__ = $9FAF), so it is NOT a tight region-pressure gate. What it is is a §6.4 per-variant-manifest regression gate, and that failure is live in this tree today. Measured with od65 on the staged archives at the v0.9.1 pin: profile manifest member RESIDENT COLD reu lib_manifest_p256verify.o 8700 430 onchip lib_manifest_p256verify_onchip.o 8700 240 onchip-comb lib_manifest_onchip.o 27000 1650 <- whole library The comb profile is excluded, with the reason recorded: it builds from upstream's FULL lib-onchip archive and our wrapper then rm -f's ~7 members, so upstream's manifest survives describing the pre-surgery archive. That is exactly the harm §6.1 names ("an archive whose member set a consumer has edited is outside every §5/§8.0 manifest claim it ships"). Comb is outside `make package`, so no shipped artifact is affected. Verified both directions: PRG byte-identical with and without the TU (66e37037… UCI) so it costs nothing, and forced on for comb it fires with the intended ld65 message against the real 27000 B manifest. Supporting Makefile change: -D BACKEND_UCI=1 / -D BACKEND_IP65=1, because the crypto code region is CRYPTO_HOT under UCI and CRYPTO_RESIDENT under ip65, so the ___SIZE__ symbol to import differs by backend. Pin-adaptive ZP slot spelling (both nistcurves wrappers) -------------------------------------------------------- libs/nistcurves master (v0.10.1) has completed the contract §2 ZP-registry migration using §6.5's loud-break alias shape: zp_ptr2 = nistcurves_zp_ptr2 ; unguarded, NO .ifndef so the wrappers' hardcoded bare spelling is a hard assemble failure the moment the pin moves. Measured against master's zp_config.s: -D 'zp_ptr2=$3d' -> zp_config.s(56): Error: Symbol 'zp_ptr2' is already defined -D 'nistcurves_zp_ptr2=$3d' -> assembles; od65 shows BOTH nistcurves_zp_ptr2 AND zp_ptr2 = $3D The canonical spelling would conversely be a silent no-op at the pinned v0.9.1, leaving the slot at upstream's $fd. Both wrappers now probe libs/nistcurves/src/zp_config.s for the canonical name and follow it, so they are correct at both pins and the wave bump does not need to touch them. fp_mul_i/fp_mul_j need no probe — `fp_` is a registered §2 prefix, so those keep their .ifndef guards across the migration. check_zp_slot now runs against both spellings, which additionally proves the alias tracks the override rather than splitting one slot across two addresses — the outcome §6.5 explicitly forbids. Also: fold LaneFollowups' tools/uci/test_*.py -> rig_*.py rename into the two references in docs/library-ingestion-architecture.md. Verification ------------ make clean && make PASS 47,105 B make clean && make BACKEND=uci PASS 62,977 B + USE_NISTCURVES_ONCHIP=1, both backends PASS + USE_NISTCURVES_ONCHIP_COMB=1 (uci) PASS tools/test_ecdsa_kat_oracle.py PASS 6/6 tools/test_x509.py PASS 11/11 ip65 blob rebuild 6,951 B, cf1a5ff7… (documented hash) Co-Authored-By: Claude Opus 5 (1M context) --- Makefile | 13 +++ docs/library-ingestion-architecture.md | 4 +- src/contract_footprint_asserts.s | 119 +++++++++++++++++++++ tools/integration/build_nistcurves_p256.sh | 64 +++++++---- tools/integration/build_nistcurves_p384.sh | 14 ++- 5 files changed, 192 insertions(+), 22 deletions(-) create mode 100644 src/contract_footprint_asserts.s diff --git a/Makefile b/Makefile index 1e72760..b04ea76 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,19 @@ IP65_BUILD := ip65-build IP65_BIN := $(IP65_BUILD)/ip65-c64.bin CA65FLAGS := -I src -I src/inc -I src/crypto/shared -I src/net/$(BACKEND) -I build --debug-info +# Which backend is selected, as a ca65 define. Sources that must name a +# cfg-level symbol need this, because the two cfgs give the same region +# different names: the crypto code+rodata region is CRYPTO_HOT under UCI and +# CRYPTO_RESIDENT under ip65, so the ld65-published ___SIZE__ symbol an +# `.import` has to spell differs by backend (src/contract_footprint_asserts.s +# is the current consumer). The `-I src/net/$(BACKEND)` path above cannot +# serve this: it carries net tuning, and region naming is a cfg property, not +# a networking one. +ifeq ($(BACKEND),uci) +CA65FLAGS += -D BACKEND_UCI=1 +else +CA65FLAGS += -D BACKEND_IP65=1 +endif # Optional HTTPS target-port override (src/boot.s defaults to 443). # `make HTTPS_PORT=4433` lets a test rig's TLS listener bind unprivileged. ifdef HTTPS_PORT diff --git a/docs/library-ingestion-architecture.md b/docs/library-ingestion-architecture.md index 2405544..7c789dd 100644 --- a/docs/library-ingestion-architecture.md +++ b/docs/library-ingestion-architecture.md @@ -351,7 +351,7 @@ Suitable to file as GitHub issues against `JC-000/c64-https`. Each is sized for **Scope:** Split `cfg/c64-https-uci.cfg`'s `CRYPTO_RESIDENT` ($6000-$BFFF, 24 KB) into `CRYPTO_HOT` ($6000-$9FFF, 16 KB) and `CRYPTO_COLD_SHADOW` ($A000-$BFFF, 8 KB, under-BASIC-ROM RAM). Move ChaCha20-Poly1305 + SHA-256 + HKDF + TLS state machine + HTTP + record I/O into CRYPTO_HOT; move ECDSA-verify dispatcher, X25519 ECDH glue, per-curve scratch BSS into CRYPTO_COLD_SHADOW. CRYPTO_OVERLAY ($4200-$5FFF, 7.5 KB) absorbs all cold-path *code* via REU-paged overlays. -**Expected output:** `build/c64-https.prg` builds clean under `BACKEND=uci`. Smoke tests (`test_https_local.py`, `test_https_local_p384.py`) PASS. `build/labels.txt` shows CRYPTO_HOT under-allocated by ≥1 KB. +**Expected output:** `build/c64-https.prg` builds clean under `BACKEND=uci`. Smoke tests (`rig_https_local.py`, `rig_https_local_p384.py`) PASS. `build/labels.txt` shows CRYPTO_HOT under-allocated by ≥1 KB. **Dependencies:** None; this work is c64-https-internal. @@ -442,7 +442,7 @@ After all four cells build clean: **Tier 2 — U64E (slow, gated, ~10-90 minutes):** - Runs on self-hosted runner with LAN access to the U64E (probably `192.168.1.81` per the project memory). - Acquires DeviceLock via `c64-test-harness` (queue-aware). If queue depth exceeds threshold (W7), skip with `:warning: queue busy, skipping U64E tier; will retry in 6h`. -- `tools/uci/test_https_local.py`, `test_https_local_p384.py`, `phase3_tcp_echo.py`, `boot_check.py`. +- `tools/uci/rig_https_local.py`, `rig_https_local_p384.py`, `phase3_tcp_echo.py`, `boot_check.py`. - Wall-clock budgets per CLAUDE.md: ~82 s P-256 handshake, ~7 min P-384 handshake. ### 5.4 U64E hardware contention diff --git a/src/contract_footprint_asserts.s b/src/contract_footprint_asserts.s new file mode 100644 index 0000000..eb6d818 --- /dev/null +++ b/src/contract_footprint_asserts.s @@ -0,0 +1,119 @@ +; src/contract_footprint_asserts.s — c64-lib-contract SPEC §6.6 consumer +; footprint asserts. +; +; Emits no bytes. Companion to src/lib_contract_asserts.s, kept as a +; separate TU because §6.6 landed at contract v0.10.0 and its gating is +; profile-dependent in a way the §1/§3/§8.0/§13.3 gates are not (see the +; comb exclusion below). +; +; Contract: https://github.com/JC-000/c64-lib-contract — read SPEC.md on +; `main`, NOT the newest git tag (tags lag; main is v0.10.0 as of +; 2026-08-15). Clause referenced here: §6.6 (consumer footprint asserts). +; +; --------------------------------------------------------------------- +; WHAT §6.6 IS FOR +; --------------------------------------------------------------------- +; SPEC §6.6 exists because of a failure measured in THIS repo and filed +; upstream as c64-lib-contract#69: a MINOR library bump grows resident +; code or rodata, the consumer's region overflows at link time, and there +; is no advance signal that the bump was a spatial event. c64-https hit it +; twice — most recently v0.7.0's +208 B validation gate against CRYPTO_HOT's +; ~1 byte of slack, bisected across three tags to find. +; +; The clause's consumer pattern binds the library's declared footprint to +; a region budget: +; +; .import LIB_NISTCURVES_RESIDENT_BYTES +; .import LIB_NISTCURVES_COLD_BYTES +; .import __MAIN_SIZE__ ; cfg: MAIN: ... define = yes; +; .assert LIB_NISTCURVES_RESIDENT_BYTES + LIB_NISTCURVES_COLD_BYTES \ +; <= __MAIN_SIZE__, lderror, "..." +; +; No cfg change was needed to adopt it: every MEMORY area in both +; cfg/c64-https-uci.cfg and cfg/c64-https-ip65.cfg already carries +; `define = yes`, so ld65 publishes __CRYPTO_HOT_SIZE__ / +; __CRYPTO_RESIDENT_SIZE__ already (verified in build/labels.txt). +; +; --------------------------------------------------------------------- +; WHAT THIS ASSERT ACTUALLY CATCHES — AND WHAT IT DOES NOT +; --------------------------------------------------------------------- +; Stated plainly, because a gate whose reach is overestimated is worse +; than no gate. +; +; It does NOT tightly bound region pressure. CRYPTO_HOT is $4000 (16,384 B) +; and holds c64-https's own crypto as well as the library, so at the v0.9.1 +; pin the assert compares 8,700 + 430 = 9,130 against 16,384 — roughly +; 7 KB of slack, while the region's REAL free space is 81 bytes +; (__CRYPTO_HOT_LAST__ = $9FAF against a $A000 end, measured). A bump that +; grows the archive by 100 B still overflows the region without tripping +; this assert. Making it tight would require hardcoding a per-pin budget +; constant, which then needs maintenance on every bump and fires as a +; false alarm on any growth of c64-https's own code; that trade was +; considered and declined. +; +; What it DOES catch is a §6.4 per-variant-manifest regression, and that +; is not hypothetical — it is measured live in this tree on a sibling +; profile. §6.6's whole implication ("declared <= budget implies actual <= +; budget") rests on the manifest describing the archive we link. When it +; does not, the number is not conservative, it is meaningless. At the +; v0.9.1 pin, measured with od65 on the staged archives: +; +; profile manifest member RESIDENT COLD +; reu lib_manifest_p256verify.o 8700 430 +; onchip lib_manifest_p256verify_onchip.o 8700 240 +; onchip-comb lib_manifest_onchip.o 27000 1650 <- whole library +; +; 27,000 B is the whole-library figure against a 16,384 B region. That is +; the exact number that made §6.6 unadoptable before contract v0.9.0's +; §6.4, and it is still live for the comb profile — so if a future bump +; regresses `reu` or `onchip` the same way, this assert names it instead of +; leaving an opaque segment overflow to be bisected. +; +; --------------------------------------------------------------------- +; WHY THE COMB PROFILE IS EXCLUDED +; --------------------------------------------------------------------- +; The comb archive would FAIL this assert today (27,000 + 1,650 = 28,650 +; against 16,384) and the failure would be a false alarm: the comb PRG +; links and runs, so the declared number is wrong, not the build. +; +; The cause is ours, not upstream's. `tools/integration/build_nistcurves_p256.sh` +; builds the comb profile from upstream's FULL `lib-onchip` archive and +; then `rm -f`s ~7 members (fp384, mod384, curve384, points384_*, ...) to +; narrow it to the P-256 comb set. Upstream's `lib_manifest_onchip.o` +; legitimately describes the archive upstream shipped; it survives our +; member surgery still describing the pre-surgery set. The `reu` and +; `onchip` profiles are unaffected because they build from upstream's +; already-minimal `lib-p256-verify` / `lib-p256-verify-onchip` targets, +; which carry per-variant manifests. +; +; This is precisely the harm SPEC §6.1 names: "an archive whose member set +; a consumer has edited is outside every §5/§8.0 manifest claim it ships." +; The sanctioned remedy is §6.2 `CONTRACT_DEFINES` / `CONTRACT_ZP_DEFINES` +; plus §6.3's `lib-app-owned` target, so the configuration is reachable +; without surgery. Neither exists in the pinned v0.9.1; both ARE implemented +; on libs/nistcurves master (v0.10.1, measured). Retiring the surgery is +; therefore unblocked by the wave bump and is sequenced in c64-https#70 — +; when it lands, delete the .ifdef below and the comb profile is covered +; by the same assert as the other two. +; +; The comb profile is deliberately excluded from `make package`, so no +; shipped artifact is affected by the gap this exclusion leaves open. + +.ifndef USE_NISTCURVES_COMB + +.import LIB_NISTCURVES_RESIDENT_BYTES +.import LIB_NISTCURVES_COLD_BYTES + +; The code+rodata region differs by backend: CRYPTO_HOT under UCI, +; CRYPTO_RESIDENT under ip65. Both are $6000-$9FFF ($4000 bytes) and both +; are declared `define = yes`, so the published size symbol is the only +; thing that differs. BACKEND_UCI / BACKEND_IP65 come from the Makefile. +.ifdef BACKEND_UCI + .import __CRYPTO_HOT_SIZE__ + .assert LIB_NISTCURVES_RESIDENT_BYTES + LIB_NISTCURVES_COLD_BYTES <= __CRYPTO_HOT_SIZE__, lderror, "libs/nistcurves declared footprint (LIB_NISTCURVES_RESIDENT_BYTES + _COLD_BYTES) exceeds CRYPTO_HOT. Contract SPEC 6.6. Most likely cause is a 6.4 regression: the archive ships a whole-library manifest instead of a per-variant one (27000 B is the whole-library value). Check with od65 --dump-exports on the staged lib_manifest*.o before assuming the library really grew." +.else + .import __CRYPTO_RESIDENT_SIZE__ + .assert LIB_NISTCURVES_RESIDENT_BYTES + LIB_NISTCURVES_COLD_BYTES <= __CRYPTO_RESIDENT_SIZE__, lderror, "libs/nistcurves declared footprint (LIB_NISTCURVES_RESIDENT_BYTES + _COLD_BYTES) exceeds CRYPTO_RESIDENT. Contract SPEC 6.6. Most likely cause is a 6.4 regression: the archive ships a whole-library manifest instead of a per-variant one (27000 B is the whole-library value). Check with od65 --dump-exports on the staged lib_manifest*.o before assuming the library really grew." +.endif + +.endif ; USE_NISTCURVES_COMB diff --git a/tools/integration/build_nistcurves_p256.sh b/tools/integration/build_nistcurves_p256.sh index 1ddcebc..61e59d5 100755 --- a/tools/integration/build_nistcurves_p256.sh +++ b/tools/integration/build_nistcurves_p256.sh @@ -95,24 +95,49 @@ AR65="${AR65:-ar65}" # unused inside ZP_CRYPTO. # Other slots match upstream defaults — see libs/nistcurves/src/zp_config.s. # -# THE POINTER SLOT IS SPELLED `nistcurves_zp_ptr2` FROM v0.10.0, AND THE -# OLD SPELLING NOW HARD-ERRORS. The §6.5 rename window (upstream #107) -# made the four general-purpose scratch slots canonically -# `nistcurves_zp_{tmp1,tmp2,ptr1,ptr2}` and left the bare `zp_*` names as -# unconditional same-address aliases: +# THE POINTER SLOT IS SPELLED `nistcurves_zp_ptr2` FROM v0.10.0. The +# spelling is PROBED, never hardcoded, because both spellings are wrong at +# some pin this script has to build. +# +# c64-lib-contract SPEC §2 gained a ZP prefix registry at v0.9.0: a bare +# `zp_ptr2` is unregistered (three adopters had independently converged on +# bare zp_tmp1/zp_ptr1 — contract #83), so c64-nist-curves renamed its four +# general-scratch slots to the registered `nistcurves_zp_*` family and left +# the bare names as aliases for the §6.5 rename window, in the "loud-break" +# shape that clause ratifies: # # .ifndef nistcurves_zp_ptr2 # nistcurves_zp_ptr2 = $fd # .endif # zp_ptr2 = nistcurves_zp_ptr2 <- NOT .ifndef-guarded # -# so `-D zp_ptr2=$3d` no longer suppresses a guarded definition; it -# collides with the alias assignment and stops the build with -# `zp_config.s(56): Error: Symbol 'zp_ptr2' is already defined`. That is -# the good case — a loud failure, not a silently-dropped override. The -# `fp_*` names are documented override knobs and were not renamed. +# Measured, ca65 V2.18, against each pin's own zp_config.s: +# +# spelling v0.9.1 v0.10.1 +# -D 'zp_ptr2=$3d' overrides correctly Error: Symbol +# 'zp_ptr2' is +# already defined +# -D 'nistcurves_zp_ptr2=$3d' defines an unused overrides both +# symbol; real slot names to $3D +# stays at $fd +# +# Neither spelling is safe across both, so probe the library source and +# follow it. Note the v0.9.1 + canonical cell is a *wrong value*, not a +# silent one: the `check_zp_slot` guards below read the emitted object with +# od65, so that combination stops the build (as `$fd` != `$3d`, or as +# `` if the guard is aimed at the canonical name). The probe's value +# is being loud AND correct at both pins, rather than merely loud at one. +# +# fp_mul_i / fp_mul_j need no probe: `fp_` is a registered §2 prefix for +# c64-nist-curves, so those names are canonical already and keep their +# `.ifndef` guards across the migration (verified on v0.10.1). +if grep -qE '^[[:space:]]*\.ifndef[[:space:]]+nistcurves_zp_ptr2[[:space:]]*$' "$LIB_SRC/zp_config.s"; then + ZP_PTR2_SLOT='nistcurves_zp_ptr2' +else + ZP_PTR2_SLOT='zp_ptr2' +fi ZP_OVERRIDES=( - '-D' 'nistcurves_zp_ptr2=$3d' + '-D' "$ZP_PTR2_SLOT=\$3d" '-D' 'fp_mul_i=$39' '-D' 'fp_mul_j=$3a' ) @@ -189,14 +214,15 @@ check_zp_slot() { exit 1 fi } -# Check the CANONICAL name. The bare `zp_ptr2` alias is also exported in a -# default build and carries the same value, but it disappears under -# `-D LIB_NO_BARE_EXPORTS=1` (SPEC §6.5) and is removed outright at the -# next MAJOR — checking it would make this guard silently vacuous exactly -# when a consumer tightens the build. -check_zp_slot nistcurves_zp_ptr2 61 # $3d -check_zp_slot fp_mul_i 57 # $39 -check_zp_slot fp_mul_j 58 # $3a +# Both spellings are checked. On the pinned v0.9.1 these are the same symbol +# and the second call is a no-op restatement; on a §2-migrated pin they are +# the canonical slot and its deprecated alias, and checking both proves the +# alias tracks the override rather than splitting one slot across two +# addresses — the silent outcome contract §6.5 forbids. +check_zp_slot "$ZP_PTR2_SLOT" 61 # $3d — canonical spelling for this pin +check_zp_slot zp_ptr2 61 # $3d +check_zp_slot fp_mul_i 57 # $39 +check_zp_slot fp_mul_j 58 # $3a # --- 4. Drop conflicting members / rebuild the onchip mul object --- # `reu_mul_init.o` is the SPEC §8.2 `reu_mul` provider. c64-https is the diff --git a/tools/integration/build_nistcurves_p384.sh b/tools/integration/build_nistcurves_p384.sh index 6a9eed4..a2a9d1d 100755 --- a/tools/integration/build_nistcurves_p384.sh +++ b/tools/integration/build_nistcurves_p384.sh @@ -54,8 +54,20 @@ CA65="${CA65:-ca65}" AR65="${AR65:-ar65}" # --- ZP-slot overrides (c64-https canonical map + SHA-384 isolated window) --- +# zp_ptr2's SPELLING is pin-dependent and probed, never hardcoded — see the +# long rationale in build_nistcurves_p256.sh (contract SPEC §2 ZP prefix +# registry + §6.5's loud-break alias shape: on a migrated pin the bare +# spelling is an unguarded alias and `-D zp_ptr2=...` dies with +# "Symbol 'zp_ptr2' is already defined"). `fp_`/`sha_` are registered §2 +# prefixes for c64-nist-curves and keep their `.ifndef` guards, so only this +# one slot needs the probe. +if grep -qE '^[[:space:]]*\.ifndef[[:space:]]+nistcurves_zp_ptr2[[:space:]]*$' "$LIB_SRC/zp_config.s"; then + ZP_PTR2_SLOT='nistcurves_zp_ptr2' +else + ZP_PTR2_SLOT='zp_ptr2' +fi ZP_OVERRIDES=( - '-D' 'zp_ptr2=$3d' + '-D' "$ZP_PTR2_SLOT=\$3d" '-D' 'fp_mul_i=$39' '-D' 'fp_mul_j=$3a' # SHA-384 streaming pointer slots (moved out of $04-$0b defaults From 6ff25839a8d2c3c885cf68a7a6192b651ef86532 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Sat, 15 Aug 2026 08:23:53 -0500 Subject: [PATCH 2/2] fix(integration): point the ZP post-check at the canonical slot name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review from the wave-bump lane: the deprecated bare `zp_ptr2` alias disappears under `-D LIB_NO_BARE_EXPORTS=1` (contract §1/§6.5), so a guard aimed at it stops guarding exactly when a consumer tightens the build. Their framing was "goes vacuous"; measured, it is worse than that in this script — `check_zp_slot` treats an absent symbol as `got=""`, which is a mismatch, so the bare check would have *failed the build* over a symbol the contract expects to be gone. Split accordingly: - canonical `$ZP_PTR2_SLOT` is checked HARD and unconditionally — it is the name the library's own code reads, so absence or disagreement is always a defect; - the bare alias is checked only when present AND when it is a distinct symbol, which still catches a split alias (one slot at two addresses, the outcome §6.5 forbids) without failing on its legitimate removal. Verified against objects built from libs/nistcurves master's zp_config.s in three shapes: A. migrated pin, alias emitted -> canonical 61, alias 61 -> proceed B. migrated pin + LIB_NO_BARE_EXPORTS -> canonical 61, alias absent -> proceed C. alias forced to $fd (split) -> canonical 61, alias 253 -> STOP The p384 wrapper gets the same canonical-first ordering. Every check there stays presence-tolerant because its sha and curve trees export disjoint slot subsets (the sha tree has no zp_ptr2 at all), so absence is legitimate per-tree and cannot be asserted away. Co-Authored-By: Claude Opus 5 (1M context) --- tools/integration/build_nistcurves_p256.sh | 35 ++++++++++++++++++---- tools/integration/build_nistcurves_p384.sh | 10 ++++++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/tools/integration/build_nistcurves_p256.sh b/tools/integration/build_nistcurves_p256.sh index 61e59d5..d349cf8 100755 --- a/tools/integration/build_nistcurves_p256.sh +++ b/tools/integration/build_nistcurves_p256.sh @@ -214,13 +214,36 @@ check_zp_slot() { exit 1 fi } -# Both spellings are checked. On the pinned v0.9.1 these are the same symbol -# and the second call is a no-op restatement; on a §2-migrated pin they are -# the canonical slot and its deprecated alias, and checking both proves the -# alias tracks the override rather than splitting one slot across two -# addresses — the silent outcome contract §6.5 forbids. +# Same as check_zp_slot, but tolerates the symbol being absent. Only correct +# for a slot whose ABSENCE is legitimate — see the alias check below. +check_zp_slot_if_present() { + local name="$1" want="$2" got + got=$("${OD65:-od65}" --dump-exports "$STAGING/$ZP_MEMBER" \ + | awk -v n="\"$name\"" '$1=="Name:" && $2==n {f=1; next} f && $1=="Value:" {gsub(/[()]/,"",$3); print $3; exit}') + [ -z "$got" ] && return 0 + if [ "$got" != "$want" ]; then + echo "ERROR: $ZP_MEMBER exports $name = $got, expected $want (c64-https ZP override did not take)" >&2 + exit 1 + fi +} + +# The CANONICAL spelling is checked unconditionally — it is the name the +# library's own code reads, so its absence or disagreement is always a defect. check_zp_slot "$ZP_PTR2_SLOT" 61 # $3d — canonical spelling for this pin -check_zp_slot zp_ptr2 61 # $3d + +# The deprecated bare alias is checked only WHEN PRESENT, and only when it is +# a distinct symbol. On a §2-migrated pin this proves the alias tracks the +# override rather than splitting one slot across two addresses (the silent +# outcome contract §6.5 forbids) — but the alias legitimately disappears under +# `-D LIB_NO_BARE_EXPORTS=1` (§1), and a hard check would then fail the build +# over a symbol the contract expects to be gone. Checking the bare name +# *instead* of the canonical one would be the worse error in the other +# direction: it is the spelling that goes away, so the guard would stop +# guarding exactly when a consumer tightens the build. +if [ "$ZP_PTR2_SLOT" != "zp_ptr2" ]; then + check_zp_slot_if_present zp_ptr2 61 # $3d — deprecated alias, if still emitted +fi + check_zp_slot fp_mul_i 57 # $39 check_zp_slot fp_mul_j 58 # $3a diff --git a/tools/integration/build_nistcurves_p384.sh b/tools/integration/build_nistcurves_p384.sh index a2a9d1d..5b562b6 100755 --- a/tools/integration/build_nistcurves_p384.sh +++ b/tools/integration/build_nistcurves_p384.sh @@ -180,7 +180,15 @@ for tree in sha curve; do "${ZP_OVERRIDES[@]}" \ -o "$STAGING/$tree/$zp_member" \ "$LIB_SRC/zp_config.s" - check_zp_slot_if_present "$STAGING/$tree/$zp_member" zp_ptr2 61 # $3d + # Canonical spelling first (see build_nistcurves_p256.sh). Every check here + # is presence-tolerant because the sha and curve trees export disjoint slot + # subsets — the sha tree has no zp_ptr2 at all — so absence is legitimate + # per-tree and cannot be asserted away. The bare alias is additionally + # optional because `-D LIB_NO_BARE_EXPORTS=1` removes it (§1/§6.5). + check_zp_slot_if_present "$STAGING/$tree/$zp_member" "$ZP_PTR2_SLOT" 61 # $3d + if [ "$ZP_PTR2_SLOT" != "zp_ptr2" ]; then + check_zp_slot_if_present "$STAGING/$tree/$zp_member" zp_ptr2 61 # $3d — deprecated alias + fi check_zp_slot_if_present "$STAGING/$tree/$zp_member" fp_mul_i 57 # $39 check_zp_slot_if_present "$STAGING/$tree/$zp_member" fp_mul_j 58 # $3a done