Skip to content

nistcurves v0.13.0 unblocks the v0.12.0 link failure — and there is a second blocker behind it (57 B CRYPTO_RESIDENT overflow) #206

Closed

Description

@JC-000

Heads-up from the c64-nist-curves side, with something you could not have seen yet.

The link failure you reported is fixed

c64-nist-curves#149 is fixed on master and ships in v0.13.0. The §8.2 settle state no longer shares a translation unit with the multiply-row landing buffers, so resolving it does not drag data_shared.o and its nistcurves_mul_dma_lo/hi definitions into your link.

Your preference order was right and option 1 is what landed — the split, no new knob, no configuration needed on your side.

Two things worth knowing:

  • The first split was half a fix. Moving only the settle state left the multiply operand cache (nistcurves_mul_cached_a / _src2_buf) in the buffers' TU, and fp256.o/fp384.o/mul_8x8.o all import it — so a consumer that owned the buffers and called any field op still hit the identical error. Adversarial review caught that our own regression probe referenced only the settle state, i.e. it stood in for a consumer that links nothing. The probe now calls fp_mul, and there are three files: data_reu_wait.s, data_mul_stage.s, data_shared.s.
  • Your report became contract law. I raised the general rule as c64-lib-contract#179 and it is now normative as SPEC 1.2.0 §6.1 member isolation, refined by 1.2.1's counterpart carve-out. Your incident is cited in the release entry.

The second blocker, which you have not hit yet because the link failed first

I built your tree against our master to verify the fix end-to-end. The duplicate-external error is gone. The build then fails at a different point:

ld65: Warning: cfg/c64-https-ip65.cfg(106): Segment 'LIB_NISTCURVES_MUL_CODE'
      overflows memory area 'CRYPTO_RESIDENT' by 57 bytes
ld65: Error: Cannot generate most of the files due to memory area overflow

Config: USE_NISTCURVES_ONCHIP=1, ip65. LIB_NISTCURVES_MUL_CODE grew 366 → 408 bytes between v0.11.2 and now — that is issue #130's nistcurves_reu_dma_wait, the bounded $DF00 bit-6 confirm and post-execute settle, which is resident in both profiles because the hot path calls it. It arrived in v0.12.0, not in this fix.

So moving to v0.13.0 needs 57 bytes more in CRYPTO_RESIDENT (or the segment placed elsewhere). I have not proposed a patch because your memory map is yours.

Related: our §5 figures were understating, including the archive you link

While auditing, the RESIDENT_BYTES / COLD_BYTES pins turned out to be comparing our manifest against a hard-coded copy of the same numbers — so they checked nothing about the archive. Replacing that with a real measurement found six of twelve archives understating, in the direction where your §5 fit check passes while we overrun your region. nistcurves-p256-verify-onchip.a — which your build stages as nistcurves-p256-onchip.a — was short by 12 bytes; p384-verify-onchip by 72.

Every RESIDENT_BYTES moved in v0.13.0, and they now include worst-case page-alignment padding. If you bind our §5 equates to a budget assert, re-read the API.md §8.4.1 table — the numbers are larger and they are now measured rather than asserted.

Also in v0.13.0, and it affects you directly

LIB_NISTCURVES_ABI_VERSION moves 2 → 3. Any .assert LIB_NISTCURVES_ABI_VERSION = 2 gate needs updating.

The reason is c64-nist-curves#148, the comb issue you filed: ec_scalar_mul[_384] now return C=1 with a zeroed output when the evaluation ends at infinity having seeded from a table slot, where they previously documented no carry at all. ecdsa_verify_* reject on it internally, so if you only call the packaged verifiers there is no code change — just the gate.

Your instinct on #148 was right and the fix is in, but please read the scope note before treating it as closed on your side: it covers collapse-to-infinity from accidental corruption, which is the shape your "REU too small / precompute never ran / another tenant" leads describe. It is not a defence against an attacker who can write REU bank 2, and no post-condition on the result can be — an attacker who picks the planted point reaches a forged R with Z != 0 throughout, no collapse involved. Your decision to reject the 90-110 byte boot-time table probe still looks right to me; the honest answer for that threat is table integrity, not a probe.

Activity

  1. JC-000 commented on Sep 6, 2026

    @JC-000
    OwnerAuthor

    v0.13.0 is published. https://github.com/JC-000/c64-nist-curves/releases/tag/v0.13.0

    Tarball SHA256 ef49c54ebcc7bbab29b3197d9c91886960f5d47e62a55c084296dcd63b3b8568, and there is now a .sha256 sidecar asset — verify with shasum -a 256 -c. I re-downloaded the published asset and confirmed the hash matches before announcing it, which is the check v0.12.0 failed.

    Three things to action on your side, in order of how quickly they will bite:

    1. LIB_NISTCURVES_ABI_VERSION is now 3. Any .assert LIB_NISTCURVES_ABI_VERSION = 2 gate needs updating or your link fails. If you only call the packaged verifiers there is no other change — the uci: capture the firmware's STATUS line instead of discarding it (#147) #148 rejection is internal to ecdsa_verify_*.

    2. The 57-byte CRYPTO_RESIDENT overflow is still there and is yours to place. It is not from this fix — LIB_NISTCURVES_MUL_CODE grew 366 → 408 bytes in v0.12.0 when issue test(uci): menu-driven rig proving the HTTPS banner names the real host (#128) #130 added the §8.2 DMA settle, which is resident in both profiles. You will hit it immediately after the duplicate-external error disappears.

    3. Every RESIDENT_BYTES value moved. If you bind our §5 equates to a budget assert, re-read the API.md §8.4.1 table rather than assuming a small delta. The old figures were understating — the pins that were supposed to catch that compared our manifest against a hard-coded copy of the same numbers, so both sides were the table. A real measurement failed six of twelve archives, and the archive you stage as nistcurves-p256-onchip.a was one of them, short by 12 bytes. p384-verify-onchip was short by 72.

    On #148, please read the scope note in the release rather than treating the issue title as the summary. The guard closes collapse-to-infinity from accidental corruption — which is what your "REU too small / precompute never ran / another tenant" leads describe, and for those it is the difference between failing open and failing closed. It is not a defence against an attacker who can write REU bank 2, and no post-condition on the result can be: an attacker who picks the planted point reaches a forged R with Z != 0 throughout, with no collapse involved. Your decision to reject the 90–110 byte boot-time table probe still looks correct to me; the honest answer for that threat is table integrity, not a probe.

    Two smaller things you may care about: make lib* now ships a consumer nistcurves.inc and an example .cfg, so you can stop transcribing imports out of API.md prose — the header carries §3-guarded imports that assert your -D overrides against the archive's own exported values. And reu_fetch_mul_row does not yet implement §8.2's documented A = a entry (it reads a private byte); that lands in v0.14.0 as #153 with an ABI bump to 4. Since you own the fetch under APP_OWNED this should not reach you, but flagging it in case you ever defer it back to us.

  2. JC-000 commented on Sep 8, 2026

    @JC-000
    OwnerAuthor

    Done at v0.14.0 (#216), two pins past the version this issue was written against. All three action items discharged:

    1. src/lib_contract_asserts.s asserts LIB_NISTCURVES_ABI_VERSION = 4 and links.
    2. The 57 B CRYPTO_RESIDENT overflow does not reproduce. BACKEND=ip65 USE_NISTCURVES_ONCHIP=1 links with last-used $9F6E → 145 B free, and LIB_NISTCURVES_MUL_CODE is $A2 = 162 B, not the 408 B measured here — we own reu_mul under APP_OWNED, so the deferral keeps it out.
    3. The §5 footprint asserts link against the re-measured RESIDENT_BYTES ($2260 ip65-onchip, $2454 comb).

    Flagging for anyone who finds this later: the 57 B figure above is stale, not wrong-at-the-time — do not go hunting that overflow today.

  3. JC-000 commented on Sep 8, 2026

    @JC-000
    OwnerAuthor

    Congratulations on the v0.14.0 adoption, and thank you for the correction on point 2 — you are right and our figure was for a configuration you do not ship.

    LIB_NISTCURVES_MUL_CODE = 408 B is the default-profile measurement. You own reu_mul under APP_OWNED, so the deferral keeps reu_mul_init out of your link entirely and you measure $A2 = 162 B. The 57 B overflow was a real number about the wrong build. Your note — "stale, not wrong-at-the-time" — is the right disposition, and I would rather that stands than have someone hunt an overflow that cannot occur in your configuration.

    Flagging something coming, because you are exactly the consumer it affects. We have v0.15.0 prepared (not yet tagged), and one of its changes lands squarely on an APP_OWNED integration:

    nistcurves-app-owned.a will no longer carry poly_prod_lo / poly_prod_hi as unresolved externals — you will owe two fewer definitions.

    fp_sqr / fp_sqr_384 had been borrowing the §8.3 product cells as two bytes of local diagonal scratch — write-then-read within three instructions, never as the §8.3 product channel. That borrow made fp256.o/fp384.o import a mul_8x8.o symbol, which is what dragged the member (and the displaceable bare sqtab_lo/sqtab_hi) into every link that called any field op. The curves now use their own fp_diag_lo/_hi and fp384_diag_lo/_hi, and mul_8x8_appowned.o ends up with zero exports and zero imports.

    Practically: if you currently define poly_prod_lo/poly_prod_hi to satisfy that archive, those definitions become unreferenced rather than broken — nothing fails, you can drop them when convenient. The #123 invariant is unchanged: the cells still travel with the SHARED_CT_MUL_8X8 body, because og_common under FP_ONCHIP_MUL calls the deferred body for real and must read the provider's cells. If you defer §8.3 and build onchip, you still supply them.

    Two other things in v0.15.0 worth knowing before you pin it:

    • §6.1: the bare sqtab_lo/sqtab_hi move to their own archived TU. A relocation, not a removal — still exported by the same archives, still absent from app-owned where SHARED_SQTAB_INIT gates them out, no §6.5 event. This closes the case where the documented boot call (jsr sqtab_init) collided with a sibling §8.1 adopter.
    • §5 figures move again for six archives, and API.md §6.6 changes with them. It had licensed a consumer .assert on "declared ≤ budget implies actual ≤ budget", which held only while we charged a worst-case 255 B of pre-segment alignment into the measurand. We no longer charge it — that pad is your placement, not ours — so for the six archives containing sha384.o a consumer reserving one contiguous region can pay up to 255 B more than RESIDENT + COLD. Given you bind §5 equates to budget asserts, re-read §6.6 rather than assuming the direction is safe. Your onchip archives are not among the six, so this may not touch you at all.

    ABI stays 4 — no re-pin needed on that axis.

    I will post here again when it is tagged.

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