Skip to content

uci: bound net_poll's copy by the request; $8A is UDP-only, $8B breadcrumb (#140) - #143

Draft
JC-000 wants to merge 4 commits into
masterfrom
fix/140-socket-read-header-bound
Draft

uci: bound net_poll's copy by the request; $8A is UDP-only, $8B breadcrumb (#140)#143
JC-000 wants to merge 4 commits into
masterfrom
fix/140-socket-read-header-bound

Conversation

@JC-000

@JC-000 JC-000 commented Aug 24, 2026

Copy link
Copy Markdown
Owner

DRAFT until #142 merges — this branch pins a snapshot of #142; if #142 is amended in review, this branch must be rebased before merge (merging it first would land both commits under this title and leave #142 empty). After #142 merges, check with git merge-base --is-ancestor origin/fix/140-socket-read-header-bound origin/master once this one merges too.

Closes #140. Stacked on #142 (same file) — merge #142 first.

What the measurements said

Porting c64-wireguard's drop-on-over-claim check verbatim aborted every real github.com handshake at the first read (2/2). With the request/claim pair added to the rig's post-mortem dump:

poll uci_req_len uci_read_hdr
first (idle poll — ClientHello sent, ServerHello not yet arrived) $0200 $FFFF
last (22 B queued) $0016 $0016

Correction (same evening): $FFFF is the firmware's no-data sentinel on both transports — c64-wireguard measured every idle UDP poll returning it (11/11 sizes), and tls_last_state=2 means our first-poll capture was an idle poll waiting for ServerHello ("~690 B queued" was an inference). The drop-and-error cut aborted on an empty read; c64-wireguard had carried the same misfiling for four days as a documented firmware quirk. $FFFF must be excluded before any over-claim test. The code here already does the right thing (cap, then the DATA_AV check copies zero bytes) — commit cfebf5f corrects the comments only, PRG byte-identical.

What lands

  • Bound, not error: uci_poll_rem = min(header, uci_req_len). Bytes beyond the request were never delivered and stay queued for the next poll, so nothing is lost; the copy can no longer run away on a bogus header regardless of what the per-byte checks do.
  • $8A UCI_ERR_LONG_READ keeps c64-wireguard's datagram disposition (excess discarded by firmware → datagram unrecoverable → dropped) and is never emitted on TCP. Registry row reworded by disposition in c64-lib-contract#139.
  • $8B UCI_ERR_BAD_READ_HDR (new): header above the request that is not the $FFFF no-data sentinel — a value the firmware has no documented mode for; the stream-family counterpart of $8A (datagram: drop). Best-effort breadcrumb on the §13.1 short-write pattern: C=0, copy still capped, code left in net_last_error for post-mortems. Allocated in SPEC §13.2's table before the adapter emits it — the first code through the rule the table exists for.
  • Rigs: _dump_diag prints uci_req_len / uci_read_hdr.

Evidence

  • Cap-only build (eccb7ec7…): U64E 48 MHz, rig_https_live github.com PASS, HTTP 200, CFIN 48.5 s.
  • Cap + $8B build (2396d97d…): $88 NO_SOCKET at TCP_CONNECT, 2/2, tls_last_state=0, uci_req_len=$0000 — the socket never opened, so net_poll never ran. This is the device's after-N-loads connect degradation (~14 loads today; c64-wireguard measured P P P P P T T T on unmodified master), not the change, which is unreachable without a socket. The twelve added bytes are hardware-unverified today; a power cycle and one run will close that. Both builds link clean, no warnings.
  • The first-cut drop-and-error build (a6d2d3b1…) is the negative control: it FAILs with $8A at tls_last_state=2 every time.

🤖 Generated with Claude Code

JC-000 and others added 2 commits August 24, 2026 12:46
… §13, #70)

The header used to be documentation: nothing included it, its declared
surface overlapped the used one in 6 of 17 symbols, and the ip65 adapter
had no error channel. Now every network-touching TU (boot, http,
tls_record_io, tls13) `.include`s it and imports no `net_*` directly, so
both backends must export the whole core/TCP/DNS surface or the link
fails by name.

ip65 (the tight one, LOADER had 16 B free):
  - net_dhcp -> net_dhcp_acquire; net_local_ip / net_resolved_ip copied
    from ip65_cfg_ip / ip65_dns_ip_addr on success; net_last_error with
    NET_ERR_IP65_* ($41-$45, ip65_errors.inc, the §13.2 ip65 range);
    net_tcp_state per §13.1.
  - net_print_ip and net_recv_ready left the adapter (below), which is
    what paid for the ~95 B of new logic in place. LOADER ends with
    58 B free instead of 16.
  - net_tcp_recv_cb / net_save_zp / net_restore_zp un-exported (§13.5).
  - net_manifest.s: NET_BACKEND_FAMILIES = CORE|TCP|DNS plus the §13.7
    blob equates, with LIB_NET_IP65_BLOB_SIZE link-asserted against
    ip65_blob_end - ip65_blob_start (labels added around the .incbin).

UCI:
  - net_dhcp alias, net_tcp_set_recv_cb stub and net_print_ip deleted;
    UCI_TCP_* -> NET_TCP_* in code (aliases kept in uci_errors.inc);
    ring-full now sets tcp_recv_overflow (§13.3); $8A UCI_ERR_LONG_READ
    reserved with c64-wireguard's meaning, not emitted.
  - net_manifest.s: CORE|TCP|DNS (DNS by deferral).

Consumer side:
  - src/net/net_families.inc (contract block verbatim, never exported),
    src/net/net_states.inc (NET_TCP_* values), src/net_abi_asserts.s
    (§13.8 family asserts + the §13.3 ring-mask assert moved out of
    lib_contract_asserts.s).
  - print_local_ip in boot.s replaces the two identical per-backend
    net_print_ip routines (§13.1 "deliberately not in the contract"),
    reading the now-ABI net_local_ip; it rides LOADER_OVERFLOW so the
    ip65 LOADER region gains rather than loses. Cost: the ip65
    HTTPS_HOST/HTTPS_PATH tail budget is ~60 B beyond the defaults now;
    the wikipedia target (+46 B) still builds on both ip65 profiles.
  - NET_BACKEND_FAMILIES exported `: absolute` — a byte-sized equate
    otherwise infers zeropage and ld65 warns (contract #74 recurring).

Evidence:
  - ip65-plain, uci-plain, ip65-onchip+wikipedia, uci-comb+wikipedia+
    HTTPS_BODY_TO_REU all link clean, no ld65 warnings; PRG sizes
    unchanged (47,105 / 62,977 B).
  - VICE: test_net.py 65/65, test_http.py 61/61, test_tls_handshake.py
    21/21.
  - U64E (uci-onchip, 48 MHz): boot_check PASS; phase2_check PASS —
    DHCP through net_dhcp_acquire, net_local_ip = 10.43.23.81 read back
    over DMA, net_last_error = 0. rig_https_local FAILs on this branch
    AND on master ab5de2b identically (name check vs the IP-as-hostname
    the rig sends) — filed separately; see the PR for the live-server
    run used instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…crumb (#140)

net_poll took the SOCKET_READ response header straight into
uci_poll_rem and copied, surviving a wild header only because the copy
loop re-checks ring-full and DATA_AV per byte. Measured on a U64E
(fw 3.14d, real github.com session): a 512 B request with ~690 B queued
is answered with header $FFFF; a 22 B request with 22 B queued with
$0016. On TCP the header is a count only when the queue fits the
request, and the $FFFF sentinel otherwise.

So c64-wireguard's drop-on-over-claim check does not port: a first cut
of it aborted every real handshake at the first read (two runs). What
lands instead is a bound — the copy count is capped at uci_req_len, the
request this poll actually made; bytes beyond it were never delivered
and stay queued, so nothing is lost. $8A UCI_ERR_LONG_READ keeps
c64-wireguard's datagram disposition and is never emitted here. A
header above the request that is NOT $FFFF — a value the firmware has
no documented mode for — leaves $8B UCI_ERR_BAD_READ_HDR as a best-
effort breadcrumb (C=0, copy still capped, §13.1 short-write pattern),
allocated in c64-lib-contract SPEC §13.2's table (#139) before use.

Evidence: cap-only build PASS on the U64E at 48 MHz, github.com HTTP
200, CFIN 48.5 s, PRG eccb7ec7. The cap+$8B build (2396d97d) hit
$88 NO_SOCKET at TCP_CONNECT 2/2 — the device's after-N-loads connect
degradation, before net_poll runs — so its twelve added bytes are
hardware-unverified today; stated on the PR rather than claimed.
rig_https_local/_live now dump uci_req_len/uci_read_hdr post-mortem.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JC-000 and others added 2 commits August 24, 2026 13:09
…omments and docs corrected (#140)

Code unchanged (byte-identical PRG): the cap plus the DATA_AV check
already copy zero bytes on the sentinel. What was wrong was every
comment saying $FFFF meant 'more than you asked': the first-poll
$FFFF was an idle poll waiting for ServerHello, as c64-wireguard's
idle-UDP measurement (11/11 sizes) makes clear.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s been observed (#140)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to 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.

net_poll trusts the firmware's claimed read length — port c64-wireguard's drop-not-trim validation

1 participant