Repository navigation
Description
Companion to #166, which records the c64-lib-contract §8.2 REU-settle gap for
libs/x25519 only. libs/nistcurves has the same gap, and unlike x25519
this code is linked into every PRG we ship. Nobody had written that down.
Assessment only — no bump proposed here. See "The blocker" for why the bump is
not a one-liner.
Revised 2026-09-05 after adversarial review. The first version of this
issue named byte pressure as the blocker and saiduci-comb"would link".
That was wrong. The actual gate is a duplicate-external collision that
stops all three products,uci-combincluded. Corrected below, reproduced
first-hand. The per-region byte split was also wrong in a way that made
uci-onchiplook worse than it is. Both are fixed here.
What we pin, and what exists upstream
| pinned | libs/nistcurves v0.11.2 (58f4cf53b28eb35c4872c93b9b6d799d031ff892) |
| latest upstream | v0.12.0, released 2026-08-30, tag dereferences to 63c9adc192e4ac30fc987ee9f0dd12b588e6ac7e |
| REU settle in the pinned tree | absent |
(ed7b7170 is one commit after the tag and touches only release notes. The
first version of this issue, and upstream c64-nist-curves#148, both call it
"v0.12.0". Harmless — the code is identical — but not literally true.)
Clone trap, worth recording: a local libs/nistcurves checkout did not have
v0.12.0 even after a forced tag fetch. Query GitHub
(gh release list --repo JC-000/c64-nist-curves) before concluding a pin is
current or that upstream has not fixed something. A freshly cloned submodule in
a new worktree does have it.
The defect
Identical to #166's, same root cause, same upstream ticket
(c64-lib-contract#144): on a turbo-clocked U64E at fw 3.15 the REU's
post-transfer restore outlasts the CPU, so the REU register write that follows
an execute is lost or misapplied — wrong multiply row, wrong field products,
silent wrong crypto with no error path.
v0.12.0 fences all thirteen sta reu_command sites: six hot fp_mul /
fp_sqr row fetches get an inline REU_DMA_CONFIRM (bit $DF00 / bvs), and
seven sites — the two reu_mul_init stashes, four comb stash/fetch
routines, and the exported reu_fetch_mul_row — get
jsr nistcurves_reu_dma_wait, a bounded spin plus
LIB_NISTCURVES_REU_SETTLE_ITER × 9 cycles.
Why this is a bigger surface than #166
libs/x25519 contributes zero bytes to every shipped PRG
(USE_X25519_SIBLING defaults to 0, no PACKAGE_VARIANTS row sets it, and it
links under neither backend). libs/nistcurves is in all three, measured from
build/c64-https.map for ip65-onchip at dc06095:
LIB_NISTCURVES_P256_CODE $7E2D-$9E9C 8,304 B
LIB_NISTCURVES_P256_RODATA $9E9D-$9F5C 192 B
LIB_NISTCURVES_MUL_CODE $9F5D-$9FD7 123 B
Exposure is not uniform across the three shipped products:
| product | REU DMA issued from nistcurves code | exposed |
|---|---|---|
c64-https-ip65-onchip |
none — LIB_NISTCURVES_REU_BANKS_USED = $0000 in the map; no turbo either |
no |
c64-https-uci-onchip |
none — same equate reads $0000; REU off |
no |
c64-https-uci-comb |
LIB_NISTCURVES_REU_BANKS_USED = $0004 (bank 2): boot precompute stash, plus sm256_reu_fetch_affine on every comb step |
yes |
Read the equate, not the profile name — that is the reliable discriminator, and
it is in every map we build.
The blocker: a duplicate external, not bytes
Reproduced here. libs/nistcurves checked out at 63c9adc (v0.12.0),
make clean before each, all three PACKAGE_VARIANTS configurations. All
three fail identically, at the link, with no PRG produced:
ld65: Error: Duplicate external identifier: 'nistcurves_mul_dma_hi'
make: *** [build/c64-https.prg] Error 1
BACKEND=ip65 USE_NISTCURVES_ONCHIP=1 rc=2
BACKEND=uci USE_NISTCURVES_ONCHIP=1 rc=2
BACKEND=uci USE_NISTCURVES_ONCHIP_COMB=1 rc=2
Mechanism, traced with od65 over the members our own wrapper stages:
-
At v0.12.0
mul_8x8_onchip.onewly importsnistcurves_reu_dma_timeout
andnistcurves_reu_wait_cnt— the §8.2 plumbing:$ od65 --dump-imports mul_8x8_onchip.o Name: "nistcurves_reu_dma_timeout" Name: "nistcurves_reu_wait_cnt" -
Those live only in
data_shared.o, which also exports the multiply-row
buffers:$ od65 --dump-exports data_shared.o Name: "nistcurves_mul_dma_hi" Name: "nistcurves_mul_dma_lo" Name: "nistcurves_reu_dma_timeout" Name: "nistcurves_reu_wait_cnt" -
At v0.11.2 ld65 never has a reason to pull
data_shared.oat all. At
v0.12.0 it must, and it drags those two exports in with it. -
src/crypto/shared/mul_tables.s:88exports the same pair, because they are
APP_OWNED here — we provide the multiply tables, per §8.3:.export nistcurves_mul_dma_lo, nistcurves_mul_dma_hi nistcurves_mul_dma_lo = mul_dma_lo nistcurves_mul_dma_hi = mul_dma_hi
So this is a c64-lib-contract §8.0 / §8.3 APP_OWNED ownership problem, not a
byte problem. It needs an upstream knob (a CONTRACT_DEFINES-style suppression
that lets the settle state live in data_shared.o without dragging the
APP_OWNED buffer exports along), or a wrapper change on our side. Bytes are the
second gate, not the first.
This matters for how the issue gets picked up. Anyone who reads "the
blocker is bytes", frees ~332 B by retiring the P-384 lane, and bumps the pin
will hit a link error this issue had told them they had already cleared.
The second gate: bytes, corrected
Per-segment deltas, measured through our own wrapper with our
CONTRACT_DEFINES and summed with od65 (not read off the changelog):
| segment | Δ onchip | Δ comb | notes |
|---|---|---|---|
LIB_NISTCURVES_P256_CODE |
+58 | +64 | |
LIB_NISTCURVES_MUL_CODE |
+39 | +39 | all of it nistcurves_reu_dma_wait |
LIB_NISTCURVES_*_BSS |
+3 | +3 | |
| total | +97 | +103 | vs the changelog's +100 / +106 |
The correction that matters: MUL_CODE is ~40% of the delta, and on both UCI
builds it lives in CRYPTO_OVERLAY, not CRYPTO_HOT. Re-derived against
free space measured from the three maps at dc06095:
| product | CRYPTO_HOT/RESIDENT free |
Δ landing there | CRYPTO_OVERLAY free |
Δ landing there | verdict |
|---|---|---|---|---|---|
ip65-onchip |
40 B ($9FD7) |
+97 (all three segments are in this region) | 22 B | 0 | short 57 B |
uci-onchip |
36 B ($9FDB) |
+58 | 2,035 B | +39 | short 22 B |
uci-comb |
193 B ($9F3E) |
+64 | 159 B | +39 | fits |
So on bytes alone it is still two-of-three, but uci-onchip is short by 22 B,
not the ~64 the first version implied. That is close enough that ordinary
churn could open or close it.
Note the onchip archives grow even though they issue no REU DMA — their share
is the profile-independent fp_mod_inv / ec_jacobian_to_affine guards. There
is no "REU-free variants pay nothing" escape.
(CLAUDE.md's standing "CRYPTO_HOT margin under UCI is 81 B" line matches
neither variant measured today — 36 B onchip, 193 B comb. Tracked in #193.)
Bounding the hazard honestly
uci-comb is structurally exposed: it is the one shipped product issuing
crypto-path REU DMA, none of it observes §8.2, and there is no error path.
But it is unobserved on our device. The §8.2 hardware row has since been
measured on our U64E (601A96) at core 1.4F — 22/22 cells, ~2100 fetches,
zero wrong bytes, including an unmitigated control that also passed. The
recorded failure was core 1.4E.
The discriminating oracle, for whoever picks this up: run the §8.2 probe on
a core 1.4E U64E, or a real 17xx REU, mitigated against unmitigated. Only
that separates "the settle fixed it" from "1.4F removed the hazard". A green
uci-comb run on our current device discriminates nothing and should not be
offered as evidence either way.
Not urgent. Should not stay unrecorded. The failure mode is silent wrong
crypto, which is the bad direction.
Also in v0.12.0, and not a defect for us
Two public entry points could lock the machine — fp_mod_inv[_384] on input
0 or on the modulus, and ec_jacobian_to_affine[_384] on Z = 0, which is
the library's own point-at-infinity encoding. Upstream states both are not
reachable through ecdsa_verify_256 / ecdsa_verify_384. Our import surface
is ecdsa_verify_256, ec_scalar_mul_var, ec_base_x, ec_gx256 — we never
call either guarded routine. Not a live defect here; recorded so nobody
re-derives it.
Additive things worth knowing when we do bump:
nistcurves_reu_dma_timeout(1 B, sticky) — upstream notes that consumers
whose linker config makes that BSSbss-typed must zero it before init.
boot.salready zeroes$A000-$BFFF; re-confirm against the map rather than
assume.LIB_NISTCURVES_REU_SETTLE_ITERdefaults to 8, and upstream says to raise it
if claiming conformance at 64 MHz. We run the C64U at 64 MHz, so that knob is
ours to set deliberately, not to inherit.LIB_NISTCURVES_ABI_VERSIONstays 2; no symbol removed or renamed.
c64-nist-curves#148 is NOT fixed by v0.12.0
The conditional ECDSA forgery filed upstream — the comb path accepts an
all-zero table slot as a point, u1*G collapses to infinity, and verify fails
open — is still OPEN, and no commit after the v0.12.0 tag touches it.
Bumping does not close it.
It stays unreachable on a healthy device (it needs a corrupt or never-written
comb table), but it lives in the same profile that carries the §8.2 exposure
above, so the two travel together.
Adjacent, tracked separately
Filed as #191: our own REU code has the same unfenced shape — 12
sta reu_command sites across src/boot.s, src/http.s, src/viewer.s and
src/crypto/shared/crypto_swap.s, none of which reads $DF00 bit 6 or
observes a settle. No upstream bump can fix that for us.
Suggested action
- Keep the pin at v0.11.2. Record the exposure (this issue).
- Resolve the
nistcurves_mul_dma_lo/hicollision first — it is the gate.
Upstream §8.0 APP_OWNED knob request, or a wrapper change here. - Only then worry about bytes:
ip65-onchipshort 57 B,uci-onchipshort
22 B. Candidates: retiring the P-384 lane (~332 B, per the 2026-08-30 note),
and the contiguous-region cfg restructure CLAUDE.md lists as a TODO. - On the bump: set
LIB_NISTCURVES_REU_SETTLE_ITERdeliberately for 64 MHz,
and confirmnistcurves_reu_dma_timeoutlands in zeroed BSS. - Chase upstream uci: capture the firmware's STATUS line instead of discarding it (#147) #148 independently of the pin.
Activity
JC-000 commentedon Sep 5, 2026
Cross-reference: the "adjacent, tracked separately" item — our own 12 unfenced
sta reu_commandsites insrc/— is #191. The x25519 half of the same clause is #166, closed by PR #189.[-]libs/nistcurves is pinned at v0.11.2, which predates the §8.2 REU settle — and unlike x25519 this code IS in every shipped PRG[/-][+]libs/nistcurves is pinned at v0.11.2, which predates the §8.2 REU settle — bumping is blocked by an APP_OWNED symbol collision[/+]JC-000 commentedon Sep 5, 2026
Filed upstream as JC-000/c64-nist-curves#149 — the
nistcurves_mul_dma_lo/hicollision, with theod65trace and two suggested fixes at that layer (split the §8.2 state into its own object, or an §8.0-shapedCONTRACT_DEFINESsuppression). That is the gate; bytes are second.JC-000 commentedon Sep 8, 2026
Closed by the v0.14.0 bump (#216). The pin now carries the SPEC §8.2 REU settle —
nistcurves_reu_dma_waitlinks at $5EE0 withLIB_NISTCURVES_REU_SETTLE_ITER = 8in the uci-comb map (LIB_NISTCURVES_REU_BANKS_USED = $0004, the one exposed product; ip65-onchip reads $0000, as predicted here).The APP_OWNED symbol collision that blocked the bump is gone: all four shipped-shape profiles link clean at master
02e5662, verified by rebuild rather than by inspection.The adjacent in-tree half — our own 12 REU execute sites having no §8.2 completion confirm or post-execute settle — stays open as #191.