Skip to content

fix(http): span-input parser — demo path shows the body, TLS plaintext never enters the ciphertext ring - #75

Merged
JC-000 merged 1 commit into
masterfrom
fix/demo-resp-body
Aug 12, 2026
Merged

fix(http): span-input parser — demo path shows the body, TLS plaintext never enters the ciphertext ring#75
JC-000 merged 1 commit into
masterfrom
fix/demo-resp-body

Conversation

@JC-000

Copy link
Copy Markdown
Owner

Closes#72.

The reported bug

The 'G' HTTPS demo in boot.s hand-rolled its response receive: one
net_poll/tls_recv iteration, then a single-page copy of that first
decrypted record into http_resp_buf (the "up to 512 bytes" comment was also
wrong — the loop caps at 256). Whenever the body arrived as a second TLS
record it was received and ACKed but never copied, so the demo displayed
headers only.

The deeper flaw it exposed

Routing the demo through the production parser surfaced a pre-existing problem
in http_get's own TLS path: it fed decrypted plaintext back into the shared
TCP ring
(tcp_recv_tail) and parsed from there via net_recv_byte. That
ring is the ciphertext queue. On ip65 the rx callback queues eagerly, so
ciphertext for later records (body record #2, the peer's close_notify) is
typically already sitting between head and tail — the parser then reads
ciphertext as HTTP text (garbage status) or loses the body. UCI masked it
because its adapter reads on demand.

Fix — dual-source parser input (http_in_byte)

modeinputused by
0TCP ring (delegates to net_recv_byte)plain HTTP — the ring legitimately holds plaintext there
1current decrypted record as a linear span (http_in_ptr/http_in_len, 16-bit)TLS

The ring stays pure ciphertext on the TLS path: no feed, no discard, and
multi-record bodies parse correctly on both backends. The span path also drops
the old feed loop's "TLS records < 256" assumption (it honored only
tls_app_len's low byte). http_in_byte preserves X/Y to match
net_recv_byte's contract and uses SMC rather than zp_ptr, which the parser's
body-store state owns.

The receive/parse loop is extracted as http_recv_body, shared by http_get
and the demo, so there is one implementation of the response semantics. Mode is
set per TLS record and reset to 0 in the plain path's parser init, so
alternating 'H' and 'G' in one session is safe.

Also (comment-only): tcp_recv_overflow semantics documented at its
declaration and set site — retransmission duplicates enter the ring too, so the
flag can latch during retransmit bursts with in-flight data far below ring size
(observed: 690 B flight, stream intact). It is a diagnostic breadcrumb, not
proof of loss — but ip65 ACKs the full inbound length regardless of what the
callback copies, so a dropped tail that was new in-sequence data is genuinely
gone. No accounting redesign here.

User-visible: the 'G' demo now prints the response body instead of
status line + headers. Shared code, so this applies to UCI too.

Validation

testresult
all five link profilespass, sizes unchanged (47,105 / 62,977 B)
tools/test_http.py (VICE, mode-0 ring path incl. 120 B body + 404)27/27
ip65 REU e2e (VICE)PASShttp_status=200, resp_len=22, body match
ip65 REU-less onchip e2e (VICE, no -reu)PASS — 1,813.9 s, http_status=200, resp_len=22, body match
C64 Ultimate hardware, UCI onchip @48 MHz (mode 1 / TLS)PASS — 200, body HELLO FROM TLS SERVER; ring verified pure ciphertext (16 03 03…, 17 03 03…) with head==tail
C64 Ultimate hardware, plain HTTP @64 MHz (mode 0 / ring)PASS — 200, full 22 B body through http_recv_body

Both parser input modes are confirmed on real hardware, and both VICE screens
now end with the body text (TLS13 OK FROM C64 TEST) where they previously
showed headers.

🤖 Generated with Claude Code

…t never enters the ciphertext ring
Closes#72.
The 'G' HTTPS demo in boot.s hand-rolled its response receive: one
net_poll/tls_recv iteration, then a single-page copy of that first
decrypted record into http_resp_buf (the "up to 512 bytes" comment was
also wrong — the loop caps at 256). Whenever the body arrived as a
second TLS record it was received and ACKed but never copied, so the
demo displayed headers only.
Routing the demo through the production parser surfaced a deeper,
pre-existing flaw in http_get's own TLS path: it fed DECRYPTED plaintext
back into the shared TCP ring (tcp_recv_tail) and parsed from there via
net_recv_byte. The ring is the ciphertext queue. On ip65 the rx callback
queues eagerly, so ciphertext for later records (body record #2, the
peer's close_notify) is typically already sitting between head and tail —
the parser then reads ciphertext as HTTP text (garbage status) or loses
the body. UCI masked this because its adapter reads on demand.
Fix — dual-source parser input (http_in_byte):
mode 0 (plain HTTP): input = TCP ring, delegates to net_recv_byte;
the ring legitimately holds plaintext there.
mode 1 (TLS): input = the current decrypted record as a linear
span (http_in_ptr/http_in_len, 16-bit).
The ring stays pure ciphertext on the TLS path: no feed, no discard, and
multi-record bodies parse correctly on both backends. The span path also
drops the old feed loop's "TLS records < 256" assumption (it honored only
tls_app_len's low byte). http_in_byte preserves X/Y to match
net_recv_byte's contract and uses SMC rather than zp_ptr, which the
parser's body-store state owns.
The receive/parse loop is extracted as http_recv_body and shared by
http_get and the boot.s demo, so there is one implementation of the
response semantics. Each path keeps its own close sequence (the demo
prints progress). Mode is set per TLS record and reset to 0 in the plain
path's parser init, so alternating 'H' and 'G' in one session is safe.
Also (issue #72, comment-only): tcp_recv_overflow's semantics documented
at the declaration (src/data.s) and the set site (src/net/ip65/net.s).
Retransmission duplicates enter the ring too, so the flag can latch
during retransmit bursts with in-flight data far below ring size
(observed in the VICE e2e with a 690 B flight, stream intact). It is a
diagnostic breadcrumb, not proof of loss — but ip65 ACKs the full
inbound length regardless of what the callback copies, so if a dropped
tail ever WAS new in-sequence data it is genuinely gone and TLS errors
follow. No accounting redesign here.
User-visible change: the 'G' demo now prints the response body (via
ascii_chrout) instead of the status line + headers. This is shared code,
so it applies to the UCI backend too.
Validation: all five profiles link (ip65 / ip65+onchip / uci /
uci+onchip / uci+comb; sizes unchanged at 47,105 and 62,977 B).
tools/test_http.py 27/27 in VICE — the mode-0 ring path, including the
120-byte-body and 404 cases. ip65 e2e with body assertions: see the
follow-up commit/PR note.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 merged commit f663032 into masterAug 12, 2026
JC-000 added a commit that referenced this pull request Aug 13, 2026
master carried #74 (per-backend post-ServerHello drain budget) and #75
(span-input parser) independently of this branch. #74's change also
exists here, so both copies of its comment block conflicted.
Both conflicts are comment-only. Resolved in favour of this branch's
text, which carries the corrected per-poll cost: ~40 ms, not ~37 ms.
CLAUDE.md derives 40.7 ms (80.8 s / 1984 polls, C64U onchip) and
39.8 ms (78.9 s / 1984, U64E REU) from the measured regression; the
~37 ms figure on master is stale by one round of measurement.
No code differences were involved. Verified the load-bearing constants
are unchanged on both backends: UCI 1x16, ip65 8x250.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000 added a commit that referenced this pull request Aug 14, 2026
VICE ip65 rig, onchip profile (REU-less stock-C64 config), master
2ceb5b1: PASS, http_status=200, resp_len=22, body match, 1,876.0 s
accelerated against the 1,813.9 s reference (+3.4%). Phase shape
unchanged.
The reason this run was owed: the ip65 PRG is no longer byte-identical
to the #71-era build (417c708594... vs db31111031e2...) because #75's
span-input parser is real code. That change had only ever been
exercised on UCI. This is also ip65's first e2e since July, so it is
the first to cover #74, #75 and the ten audit PRs.
Co-Authored-By: Claude Opus 5 (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.

boot.s HTTPS demo path copies only the first decrypted TLS record into http_resp_buf — body never displayed

1 participant

@JC-000