Skip to content

feat(crypto): c64-x25519 v0.4.0 optional sibling integration (Phase C.5) - #41

Merged
JC-000 merged 2 commits into
masterfrom
feat/x25519-v0.4.0-phase-c5
May 15, 2026
Merged

feat(crypto): c64-x25519 v0.4.0 optional sibling integration (Phase C.5)#41
JC-000 merged 2 commits into
masterfrom
feat/x25519-v0.4.0-phase-c5

Conversation

@JC-000

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

Copy link
Copy Markdown
Owner

Summary

Adds libs/x25519@v0.4.0 as an optional sibling library and a USE_X25519_SIBLING=1 Makefile flag. Default is OFF — the shipped PRG is byte-identical to master at be57811. Flag is opt-in pending a supervisor decision to flip the default.

Closes the two prior Phase C.1 / v0.3.0 rollback dead-ends. Root cause was upstream c64-x25519#33 — REU register residue from c64-https's do_swap clobbering $DF04/$DF0A before the sibling's first fe25519_mul. Upstream PR #36 + v0.4.0 H2 add defensive REU register init at every public fe25519_* / x25519_scalarmult entry. PR #36's adversarial reu_full_dirty harness explicitly simulates our do_swap residue and passes 4/4 on real U64E.

Second commit (added 2026-05-14): MemoryPolicy adoption. Replaces the defensive ROUTINE_ADDR=$5100 hack in tools/uci/test_https_local.py (and 4 sibling scripts) with MemoryArbiter-driven allocation backed by a c64-https-aware MemoryPolicy. Builds on the just-merged c64-test-harness PR #95 (transport-layer write guard). The arbiter reads build/labels.txt for segment boundaries — same scratch addresses derived dynamically per build config (BACKEND × USE_X25519_SIBLING), no more hardcoded constants for future layouts to collide with.

Validation

  • Default build (flag OFF): PRG md5 byte-identical to baseline, both BACKEND=ip65 and BACKEND=uci. tools/test_x509.py 11/11, tools/test_entropy.py 7/7. U64E HTTPS e2e green at 48 MHz (86.7 s).
  • Sibling build (USE_X25519_SIBLING=1BACKEND=uci):
    • U64E tools/uci/test_https_local.py at TURBO_MHZ=48: PASS, handshake 98.3 s (post-MemoryPolicy commit; was 101.4 s pre-adopt — variance within run-to-run noise).
    • U64E tools/uci/test_https_local.py at TURBO_MHZ=1: PASS, handshake 1280.5 s (~21 min stock-CPU).
    • Body parsed correctly in all cases: HELLO FROM TLS SERVER.
  • Sibling build BACKEND=ip65: overflows CRYPTO_RESIDENT by ~1 KB — documented limitation, separate cfg-restructure follow-up.

Investigation highlights

Two consecutive bugs in the prototype were caught before this lands:

  1. Build-script:x25_basepoint + fe_p initially routed to X25519_BSS (type = bss). ld65 silently drops init bytes from bss segments; both constants were zero at runtime. Symptom: every fe25519 modular reduction saw fe_p=0, every x25519_base saw basepoint=0. Fix: route to X25519_RODATA (type = ro).
  2. Test harness:tools/uci/test_https_local.py had a hardcoded ROUTINE_ADDR = 0x4200 that DMA-injected a 117-byte 6502 trampoline directly on top of the new X25519_RODATA segment. Silently clobbered x25_basepoint, fe_p, mul38_*_tab, sqr_* — every fe25519_mul/sqr then read garbage tables, X25519 emitted wrong-but-deterministic output, surfaced 2 layers downstream as tls_state=$FF AEAD decrypt fail. 12-hour bisection. This PR's second commit replaces the defensive $5100 relocation with proper MemoryArbiter-driven allocation, so future layout changes can't recur this class of bug silently.

The harness-level fix is now upstream in c64-test-harness#93 / #95 (merged). c64-https adopts that API in commit 2 here.

Test plan

  • Default build flag-OFF: master byte-identical
  • Default build regression: 11/11 test_x509, 7/7 test_entropy, U64E HTTPS PASS (86.7 s)
  • Sibling build USE_X25519_SIBLING=1 BACKEND=uci: builds clean, zero ld65 warnings
  • U64E sibling HTTPS at TURBO_MHZ=48: PASS (98.3 s post-policy adoption)
  • U64E sibling HTTPS at TURBO_MHZ=1: PASS (1280.5 s)
  • MemoryPolicy adoption: 5 tools/uci/ scripts migrated, 28 hardcoded addresses replaced, arbiter-derived at runtime
  • Decision: flip default to USE_X25519_SIBLING=1 (out of scope for this PR)
  • ip65 cfg restructure to fit sibling (out of scope, follow-up)
  • Promote arbiter allocations to safe_regions (silences 6 of 8 expected UserWarnings; follow-up)
  • Declare KERNAL kbd-buf state ($00C0-$00CF, $0277-$0280) as safe — probably belongs in c64-test-harness's keyboard module (follow-up)
  • Tighten unknown_policy=WARNDENY once the above two land (follow-up)
  • tools/test_x25519.py shim/rewrite to validate sibling primitives directly under VICE (follow-up)

Commits

  1. feat(crypto): c64-x25519 v0.4.0 optional sibling integration (Phase C.5) — submodule, build script, Makefile flag, cfg, src wiring, CLAUDE.md, defensive $5100 ROUTINE_ADDR relocation.
  2. chore(tests): adopt MemoryPolicy + MemoryArbiter for tools/uci/ scratch — replaces commit 1's defensive $5100 hack with proper transport-layer policy-driven allocation; sweeps 5 scripts (28 hardcoded addresses → 0).

🤖 Generated with Claude Code

JC-000and others added 2 commits May 14, 2026 09:36
Adds `libs/x25519` submodule at v0.4.0 and a `USE_X25519_SIBLING=1`
Makefile flag that links against the sibling X25519 implementation
instead of the in-tree `src/crypto/{fe25519,x25519}.s`. Default is
OFF; nothing changes for `make` / `make BACKEND=uci` and the shipped
PRG is byte-identical to the pre-branch baseline. Flag is opt-in
pending the supervisor's decision to flip the default.
## Why now
Two prior integration attempts (Phase C.1 commit 6c9d2a3 against
v0.2.0-era source, and a v0.3.0 retry) rolled back due to wrong-
result symptoms under BACKEND=uci at 48 MHz. Trace analysis later
showed the symptom was NOT a Montgomery-ladder hang but a wrong-
output cascade caused by REU register residue ($DF04, $DF0A) left
by c64-https's `do_swap` dispatcher before the sibling's first
`fe25519_mul`. Upstream issue c64-x25519#33 closed this with PR #36
(defensive REU register init at `x25519_scalarmult` entry), extended
in v0.4.0 H2 to every `fe25519_{mul,sqr,mul_a24,inv}` entry point.
PR #36's adversarial harness explicitly simulates c64-https `do_swap`
residue and passes 4/4 on real U64E hardware. The v0.4.0 release is
strictly safer than v0.3.0 for our composition.
## What's in this commit
- `libs/x25519@v0.4.0` submodule (commit 47c0ad2)
- `Makefile`: `USE_X25519_SIBLING ?= 0` flag. When `=1`, drops
`src/crypto/fe25519.o` + `src/crypto/x25519.o` from the link line
and links `build/lib/x25519.a` instead.
- `tools/integration/build_x25519.sh`: stages sibling .s files,
emits two custom data files (BSS for zero-init buffers, RODATA
for x25_basepoint + fe_p + mul_by_38 / sqr / a24 LUTs), assembles
to `build/lib/x25519.a`. Critical detail: x25_basepoint + fe_p
MUST live in a `type = ro` segment, not `type = bss` — ld65 drops
init bytes from bss and the constants will be zero at runtime.
- `cfg/c64-https-uci.cfg`, `cfg/c64-https-ip65.cfg`: new
`X25519_RODATA` (type = ro) and `X25519_BSS` (type = bss)
segments routing to `CRYPTO_OVERLAY`. ip65 cfg has the segments
declared but the backend overflows `CRYPTO_RESIDENT` by ~1 KB
under the flag — UCI is the only supported flagged path until
a separate cfg restructure lands.
- `src/boot.s`: `.ifdef USE_X25519_SIBLING` guards. Under the flag,
in-tree `reu_mul_init` / `reu_fetch_mul_row` / `reu_init_a/b`
state are dropped; sibling's `x25519_init.s` provides them.
In-tree `sqtab_init` + `mul_8x8` (from src/crypto/poly1305.s) are
retained — calling-convention compatible with the sibling.
- `src/data.s`: in-tree `fe_*` / `x25_*` buffers dropped under the
flag (sibling exports its own from `data_x25519_bss_raw.s`).
- `src/crypto/shared/reu_layout.inc`: comment note about overlap
between REU_P{256,384}_PRECOMPUTE_BASE (banks 3-5, reserved-
but-unused) and the sibling's per-row multiply tables.
- `tools/uci/test_https_local.py`: **load-bearing fix**. The
hardcoded `ROUTINE_ADDR = 0x4200` (+ 5 sibling scratch addresses
through $4542) DMA-injected a 6502 trampoline + harness data
directly on top of the new X25519_RODATA segment ($4200-$4AFF
under the flag), silently clobbering x25_basepoint, fe_p, and
the multiply / square lookup tables. Every fe25519_mul/sqr then
read garbage and X25519 emitted wrong-but-deterministic output;
the symptom surfaced 2 layers downstream as `tls_state=$FF` /
AEAD decrypt fail and cost a 12-hour bisection. Relocated the
trampoline + scratch to $5100-$5442 (past the X25519_BSS tail at
$50FF, still inside CRYPTO_OVERLAY). Comment block at the
declaration explains the constraint for future maintainers. An
upstream issue (JC-000/c64-test-harness#93) + draft PR #94
propose a `MemoryArbiter` API to surface this class of collision
at harness level rather than via cryptographic mystery.
- `CLAUDE.md`: updated public-symbol section, Known issues entry,
and CRYPTO_OVERLAY collision lesson.
## Validation
- Default builds (USE_X25519_SIBLING=0): PRG md5 byte-identical to
be57811 baseline, both BACKEND=ip65 and BACKEND=uci. Zero impact
on the shipped binary.
- Default-build regression: `tools/test_x509.py` 11/11 PASS,
`tools/test_entropy.py` 7/7 PASS, U64E HTTPS e2e PASS at 48 MHz.
- Sibling build (USE_X25519_SIBLING=1) BACKEND=uci:
- U64E HTTPS e2e at TURBO_MHZ=48: PASS, http_status=200, body =
"HELLO FROM TLS SERVER", handshake wall-clock 101.4 s (vs the
87 s in-tree baseline; +14 s is consistent with v0.4.0's
documented +27 % scalarmult cost from the L1-L29 CT closures
and PR #36 defensive REU init).
- U64E HTTPS e2e at TURBO_MHZ=1: PASS, body = "HELLO FROM TLS
SERVER", handshake wall-clock 1280.5 s (~21 min) — stock-CPU
baseline.
- Sibling build BACKEND=ip65: overflows CRYPTO_RESIDENT by ~1 KB —
documented blocker, follow-up cfg work.
## Known follow-ups (not in this commit)
- Flip the default to `USE_X25519_SIBLING=1` after a reviewing pass
on the cleanliness of the build script + a second e2e validation
pass.
- Restructure ip65 cfg to make room for the sibling (~1 KB short).
- Sweep `tools/uci/` for other `ROUTINE_ADDR=$4200`-class addresses
(`bench_ecdsa_u64e.py:80` spotted; doesn't exercise X25519 today
so unblocked, should be moved before any default flip).
- `tools/test_x25519.py` references in-tree `fe_*` label names; a
shim or rewrite is needed before that test can validate the
sibling implementation directly. Today the U64E HTTPS e2e is the
load-bearing validation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces hardcoded scratch DMA addresses across five tools/uci/ scripts
with arbiter-driven allocation backed by a c64-https-aware MemoryPolicy
factory. Builds on c64-test-harness PR #95 (released 2026-05-14) which
adds a transport-level write guard: every `transport.write_memory()`
call now passes through `policy.check_write()` before any byte crosses
the wire, raising `MemoryPolicyError` on a collision.
## Why this exists
The Phase C.5 X25519 integration in this branch surfaced a silent
memory-corruption class: `tools/uci/test_https_local.py` had
`ROUTINE_ADDR = 0x4200` hardcoded; under `USE_X25519_SIBLING=1` that
sat inside `X25519_RODATA`, every DMA-inject quietly clobbered
`x25_basepoint` + `fe_p` + multiply/sqr LUTs, X25519 emitted wrong-
but-deterministic output, and the failure surfaced two layers down
the TLS state machine as `tls_state=$FF`. The PR #41 prior commit
relocated the constants to `$5100-$5442` as a defensive hack, but
the antipattern (hardcoded harness scratch in a moving consumer
memory layout) is the actual class of bug. The new MemoryPolicy /
MemoryArbiter API addresses it at the source.
## What changed
New `tools/uci/_memory_policy.py` (295 LOC) — shared factory:
- `build_policy_and_arbiter(labels_path, prg_path)` parses ld65's
`__<SEGMENT>_START__` / `__<SEGMENT>_LAST__` / `__<SEGMENT>_SIZE__`
markers from `build/labels.txt` to derive a MemoryPolicy reflecting
the *current* build's memory map (BACKEND ip65/uci, with/without
USE_X25519_SIBLING). Reserves the *declared* area of each segment
(conservative — runtime tails are protected against silent growth).
- Returns a paired MemoryArbiter ready to `.alloc(size, name=...)`
inside the CRYPTO_OVERLAY tail (or wherever the layout has slack).
- `unknown_policy = WARN` — stray writes outside declared regions
surface as UserWarning, not failure. Tighten to DENY in a follow-up
once the existing call sites (notably `c64_test_harness.keyboard`
KERNAL kbd-buf writes) are declared safe.
Five tools/uci/ scripts migrated (28 hardcoded addresses replaced):
- `test_https_local.py` (6 addrs)
- `bench_ecdsa_u64e.py` (5 addrs)
- `test_http_local.py` (6 addrs)
- `test_http_live.py` (6 addrs)
- `phase3_tcp_echo.py` (11 addrs incl. RECV_BYTES alias)
Each script: module-scope constants become `-1` sentinels populated in
`main()` after labels load; `transport.memory_policy = policy` attaches
the policy right after `Ultimate64Transport(...)` construction; the
arbiter's per-allocation list is printed to test output for debugging.
## Validation
Real U64E hardware @ 10.43.23.81, 48 MHz turbo, both build configs:
- `make BACKEND=uci USE_X25519_SIBLING=1`: HTTPS PASS, 98.3 s wall-
clock, http_status=200, body="HELLO FROM TLS SERVER". Trampoline
allocated at $5100 (matches the prior defensive hack location).
- `make BACKEND=uci` (default, in-tree X25519): HTTPS PASS, 86.7 s
wallclock, http_status=200, body OK. Trampoline at $4200 — the
historical hardcoded value, now arbiter-derived because $4200
is genuinely free in the default build.
- First-attempt firmware flakiness (UCI_ERR_NO_SOCKET on cold
socket) observed on BOTH builds — orthogonal to migration; the
second attempt passes with zero code change. Pattern is identical
across configurations.
- Surfaced UserWarnings (expected, signal-bearing): 8 per run — 2
from `c64_test_harness.keyboard.send_text` writes to KERNAL
kbd-buf state ($00C0-$00CF, $0277-$0280), 6 from the arbiter's
own allocations because the policy doesn't auto-add arbiter claims
as safe regions. Both are follow-up scope.
## Follow-ups (not in this commit)
- Promote arbiter allocations to `safe_regions` via
`arbiter.policy_with_allocations()` (silences 6 of 8 warnings).
- Declare KERNAL kbd-buf state as safe (probably belongs in the
harness's `c64_test_harness.keyboard` rather than per-test).
- Tighten `unknown_policy = WARN` → `DENY` once the above lands.
- Migrate the VICE-side `tools/test_*.py` similarly when convenient
— the same factory works for ip65 builds (segment-marker driven).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 merged commit 0d5b6ab into masterMay 15, 2026
@JC-000
JC-000 deleted the feat/x25519-v0.4.0-phase-c5 branch May 15, 2026 11:27
JC-000 added a commit that referenced this pull request May 15, 2026
Two entries described work that has now landed on master:
1. The MEMORY requirements paragraph claimed "the sibling overlay
integration in Phase C.1 was rolled back — see Known issues."
That's historical; Phase C.5 (PR #41) landed a working flag-gated
sibling integration. Replaced with the current dual-state
description (default build leaves banks 0-1 free; sibling build
populates banks 0-5).
2. The "CRYPTO_OVERLAY address collision lesson" entry described
relocating ROUTINE_ADDR to $5100 as the fix. That defensive hack
was superseded by the MemoryPolicy + MemoryArbiter adoption in
PR #41's second commit (tools/uci/_memory_policy.py). Rewrote the
entry to point at the now-active safety mechanism: every
tools/uci/* script derives its scratch addresses from the arbiter,
and the c64-test-harness PR #95 transport-layer write guard
catches any future collision before a byte crosses the wire.
No behaviour change — documentation only.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

1 participant

@JC-000