Skip to content

tls_state has no high-water latch, so the rig's CONNECTED oracle is outrun at turbo #204

Description

@JC-000

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions