Skip to content

Retire ar65 member surgery in the nistcurves wrappers (SPEC §6.1) #119

Closed
JC-000/c64-lib-contract
#122

Description

@JC-000

What

tools/integration/build_nistcurves_p256.sh builds our P-256 archives by editing upstream's member set:

:274  rm -f "$STAGING/mul_8x8.o" "$STAGING/data_shared.o" "$STAGING/reu_mul_init.o"
:278  # onchip-comb: rm -f fp384_onchip.o mod384.o curve384.o points384_core.o
      #              points384_comb.o data_p384.o data_p384_limlee.o ecdsa384*.o
      #              sha384*.o data_sha.o inv256.o data_p256_invref.o
:337  rm -f "$STAGING/mul_8x8_onchip.o"

c64-lib-contract SPEC §6.1 now names this as forbidden:

Consumers fetch build/lib/<shortname>[-<variant>].a and link directly. No mid-build sed, no copying intermediates around, and no ar65 member surgery — an archive whose member set a consumer has edited is outside every §5/§8.0 manifest claim it ships. §6.2 and §6.3 exist so surgery is never the only route to a configuration.

This is the root cause of two gaps we already document as deferred:

  • src/contract_footprint_asserts.s excludes the comb profile from the §6.6 assert, because the surviving manifest describes the pre-surgery archive (27,000 B declared against a 16,384 B region).
  • src/lib_contract_asserts.s records the §8.0 disjointness/coverage asserts as "not writable today" for the same reason — the shipped mask describes upstream's archive, not the one we link.

What is now solvable, measured against the v0.10.2 pin

Upstream gained CONTRACT_DEFINES and APP_OWNED_DEFINES in the wave. The variant × APP_OWNED combination is reachable today — verified by building it:

make lib-p256-verify CONTRACT_DEFINES='-D SHARED_SQTAB_INIT -D SHARED_REU_MUL_INIT \
                                       -D SHARED_REU_MUL_FETCH -D SHARED_CT_MUL_8X8'

exit 0, and od65 --dump-exports on the resulting members shows the switches do most of our surgery for us:

member exports under the switches our current surgery
reu_mul_init.o nothing at all rm -f — unnecessary, the object is inert
mul_8x8.o poly_prod_lo/hi, sqtab_lo/hi only (no sqtab_init, no ct_mul_8x8, no mul_8x8) rm -f — nearly unnecessary
data_shared.o mul_dma_lo/hi, mul_cached_a, mul_src2_buf (+ prefixed) rm -f — still collides

So items 1 and 2 are close to free. Item 3 is not.

What is hard, and why I am not doing it under release pressure

Dropping the data_shared.o surgery means c64-https stops defining mul_dma_lo/hi, mul_cached_a, mul_src2_buf in src/data.s and imports the library's instead — which is what upstream's own lib-app-owned assumes, since it keeps data_shared.o in the member list. Those buffers are gated only by LIB_NO_BARE_EXPORTS (a naming switch), so there is no way to have the library ship the archive and not the buffers.

That is a re-pointing of the REU multiply row table, and it is the single worst place in this codebase to be wrong: CLAUDE.md's "VICE harness gotcha" records that when mul_dma_lo/hi holds the wrong contents, every fp_mul returns a*255*b mod p, the handshake fails minutes later at the first encrypted record, and there is no link error and no diagnostic. The same note records the near-miss where reu_mul_init would have bound to the wrong provider's table with no diagnostic either.

It is worth doing. It is not worth doing in the same change as a release.

What is blocked upstream regardless

There is no P-256 comb archive target. Upstream's full list is lib, lib-p256-verify, lib-p384-verify, lib-p384-sha384, lib-p384-curve, lib-app-owned, lib-onchip, lib-p256-verify-onchip, lib-p384-verify-onchip, lib-p384-curve-onchip. The comb objects exist only inside the full lib-onchip archive, which is why we narrow it by hand.

So even a perfect fix to items 1-3 leaves the comb profile non-conformant, and §6.3 ("every documented variant/profile axis MUST be reachable through §6.1 targets plus §6.2 defines, with no library edits") is not satisfiable for it from our side. Filed upstream as c64-nist-curves#TBD — link to follow.

Sequencing

  1. Upstream: a lib-p256-verify-onchip-comb (or equivalent) target. Blocks the comb arm entirely.
  2. Here: switch the REU and onchip arms to CONTRACT_DEFINES + APP_OWNED switches, flipping poly_prod_lo/hi and the data_shared buffer ownership to imports. Wants its own change, its own KAT run on both profiles, and an e2e handshake — a link that succeeds proves nothing about this failure mode.
  3. Then: delete the .ifdef USE_NISTCURVES_COMB exclusion in src/contract_footprint_asserts.s and write the §8.0 disjointness/coverage asserts that the surgery currently makes meaningless.

Related: #70 (the other open consumer-side alignment item, §13 net ABI).

Activity

  1. JC-000 commented on Aug 15, 2026

    @JC-000
    OwnerAuthor

    Upstream blocker cleared: c64-nist-curves v0.11.0 is released with the missing comb arm (JC-000/c64-nist-curves#117 → PR JC-000/c64-nist-curves#119).

    • make lib-p256-comb → nistcurves-p256-comb.a (DMA multiply profile)
    • make lib-p256-comb-onchip → nistcurves-p256-comb-onchip.a (the USE_NISTCURVES_ONCHIP_COMB=1 configuration)

    Each carries its own §6.4 manifest measured from the built objects — ZP_USAGE_BYTES 17, RESIDENT_BYTES 9216, COLD_BYTES 1050 (DMA) / 870 (onchip), REU_BANKS_USED $07/$04, masks $0007/$0007 and $0005/$0005 — so the comb profile can come back into the §6.6 assert with real figures, and the §8.0 disjointness asserts become writable. The lim_lee_comb_p256 precalc row is enumerated; the P-384 row is not (per-curve gates).

    Member set = the lib-p256-verify[-onchip] set with the comb-fast ecdsa256.o in place of ecdsa256_nocomb.o, plus points256_comb.o + data_p256_limlee.o — i.e. exactly what tools/integration/build_nistcurves_p256.sh's surgery was approximating, now conformant. Boot obligation adds ec_precompute_256; REU bank 2 carries the 16 KB anchor table in both profiles.

    Pin v0.11.0 (ABI stays 2; PRG byte-identical to v0.10.2, all pre-existing archives unchanged): https://github.com/JC-000/c64-nist-curves/releases/tag/v0.11.0

  2. JC-000 commented on Aug 15, 2026

    @JC-000
    OwnerAuthor

    Partial progress — the comb third is done

    libs/nistcurves v0.11.0 landed lib-p256-comb / lib-p256-comb-onchip (c64-nist-curves#117), so the comb-specific surgery is retired:

    • build_nistcurves_p256.sh now stages lib-p256-comb-onchip instead of the full lib-onchip, and step 4b — the rm -f of ~13 P-384/SHA-384/invref members — is gone.
    • The switch is PRG-neutral: the comb image is byte-identical (63c7ce868cdd…) built either way, so the archive change moved the manifest and nothing else.
    • Consequently the §6.6 comb exclusion in src/contract_footprint_asserts.s is deleted. The comb profile now links with the footprint assert active, alongside reu and onchip. That was the concrete payoff named when this issue was filed.
    • One wrapper fix was needed on the way: the ZP-override case block did not know zp_config_p256comb.o and failed loudly (unrecognised zp_config member) rather than silently dropping the override — the discovery machinery working as designed. Its gate is LIB_P256_COMB_ONLY, confirmed against zp_config.s:162 and Makefile:302 rather than inferred from the basename.

    Verified: comb builds on both backends, ECDSA KAT oracle 6/6 on the comb PRG (3 valid accepted, 3 invalid rejected), sqtab_reserved still at $BC00.

    What is still open — and it is the risky part

    Three kinds of member editing remain, all pre-existing:

    site what
    build_nistcurves_p256.sh:296 drops mul_8x8.o, data_shared.o, reu_mul_init.o — every profile
    :359 replaces mul_8x8_onchip.o with a locally-rebuilt glue TU
    ZP path replaces zp_config_*.o with a locally-rebuilt override

    For the first: two of the three are now easy. Building lib-p256-verify with CONTRACT_DEFINES='-D SHARED_SQTAB_INIT -D SHARED_REU_MUL_INIT -D SHARED_REU_MUL_FETCH -D SHARED_CT_MUL_8X8' leaves reu_mul_init.o exporting nothing and mul_8x8.o exporting only poly_prod_lo/hi + sqtab_lo/hi — so neither needs dropping.

    data_shared.o is the blocker and is unchanged by v0.11.0. It exports mul_dma_lo/hi, mul_cached_a, mul_src2_buf, gated only by LIB_NO_BARE_EXPORTS (a naming switch), and upstream's own lib-app-owned keeps the member — so the intended model is that the consumer imports those buffers rather than defining them in src/data.s. That is a re-pointing of the REU multiply row table, whose failure mode is a*255*b mod p with no link error and no diagnostic, surfacing minutes later at the first encrypted record.

    So this stays open deliberately. When it is picked up it wants its own change, an ECDSA KAT run on both profiles, and an e2e handshake — a successful link demonstrates nothing about this failure mode.

  3. JC-000 commented on Aug 15, 2026

    @JC-000
    OwnerAuthor

    Tested to a conclusion. Summary: half of this is done and proven, the other half is blocked upstream.

    The risk I originally described was wrong

    This issue said the blocker was flipping mul_dma_lo/hi ownership so c64-https imports the library's buffers, and called that the silent-wrong-crypto path. That framing was mistaken. SPEC §8.2 explicitly rules those four buffers adopter-private and rules the flip out by name:

    that was an overreach — those are adopter-private buffers … and deferring them would point a library's own field arithmetic at another library's memory

    The sanctioned route is the §6.5 rename track plus the LIB_NO_BARE_EXPORTS suppression gate. Measured at v0.11.0, building lib-p256-verify with the four APP_OWNED switches plus -D LIB_NO_BARE_EXPORTS=1:

    member exports needs dropping?
    reu_mul_init.o nothing no
    mul_8x8.o poly_prod_lo/hi only no
    data_shared.o nistcurves_mul_* prefixed only no

    All four symbols we consume (ec_base_x, ec_gx256, ec_scalar_mul_var, ecdsa_verify_256) survive the gate. No ownership flip, no touching the REU row table.

    What was proven end-to-end (REU + ip65)

    Built and linked with zero member drops:

    • wrapper requests the shape via CONTRACT_DEFINES instead of ar65 deletion
    • src/crypto/poly1305.s imports poly_prod_lo/hi unconditionally (it already did under on-chip; the pair now lives in LIB_NISTCURVES_MUL_CODE in every profile)
    • src/lib_contract_asserts.s switches to the prefixed LIB_NISTCURVES_ABI_VERSION — the bare name is suppressed by the gate, which is exactly the migration that file's own header anticipated

    And the payoff: the manifest now attests the deferral instead of us resolving it by deletion —

    LIB_NISTCURVES_SHARED_PRIMITIVES  $0000   library owns nothing
    LIB_NISTCURVES_SHARED_CONSUMES    $0007   it still reads all three
    

    — which makes the §8.0 disjointness and coverage asserts writable for the first time. Both were written and both link:

    .assert (APP_OWNED & LIB_NISTCURVES_SHARED_PRIMITIVES) = 0            ; disjointness
    .assert (LIB_NISTCURVES_SHARED_CONSUMES & ~(APP_OWNED | LIB_...)) = 0 ; coverage
    

    make BACKEND=uci and make BACKEND=ip65 both link clean with all of that active.

    What is blocked — and it is the half we ship

    The on-chip profiles cannot follow. Upstream's mul_8x8.s references ct_mul_8x8 and two smc_* labels as same-TU symbols inside og_common; SHARED_CT_MUL_8X8 gates out their definitions without adding imports, so the define does not assemble:

    make lib-p256-verify-onchip CONTRACT_DEFINES='-D SHARED_CT_MUL_8X8'
    src/mul_8x8.s(352): Error: Symbol 'ct_mul_8x8' is undefined
    

    Control with defaults: exit 0. Filed as JC-000/c64-nist-curves#123 (§6.3 reachability — the APP_OWNED × on-chip intersection is unreachable through §6.1 targets plus §6.2 defines).

    That matters disproportionately here because every image in our release lineup is an on-chip build (ip65-onchip, uci-onchip, uci-comb). So landing the REU-only half would change no shipped artifact, while splitting the tree into profiles that assert full deferral and profiles that still use surgery — and would need the §8.0 asserts made profile-conditional again, which is most of what makes them worth having.

    Decision

    Reverted, not landed. The release (PR #121) stands as verified; the working tree rebuilds to the same PRG hash it was verified at.

    Unblocking sequence:

    1. upstream c64-nist-curves#123 adds the imports under the gate
    2. re-apply the four changes above (they are small and now known-good on the REU arm)
    3. delete the profile split from the §8.0 section and let disjointness + coverage cover every profile
    4. KAT all three profiles — the poly_prod relocation is a real layout change, so a clean link proves nothing here
  4. JC-000 commented on Aug 15, 2026

    @JC-000
    OwnerAuthor

    Correction to the wording above — "every image is an on-chip build"

    That phrasing was sloppy, and misleading in exactly the way this project has been at pains to avoid elsewhere: uci-comb requires an REU (bank 2, LIB_NISTCURVES_REU_BANKS_USED = $04), and calling it "on-chip" without qualification implies the opposite. Two independent axes were conflated:

    • field layer — where multiply rows come from: CPU (FP_ONCHIP_MUL) or REU DMA
    • REU requirement — whether the image needs an REU at all

    uci-comb is on-chip on the first axis and REU-requiring on the second. The precise claim is that all three shipped images use the on-chip field layer, and that is what c64-nist-curves#123 blocks, because the broken define gates a TU (mul_8x8_onchip.s) that only the on-chip field layer compiles.

    And the distinction turns out to be load-bearing

    Separating the axes surfaces something the loose wording hid: a fully alignable comb configuration already exists. v0.11.0 ships lib-p256-comb — comb on the DMA field layer — and it builds clean under the full APP_OWNED gate:

    make lib-p256-comb CONTRACT_DEFINES='-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'
    exit 0
    

    Its member set carries mul_8x8.o, not mul_8x8_onchip.o, so it never reaches the defect.

    So the accurate statement of the constraint is not "we ship on-chip images and are therefore stuck". It is: the only comb arm that is conformance-clean today is the one whose performance we specifically rejected. The DMA field layer carries the REU floor — measured at D = 48.2 s for the REU verify profile on a U64E, and comb cannot remove it because comb reduces point operations while the floor is DMA-anchored. That puts a DMA-comb verify at ≥48 s at 48 MHz against the 16.402 s we measured for on-chip comb, i.e. roughly 3x worse at exactly the clocks the product targets.

    Trading the flagship's headline number for a conformance property is not a trade worth making, so the conclusion stands. But it stands because the conformant arm is too slow, not because no conformant arm exists — and that is a materially different thing to record for whoever picks this up after #123.

  5. JC-000 commented on Aug 15, 2026

    @JC-000
    OwnerAuthor

    Second upstream blocker cleared: c64-nist-curves v0.11.1 fixes JC-000/c64-nist-curves#123 — -D SHARED_CT_MUL_8X8 now assembles against the on-chip TU (the deferring arm imports the five-symbol §8.3 provider surface: ct_mul_8x8, smc_sum_a_imm/smc_diff_a_imm, and poly_prod_lo/hi), so the three onchip images can retire the glue-TU workaround and the member surgery together. Note the fifth-and-sixth symbols: your §8.3 provider must export poly_prod_lo/hi (the glue that .includes pristine mul_8x8.s already does). Pin v0.11.1: https://github.com/JC-000/c64-nist-curves/releases/tag/v0.11.1 (PRG byte-identical to v0.11.0; ABI stays 2; also carries the #121 precompute-cost correction).

  6. added a commit that references this issue on Aug 16, 2026
    39a43eb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions