Not for now — after the release. Filing it while the evidence is fresh.
What happened
The first 48 MHz run of tests/rig_ip65_rrnet_hw.py (2026-09-06, TURBO_MHZ=48, real RR-Net cartridge, U64E) passed 24 of 25 checks. HTTP 200 with the exact body out of the C64's own buffer, net_last_error $00, ClientHello with SNI on the wire, application data both ways, 43.1 s G to CONNECTION CLOSED. The one failure was check_tls_connected:
the highest tls_state observed is CERT_VERIFY (5), not CONNECTED (7)
The client was fine. The oracle was outrun.
The oracle problem, precisely
tls_state holds TLS_STATE_CONNECTED only between two stores:
src/tls13.s:303-304 sets it, immediately after the traffic-key derivation and the tls_write_seq/tls_read_seq reset.
tls_close (src/tls13.s:379) writes TLS_STATE_IDLE back over it at :382-383.
So a post-run read cannot distinguish "never reached CONNECTED" from "reached it and closed cleanly" — which is exactly why check_tls_connected's docstring says so and why the rig polls tls_state over DMA during the fetch instead of reading it afterwards.
tls_last_state is not a fallback. tls13.s writes it only on the @error path — sta tls_last_state appears exactly once, at :310 — so a clean run leaves it 0, indistinguishable from a run that never started.
Why it only bites now
At 1 MHz the CONNECTED window is minutes wide and the rig's 5 s poll cadence cannot miss it — the 1 MHz run on 2026-09-05 sampled it without trouble. At 48 MHz everything after the P-256 verify (client Finished, the GET, the response, the body, the close) fits inside a single 1 s poll. The phase timeline from the turbo run shows it: PROC at +15.2 s, then nothing until CONNECTION CLOSED at +43.1 s. Tightening the cadence does not fix this in general — the window is bounded by network round-trip time, not by anything the host controls, and REST reads cost tens of milliseconds each.
Proposed fix, and why it is a user decision
A high-water latch for tls_state: a byte that only ever moves upward, written alongside every sta tls_state, never cleared by tls_close, exported for rigs.
Cost, stated plainly: this is a change to shipped source, inside the TLS state machine, purely for test observability. A handful of bytes in LOADER/TLS_CODE (ip65 has ~40 B free in CRYPTO_RESIDENT and 16 B in LOADER, so placement is not free) and a new store on a security-relevant path. That is why this is not a rig-side fix somebody just lands.
Design notes for whoever picks it up:
- The latch must not swallow
TLS_STATE_ERROR ($FF), which is numerically the highest value but semantically not progress; check_tls_connected already special-cases $FF in the sampled value and the latch has to keep that distinction legible.
- The byte cost probably decides the design, so start there rather than from tidiness.
sta tls_state has ten sites, all in tls13.s (:137, 158, 169, 207, 218, 229, 243, 304, 312, 383 — nine in the state machine plus tls_close). A tls_state_high written inline at each keeps the two in sync by construction, but an inline compare-and-store is plausibly 7-9 bytes a site, so 70-90 B — against ip65's ~40 B free in CRYPTO_RESIDENT and 16 B in LOADER. On ip65 the shared routine may be the only form that fits, and it costs a jsr on every transition rather than being merely tidier. Measure both against a real link before choosing; ip65 is the binding constraint, not UCI.
- It wants a red case in
tools/test_ip65_hw_checks_unit.py like every other verdict.
The alternative, and why it is unattractive
Leave the check red at turbo and document it, which is what the turbo run did (tests/rig_ip65_rrnet_hw.py explains it in the TURBO_MHZ docstring section, and docs/engineering-notes.md records it). This is honest but corrosive: a permanently red check in a suite whose whole value rests on red meaning something is how a suite stops being read. This repo has three recorded cases of a suite passing for the wrong reason (#158, #161, #176); a check that is expected to fail is the same failure wearing the other colour.
Softening check_tls_connected to accept the terminal evidence instead is explicitly not the fix. The handshake's completion at turbo is currently inference from converging evidence — the decrypted body, the screen markers, application data both ways — and that is weaker than the direct observation the 1 MHz run had. A latch is what makes it direct again.
Not for now — after the release. Filing it while the evidence is fresh.
What happened
The first 48 MHz run of
tests/rig_ip65_rrnet_hw.py(2026-09-06,TURBO_MHZ=48, real RR-Net cartridge, U64E) passed 24 of 25 checks. HTTP 200 with the exact body out of the C64's own buffer,net_last_error$00, ClientHello with SNI on the wire, application data both ways, 43.1 sGtoCONNECTION CLOSED. The one failure wascheck_tls_connected:The client was fine. The oracle was outrun.
The oracle problem, precisely
tls_stateholdsTLS_STATE_CONNECTEDonly between two stores:src/tls13.s:303-304sets it, immediately after the traffic-key derivation and thetls_write_seq/tls_read_seqreset.tls_close(src/tls13.s:379) writesTLS_STATE_IDLEback over it at:382-383.So a post-run read cannot distinguish "never reached CONNECTED" from "reached it and closed cleanly" — which is exactly why
check_tls_connected's docstring says so and why the rig pollstls_stateover DMA during the fetch instead of reading it afterwards.tls_last_stateis not a fallback.tls13.swrites it only on the@errorpath —sta tls_last_stateappears exactly once, at:310— so a clean run leaves it0, indistinguishable from a run that never started.Why it only bites now
At 1 MHz the CONNECTED window is minutes wide and the rig's 5 s poll cadence cannot miss it — the 1 MHz run on 2026-09-05 sampled it without trouble. At 48 MHz everything after the P-256 verify (client Finished, the GET, the response, the body, the close) fits inside a single 1 s poll. The phase timeline from the turbo run shows it:
PROCat +15.2 s, then nothing untilCONNECTION CLOSEDat +43.1 s. Tightening the cadence does not fix this in general — the window is bounded by network round-trip time, not by anything the host controls, and REST reads cost tens of milliseconds each.Proposed fix, and why it is a user decision
A high-water latch for
tls_state: a byte that only ever moves upward, written alongside everysta tls_state, never cleared bytls_close, exported for rigs.Cost, stated plainly: this is a change to shipped source, inside the TLS state machine, purely for test observability. A handful of bytes in
LOADER/TLS_CODE(ip65 has ~40 B free inCRYPTO_RESIDENTand 16 B inLOADER, so placement is not free) and a new store on a security-relevant path. That is why this is not a rig-side fix somebody just lands.Design notes for whoever picks it up:
TLS_STATE_ERROR($FF), which is numerically the highest value but semantically not progress;check_tls_connectedalready special-cases$FFin the sampled value and the latch has to keep that distinction legible.sta tls_statehas ten sites, all intls13.s(:137, 158, 169, 207, 218, 229, 243, 304, 312, 383— nine in the state machine plustls_close). Atls_state_highwritten inline at each keeps the two in sync by construction, but an inline compare-and-store is plausibly 7-9 bytes a site, so 70-90 B — against ip65's ~40 B free inCRYPTO_RESIDENTand 16 B inLOADER. On ip65 the shared routine may be the only form that fits, and it costs ajsron every transition rather than being merely tidier. Measure both against a real link before choosing; ip65 is the binding constraint, not UCI.tools/test_ip65_hw_checks_unit.pylike every other verdict.The alternative, and why it is unattractive
Leave the check red at turbo and document it, which is what the turbo run did (
tests/rig_ip65_rrnet_hw.pyexplains it in theTURBO_MHZdocstring section, anddocs/engineering-notes.mdrecords it). This is honest but corrosive: a permanently red check in a suite whose whole value rests on red meaning something is how a suite stops being read. This repo has three recorded cases of a suite passing for the wrong reason (#158, #161, #176); a check that is expected to fail is the same failure wearing the other colour.Softening
check_tls_connectedto accept the terminal evidence instead is explicitly not the fix. The handshake's completion at turbo is currently inference from converging evidence — the decrypted body, the screen markers, application data both ways — and that is weaker than the direct observation the 1 MHz run had. A latch is what makes it direct again.