You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Companion to #166, which records the c64-lib-contract §8.2 REU-settle gap for libs/x25519only. 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 said uci-comb "would link". That was wrong. The actual gate is a duplicate-external collision that
stops all three products, uci-comb included. Corrected below, reproduced
first-hand. The per-region byte split was also wrong in a way that made uci-onchip look worse than it is. Both are fixed here.
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 thirteensta 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.
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:
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 BSS bss-typed must zero it before init. boot.s already zeroes $A000-$BFFF; re-confirm against the map rather than
assume.
LIB_NISTCURVES_REU_SETTLE_ITER defaults 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_VERSION stays 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/hi collision first — it is the gate.
Upstream §8.0 APP_OWNED knob request, or a wrapper change here.
Only then worry about bytes: ip65-onchip short 57 B, uci-onchip short
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_ITER deliberately for 64 MHz,
and confirm nistcurves_reu_dma_timeout lands in zeroed BSS.
Companion to #166, which records the c64-lib-contract §8.2 REU-settle gap for
libs/x25519only.libs/nistcurveshas the same gap, and unlike x25519this 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.
What we pin, and what exists upstream
libs/nistcurvesv0.11.2 (58f4cf53b28eb35c4872c93b9b6d799d031ff892)63c9adc192e4ac30fc987ee9f0dd12b588e6ac7e(
ed7b7170is one commit after the tag and touches only release notes. Thefirst 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/nistcurvescheckout did not havev0.12.0 even after a forced tag fetch. Query GitHub
(
gh release list --repo JC-000/c64-nist-curves) before concluding a pin iscurrent 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_commandsites: six hotfp_mul/fp_sqrrow fetches get an inlineREU_DMA_CONFIRM(bit $DF00/bvs), andseven sites — the two
reu_mul_initstashes, four comb stash/fetchroutines, and the exported
reu_fetch_mul_row— getjsr nistcurves_reu_dma_wait, a bounded spin plusLIB_NISTCURVES_REU_SETTLE_ITER× 9 cycles.Why this is a bigger surface than #166
libs/x25519contributes zero bytes to every shipped PRG(
USE_X25519_SIBLINGdefaults to 0, noPACKAGE_VARIANTSrow sets it, and itlinks under neither backend).
libs/nistcurvesis in all three, measured frombuild/c64-https.mapforip65-onchipatdc06095:Exposure is not uniform across the three shipped products:
c64-https-ip65-onchipLIB_NISTCURVES_REU_BANKS_USED = $0000in the map; no turbo eitherc64-https-uci-onchip$0000; REU offc64-https-uci-combLIB_NISTCURVES_REU_BANKS_USED = $0004(bank 2): boot precompute stash, plussm256_reu_fetch_affineon every comb stepRead 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/nistcurveschecked out at63c9adc(v0.12.0),make cleanbefore each, all threePACKAGE_VARIANTSconfigurations. Allthree fail identically, at the link, with no PRG produced:
Mechanism, traced with
od65over the members our own wrapper stages:At v0.12.0
mul_8x8_onchip.onewly importsnistcurves_reu_dma_timeoutand
nistcurves_reu_wait_cnt— the §8.2 plumbing:Those live only in
data_shared.o, which also exports the multiply-rowbuffers:
At v0.11.2 ld65 never has a reason to pull
data_shared.oat all. Atv0.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 areAPP_OWNED here — we provide the multiply tables, per §8.3:
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 suppressionthat lets the settle state live in
data_shared.owithout dragging theAPP_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_DEFINESand summed withod65(not read off the changelog):LIB_NISTCURVES_P256_CODELIB_NISTCURVES_MUL_CODEnistcurves_reu_dma_waitLIB_NISTCURVES_*_BSSThe correction that matters:
MUL_CODEis ~40% of the delta, and on both UCIbuilds it lives in
CRYPTO_OVERLAY, notCRYPTO_HOT. Re-derived againstfree space measured from the three maps at
dc06095:CRYPTO_HOT/RESIDENTfreeCRYPTO_OVERLAYfreeip65-onchip$9FD7)uci-onchip$9FDB)uci-comb$9F3E)So on bytes alone it is still two-of-three, but
uci-onchipis 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_affineguards. Thereis no "REU-free variants pay nothing" escape.
(CLAUDE.md's standing "
CRYPTO_HOTmargin under UCI is 81 B" line matchesneither variant measured today — 36 B onchip, 193 B comb. Tracked in #193.)
Bounding the hazard honestly
uci-combis structurally exposed: it is the one shipped product issuingcrypto-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-combrun on our current device discriminates nothing and should not beoffered 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 input0or on the modulus, andec_jacobian_to_affine[_384]onZ = 0, which isthe library's own point-at-infinity encoding. Upstream states both are not
reachable through
ecdsa_verify_256/ecdsa_verify_384. Our import surfaceis
ecdsa_verify_256,ec_scalar_mul_var,ec_base_x,ec_gx256— we nevercall 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 consumerswhose linker config makes that BSS
bss-typed must zero it before init.boot.salready zeroes$A000-$BFFF; re-confirm against the map rather thanassume.
LIB_NISTCURVES_REU_SETTLE_ITERdefaults to 8, and upstream says to raise itif 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*Gcollapses to infinity, and verify failsopen — 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_commandsites acrosssrc/boot.s,src/http.s,src/viewer.sandsrc/crypto/shared/crypto_swap.s, none of which reads$DF00bit 6 orobserves a settle. No upstream bump can fix that for us.
Suggested action
nistcurves_mul_dma_lo/hicollision first — it is the gate.Upstream §8.0 APP_OWNED knob request, or a wrapper change here.
ip65-onchipshort 57 B,uci-onchipshort22 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.
LIB_NISTCURVES_REU_SETTLE_ITERdeliberately for 64 MHz,and confirm
nistcurves_reu_dma_timeoutlands in zeroed BSS.