Skip to content

Preflight the REU on the UCI test path (closes #97) - #107

Merged
JC-000 merged 1 commit into
masterfrom
feat/reu-preflight
Aug 14, 2026
Merged

Preflight the REU on the UCI test path (closes #97)#107
JC-000 merged 1 commit into
masterfrom
feat/reu-preflight

Conversation

@JC-000

@JC-000 JC-000 commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Closes #97.

The problem, restated

The default make BACKEND=uci image is the REU profile: src/crypto/fe25519.s and the libs/nistcurves P-256 archive both fetch multiply rows from REU banks by DMA. With no REU that DMA silently does nothing — no fault, rows keep reu_mul_init's residue, every multiply returns a wrong-but-deterministic result. X25519 derives the wrong shared secret, the first encrypted record fails its AEAD tag, and tls13.s spins 65,536 net_poll calls at ~40 ms each: ~44 minutes on a screen frozen at KEYS ENC1 RX.

The reporter on #97 lost a working day to this, including a cold power cycle we wrongly sent them after by matching the screen to the unrelated device-wedge signature. Their close-out asked for the README to say the REU is required, and noted that a check at script start would be useful.

What this does

The check is the fix; the README paragraph is secondary. A prerequisite a program can verify should not be left as prose.

New tools/uci/_reu_preflight.py. Called under the DeviceLock the script already holds, after enable_uci, before the reset:

  • Onchip builds are let through with no REST call at all. A blanket check would break the exact configuration we recommend to REU-less users. The profile is read from build/labels.txt, which ld65 emits from the same link as the PRG (every guarded script loads both from build/, so they always correspond).
  • REU-profile builds read C64 and Cartridge Settings / RAM Expansion Unit. Enabled: one line, carry on. Disabled: exit 4 immediately, naming both remedies — enable the REU, or make BACKEND=uci USE_NISTCURVES_ONCHIP=1, which needs none.
  • An inconclusive or failed probe warns and continues. Blocking on a REST hiccup would be a new failure mode.
  • C64_SKIP_REU_PREFLIGHT=1 bypasses.

Detection, and why it is a union

Onchip markers are checked as a union, not a conjunction:

marker REU build onchip build
LIB_NISTCURVES_REU_BANKS_USED 3 0
gen_mul_row absent present
fe_gen_mul_row absent present
sqtab_reserved absent present

Verified against both builds in this worktree (REU PRG 66e37037… / labels c4ef5eab…; onchip PRG 518ee446… / labels f6a30919…). The union matters: the failure mode to avoid is misclassifying an onchip build as REU-profile and blocking it, so no single upstream rename can do that. LIB_NISTCURVES_REU_BANKS_USED is the semantically exact one — the library declaring its own REU claim through the c64-lib-contract manifest. Absence of every marker fails closed to "REU profile", which is the cheap direction to be wrong in: one skippable error message versus 44 minutes of silence.

Scope

Guarded (they exercise a REU-profile crypto path): test_https_local.py — which test_https_print_body.py and test_https_local_p384.py both delegate to — plus test_https_bad_finished.py and bench_ecdsa_u64e.py. The bench matters for a different reason: REU-less fp_mul returns a*255*b mod p, so every vector "verifies" as a reject with no diagnostic. A silently wrong benchmark is worse than a slow one.

Not guarded, deliberately: boot_check.py, phase2_check.py, phase3_tcp_echo.py, test_http_local.py, test_http_live.py. None touches the REU, and bolting it on for symmetry would just add a REST call and a failure mode to scripts that work fine without one.

It does not auto-enable the REU

#97 offered that as an alternative and it is the wrong trade. The U64E is queue-shared across the c64-* projects, REST config writes persist until the next power cycle, and a test that silently reconfigures someone else's hardware converts a legible error into a mystery two runs later on a different branch. A clear refusal costs seconds; a silent reconfiguration costs trust. So the message tells you the menu path and leaves the write to you.

Acceptance — measured on hardware

U64E at 10.43.23.81, 48 MHz, DeviceLock throughout, RAM Expansion Unit restored to its baseline Enabled and confirmed by readback. All three via tools/uci/test_https_local.py:

case build device REU result wall
A REU Disabled exit 4, actionable message 2.08 s
B REU Enabled exit 0 PASS, HTTP 200, body match 128.42 s
C onchip Disabled exit 0 PASS, HTTP 200, body match 90.99 s

Case C is the one most likely to regress and it passes unblocked, with the preflight reporting build is the on-chip profile (LIB_NISTCURVES_REU_BANKS_USED=0) — no REU required, skipping device check.

Preflight cost measured in isolation, n=3 each:

reu-profile preflight 0.080 / 0.081 / 0.073 s   (2 REST GETs)
onchip      preflight 0.002 / 0.001 / 0.001 s   (zero REST calls)

0.08 s against a 128 s run is not latency worth noticing, and the onchip path adds nothing measurable.

Case A's message:

REU PREFLIGHT FAILED — this build needs an REU and the device has none enabled.

  device: C64 and Cartridge Settings / RAM Expansion Unit = 'Disabled'
  build : REU profile (LIB_NISTCURVES_REU_BANKS_USED=3)
  ...
  1. Enable the REU on the device:
       Settings -> C64 and Cartridge Settings -> RAM Expansion Unit -> Enabled
  2. Or build the on-chip profile, which needs no REU at all:
       make clean && make BACKEND=uci USE_NISTCURVES_ONCHIP=1

One note for the record: the first case-B attempt after re-enabling the REU came back net_last_error=0x88 UCI_ERR_NO_SOCKET — the known post-config-write bridge glitch, not a result. The re-run passed, per the standing rule.

Docs

README's "Ultimate 64 Elite Hardware Tests (UCI backend)" section now states the requirement and that the preflight enforces it. CLAUDE.md gets the detection rationale and a pointer from the KEYS ENC1 RX discriminator block added in #106 — if a guarded script reached RX, the REU cause is already excluded.

🤖 Generated with Claude Code

A REU-profile PRG on a device with `RAM Expansion Unit: Disabled` does
not fail — it computes a wrong X25519 shared secret from DMA that
silently no-ops, then spins 65,536 net_polls at ~40 ms looking for a
record it can decrypt. That is ~44 minutes on a frozen-looking screen
ending `KEYS ENC1 RX`, and it cost an outside contributor a working day
(#97) plus a cold power cycle we wrongly sent them after.

The prerequisite was documented nowhere on the UCI test path, and it is
one a program can check in one REST call. So check it, under the
DeviceLock the script already holds, before committing to the run.

- tools/uci/_reu_preflight.py: detect the crypto profile from
  build/labels.txt, and for REU-profile builds read the device setting.
  Onchip builds are let through with no REST call at all — a blanket
  check would break the very configuration we recommend as the fix.
- Onchip markers are a UNION (LIB_NISTCURVES_REU_BANKS_USED == 0,
  gen_mul_row, fe_gen_mul_row, sqtab_reserved), verified against both
  builds, so an upstream rename cannot silently reclassify an onchip
  build and block it. Absence of all markers fails closed.
- Wired into test_https_local.py (which test_https_print_body.py and
  test_https_local_p384.py delegate to), test_https_bad_finished.py and
  bench_ecdsa_u64e.py. Exit code 4. Scripts that never touch the REU
  (boot_check, phase2, phase3_tcp_echo, http_local, http_live) are
  deliberately unguarded.
- It never enables the REU for you. #97 offered that; the U64E is
  queue-shared and config writes persist until power cycle, so a silent
  reconfiguration trades a legible error for a mystery two runs later on
  someone else's branch. C64_SKIP_REU_PREFLIGHT=1 bypasses.

Measured on the U64E at 48 MHz, test_https_local.py:

  build   device REU  result                      wall
  REU     Disabled    exit 4, both remedies named  2.08 s
  REU     Enabled     exit 0 PASS, HTTP 200      128.42 s
  onchip  Disabled    exit 0 PASS, HTTP 200       90.99 s

Preflight cost in isolation (n=3): REU path 0.073-0.081 s, onchip path
0.001-0.002 s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

write_memory errors when running test_https_local.py

1 participant