Skip to content

fix: remove 255-byte clamp in ip65 TCP RX callback - #27

Merged
JC-000 merged 7 commits into
masterfrom
fix/ip65-tcp-recv-callback-255-byte-clamp
May 6, 2026
Merged

fix: remove 255-byte clamp in ip65 TCP RX callback#27
JC-000 merged 7 commits into
masterfrom
fix/ip65-tcp-recv-callback-255-byte-clamp

Conversation

@JC-000

Copy link
Copy Markdown
Owner

Summary

Removes a data-loss bug in the ip65 TCP RX callback that silently
dropped any byte past the first 255 of a single TCP segment. This
unblocks end-to-end TLS 1.3 handshakes over the ip65 backend: a
Certificate record (~369 B) that arrives as one segment is now
reassembled correctly instead of being truncated.

Root cause

src/net/ip65/net.s clamped cb_remaining to 255 bytes when the
high byte was non-zero, on the theory that ip65 would re-deliver the
overflow on a subsequent callback. That never happens: ip65 ACKs the
full tcp_inbound_data_length regardless of how many bytes the
callback consumes, so anything past byte #255 was silently lost.

Repro: a TLS 1.3 Certificate record (~369 B) delivered as a single
TCP segment was truncated; tls_rec_buf ended up with a correct
prefix followed by bytes from the next TCP segment, breaking AEAD
reassembly during handshake.

Fix

Replace the clamp + 8-bit X-indexed copy loop with a 16-bit-safe
copy loop modeled on the UCI adapter's pattern in
src/net/uci/net.s. The inner loop still uses X as an 8-bit source
index, but when X wraps $FF -> $00 the SMC source high byte
(cb_copy_byte+2) is incremented so the next 256-byte window of
the inbound buffer is read from the correct address.
cb_remaining is decremented as a 16-bit quantity.

Validation

  • Server-side: local TLS 1.3 listener (ECDSA-P256 cert) completed
    full handshake, received GET / HTTP/1.1 from the C64, sent
    200 OK with body HELLO FROM TLS SERVER, and closed cleanly
    via the 4-way teardown.
  • C64-side: VICE + ip65 at 1 MHz (no WARP) displayed
    CONNECTION CLOSED on screen; bridge's LISTEN :443 returned
    to accept-polling after the ESTAB socket closed.
  • UCI backend (src/net/uci/net.s) unchanged and unaffected; this
    bug was ip65-specific.

Known follow-up

The HTTP response body is rendered on the C64 screen with
ASCII -> screen-code mismatch (cosmetic; filed as a separate issue).
The bytes in http_resp_buf are correct — only the display code
needs the same translation the boot banner already applies.

Test plan

  • python3 tools/test_tls_handshake.py (21/21)
  • python3 tools/test_keyschedule_steps.py (9/9)
  • python3 tools/test_hkdf.py (12/12)
  • VICE + ip65 1 MHz end-to-end HTTPS GET: server receives
    request, sends response, TCP teardown clean, C64 shows
    CONNECTION CLOSED.

Generated with Claude Code.


Originally posted by @JC-000 on 2026-04-19

JC-000and others added 7 commits April 18, 2026 09:36
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The other five tools/uci/ scripts already read U64_HOST with a default
of 192.168.1.81; these two had the address hardcoded. Make them
consistent and document the override in README and CLAUDE.md.
Co-authored-by: JC_000 <3798556+JC-000@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ┴ on the Key Schedule box was at column 28 while the ┬ above it
(and the │ bridging them) sat at column 29.
Co-authored-by: JC_000 <3798556+JC-000@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause
----------
The ip65 TCP RX callback in src/net/ip65/net.s clamped cb_remaining to
255 bytes when the high byte was non-zero, then relied on the ip65 stack
to re-deliver the overflow on a subsequent callback. That second call
never happened: ip65 ACKs the full tcp_inbound_data_length regardless of
how many bytes the callback consumes, so any byte past #255 was silently
dropped.
Repro: a TLS 1.3 Certificate record (~369 B) delivered as a single TCP
segment was truncated; tls_rec_buf ended up with a correct prefix
followed by bytes from the next TCP segment, breaking AEAD reassembly
during handshake.
Fix
---
Replace the clamp + 8-bit X-indexed inner loop with a 16-bit-safe copy
loop modeled on the UCI adapter's pattern (src/net/uci/net.s). The
inner loop still uses X as an 8-bit source index, but when X wraps
$FF -> $00 the SMC source high byte (cb_copy_byte+2) is incremented so
the next 256-byte window of the inbound buffer is read from the
correct address. cb_remaining is decremented as a 16-bit quantity.
Validation
----------
- tools/test_tls_handshake.py (21/21), tools/test_keyschedule_steps.py
(9/9), tools/test_hkdf.py (12/12) all pass.
- VICE + ip65 1 MHz (no WARP) end-to-end HTTPS test reaches PASS:
server-side log shows completed TLS 1.3 handshake, received
"GET / HTTP/1.1" from the C64, sent 200 OK with body "HELLO FROM TLS
SERVER", and the TCP connection closed cleanly via the 4-way
teardown. C64 screen shows "CONNECTION CLOSED"; the bridge's
LISTEN :443 socket returned to accept-polling once the per-connection
ESTAB socket closed.
- UCI backend (src/net/uci/net.s) unchanged and still passes; the bug
was ip65-specific.
Known follow-up
---------------
The HTTP response body is rendered on the C64 screen with
ASCII -> screen-code mismatch (cosmetic, filed as a separate GitHub
issue). The response bytes in http_resp_buf are correct; only the
display code needs the translation the boot banner already does.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Checks in the VICE/ip65 companion to the UCI e2e test that validated
the ip65 TCP RX 255-byte clamp fix in this PR.
- Runs a full TLS 1.3 handshake + HTTP GET against a local python
listener bound to the bridge IP (10.0.65.1:443).
- All timeouts scaled for 1 MHz stock (SENTINEL/ACCEPT/settimeout=9000s,
heartbeat 120s) so ECDSA-P256 CertificateVerify (~60-70 min at 1 MHz)
has headroom in a ~2-3 h total budget.
- Pre-flight port gate + `VICE_HTTPS_OK_TO_RUN=1` env var to prevent
accidental concurrent start while the UCI 443/4433 listener is in
use on the same host.
- Per-heartbeat screen-RAM snapshot under
/tmp/c64-https-phase3-1mhz-screens/ so a long run's stall point can
be bisected post-hoc.
- _dump_diagnostics extended with TLS key-material snapshots (client
random, server random, ECDHE priv/pub, server pub, shared secret)
to let an offline decryptor distinguish ip65-deadlock from
TLS-stall from AEAD failure.
Run after the UCI HTTPS listener has stopped:
sudo env VICE_HTTPS_OK_TO_RUN=1 PYTHONPATH=tools \
python3 tests/test_phase3_https_1mhz.py
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 merged commit 84fbea9 into masterMay 6, 2026
JC-000 added a commit that referenced this pull request May 6, 2026
* README: split binary size + label count by backend
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add MIT LICENSE
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* README: align right edge of TLS inner crypto rows in diagram
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* tools/uci: honor U64_HOST in boot_check.py and phase2_check.py (#25)
The other five tools/uci/ scripts already read U64_HOST with a default
of 192.168.1.81; these two had the address hardcoded. Make them
consistent and document the override in README and CLAUDE.md.
Co-authored-by: JC_000 <3798556+JC-000@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* README: align Handshake Proto → Key Schedule connector in diagram (#26)
The ┴ on the Key Schedule box was at column 28 while the ┬ above it
(and the │ bridging them) sat at column 29.
Co-authored-by: JC_000 <3798556+JC-000@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: remove 255-byte clamp in ip65 TCP RX callback (#27)
* fix: remove 255-byte clamp in ip65 TCP RX callback
Root cause
----------
The ip65 TCP RX callback in src/net/ip65/net.s clamped cb_remaining to
255 bytes when the high byte was non-zero, then relied on the ip65 stack
to re-deliver the overflow on a subsequent callback. That second call
never happened: ip65 ACKs the full tcp_inbound_data_length regardless of
how many bytes the callback consumes, so any byte past #255 was silently
dropped.
Repro: a TLS 1.3 Certificate record (~369 B) delivered as a single TCP
segment was truncated; tls_rec_buf ended up with a correct prefix
followed by bytes from the next TCP segment, breaking AEAD reassembly
during handshake.
Fix
---
Replace the clamp + 8-bit X-indexed inner loop with a 16-bit-safe copy
loop modeled on the UCI adapter's pattern (src/net/uci/net.s). The
inner loop still uses X as an 8-bit source index, but when X wraps
$FF -> $00 the SMC source high byte (cb_copy_byte+2) is incremented so
the next 256-byte window of the inbound buffer is read from the
correct address. cb_remaining is decremented as a 16-bit quantity.
Validation
----------
- tools/test_tls_handshake.py (21/21), tools/test_keyschedule_steps.py
(9/9), tools/test_hkdf.py (12/12) all pass.
- VICE + ip65 1 MHz (no WARP) end-to-end HTTPS test reaches PASS:
server-side log shows completed TLS 1.3 handshake, received
"GET / HTTP/1.1" from the C64, sent 200 OK with body "HELLO FROM TLS
SERVER", and the TCP connection closed cleanly via the 4-way
teardown. C64 screen shows "CONNECTION CLOSED"; the bridge's
LISTEN :443 socket returned to accept-polling once the per-connection
ESTAB socket closed.
- UCI backend (src/net/uci/net.s) unchanged and still passes; the bug
was ip65-specific.
Known follow-up
---------------
The HTTP response body is rendered on the C64 screen with
ASCII -> screen-code mismatch (cosmetic, filed as a separate GitHub
issue). The response bytes in http_resp_buf are correct; only the
display code needs the translation the boot banner already does.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* tests: add VICE ip65 1 MHz no-WARP HTTPS regression
Checks in the VICE/ip65 companion to the UCI e2e test that validated
the ip65 TCP RX 255-byte clamp fix in this PR.
- Runs a full TLS 1.3 handshake + HTTP GET against a local python
listener bound to the bridge IP (10.0.65.1:443).
- All timeouts scaled for 1 MHz stock (SENTINEL/ACCEPT/settimeout=9000s,
heartbeat 120s) so ECDSA-P256 CertificateVerify (~60-70 min at 1 MHz)
has headroom in a ~2-3 h total budget.
- Pre-flight port gate + `VICE_HTTPS_OK_TO_RUN=1` env var to prevent
accidental concurrent start while the UCI 443/4433 listener is in
use on the same host.
- Per-heartbeat screen-RAM snapshot under
/tmp/c64-https-phase3-1mhz-screens/ so a long run's stall point can
be bisected post-hoc.
- _dump_diagnostics extended with TLS key-material snapshots (client
random, server random, ECDHE priv/pub, server pub, shared secret)
to let an offline decryptor distinguish ip65-deadlock from
TLS-stall from AEAD failure.
Run after the UCI HTTPS listener has stopped:
sudo env VICE_HTTPS_OK_TO_RUN=1 PYTHONPATH=tools \
python3 tests/test_phase3_https_1mhz.py
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: JC_000 <3798556+JC-000@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(tools/uci): TURBO_MHZ env var for UCI HTTPS e2e test
Enable running the UCI HTTPS end-to-end test at arbitrary C64 speeds
(specifically stock 1 MHz) via environment variable, instead of
hand-editing the script to bump each wall-clock budget.
Behavior
TURBO_MHZ (default 48) drives
_TIMEOUT_SCALE = max(1.0, 48.0 / TURBO_MHZ)
which scales the five time-based constants used in the test:
- SENTINEL_POLL_TIMEOUT (C64-side handshake completion poll)
- ACCEPT_TIMEOUT (server-side accept + post-handshake read)
- the 22 s post-run_prg settle (entropy + REU sqtab + DHCP)
- the 2 s post-'Q' grace period
- the tls_conn.settimeout on the listener's recv()
Individual overrides remain available via SENTINEL_POLL_TIMEOUT and
ACCEPT_TIMEOUT env vars for fine-tuning without disturbing the
TURBO_MHZ-driven defaults. At the default 48 MHz _TIMEOUT_SCALE is
1.0 and the script behaves identically to before.
Validation
TURBO_MHZ=1 was used to run the full UCI HTTPS handshake at stock
1 MHz on real U64E hardware; the handshake + HTTP GET completed
with "HELLO FROM TLS SERVER" decoded, ~1h 37min wall-clock.
Unit tests (test_tls_handshake.py, test_keyschedule_steps.py) pass
unchanged; both ip65 and UCI backends build clean;
`python3 -m py_compile tools/uci/test_https_local.py` is clean.
Docs
README.md and CLAUDE.md updated to mention TURBO_MHZ and the
per-test timeout overrides alongside the existing U64_HOST,
HTTPS_PORT, DEBUG_CAPTURE, and KEEP_DEBUG_ON_PASS env vars.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: reflect post-merge ip65 HTTPS working state
Now that PR #27 (ip65 TCP RX 255-byte clamp fix + phase3 1 MHz test)
is merged, update docs to stop claiming the ip65/VICE HTTPS path is
blocked or unproven.
- README.md project-status bullet: both backends validated end-to-end.
- README.md Known Issues: drop the "ip65/VICE has not been measured
end-to-end" qualifier from the ECDSA section.
- README.md End-to-End Bridge Tests: all three phases (DHCP, HTTP,
HTTPS) pass.
- CLAUDE.md End-to-end HTTPS status: mention both backends, cite the
ip65 fix.
- CLAUDE.md Summary of recent fixes: add item 10 for the 255-byte
clamp fix.
- CLAUDE.md Known issues: add the cosmetic http_resp_buf PETSCII
display note (issue #28).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
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