Skip to content

Align networking surface with c64-lib-contract SPEC §13 (network backend ABI) #70

Closed

Description

@JC-000

Tracking issue for c64-lib-contract §13 (network backend ABI) alignment — spec PR: JC-000/c64-lib-contract#47.

c64-lib-contract v0.5.0 adds §13, standardizing the net backend ABI across
consumers (c64-https is the origin surface; c64-wireguard forked it and
drifted). c64-https is closest to conformant; the items below close the gap.

Items

  1. Declare the de-facto surface in src/net_abi.inc. These are used by
    TLS/HTTP but never declared in the ABI header:
    • net_send_len (2 B, input to net_tcp_send)
    • TCP rx ring symbols: tcp_recv_buf, TCP_RECV_MASK, tcp_recv_head,
      tcp_recv_tail, tcp_recv_overflow (§13.3 table)
    • drain helpers net_recv_ready / net_recv_byte — declare as the
      blessed drain entry points, or retire in favor of documented raw ring
      access; pick one and make http.s (which currently does raw masked
      reads) consistent with the choice.
  2. Delete the net_tcp_set_recv_cb stub (RTS, zero callers) from both
    adapters and the ABI header — §13.1 retires it from the contract.
  3. Adopt NET_TCP_* state names (NET_TCP_CLOSED/CONNECTED/ERROR/ CONNECT_FAIL) as the primary names; keep UCI_TCP_* as aliases. Values
    are unchanged (§13.1 pins the existing values as normative).
  4. Ship src/net/<backend>/net_manifest.s exporting
    NET_BACKEND_FAMILIES (§13.0): ip65 backend = CORE|TCP|DNS; UCI backend
    = CORE|TCP|DNS (DNS by deferral — the bit still set per §13.0).
    For ip65 also export the §13.7 blob-footprint equates
    (LIB_NET_IP65_BLOB_BASE/_SIZE/_BSS_BASE/_BSS_SIZE).
  5. Ship src/net_abi_asserts.s (§13.8): core-family assert, required-
    families assert, ring-mask power-of-two assert.
  6. Error-namespace audit (§13.2): UCI codes $81-$89 are grandfathered;
    confirm no ip65-side code collides into $80-$BF and that any future
    ip65 codes allocate from $40-$7F.

No behavioral changes required — c64-https's semantics (bounded waits,
fence floor, 16-bit rx, poll-pump) are what §13 codified.

References

  • c64-lib-contract SPEC §13 (v0.5.0), esp. §13.1, §13.3, §13.8 intake table.

🤖 Generated with Claude Code

Activity

  1. JC-000 commented on Aug 14, 2026

    @JC-000
    OwnerAuthor

    Contract-alignment audit, 2026-08-14 — measured drift + sequenced plan

    Full re-measure of c64-https against c64-lib-contract. Everything below was run,
    not inferred. Toolchain ca65/ld65 V2.18; pins libs/nistcurves@v0.6.0,
    libs/x25519@v0.6.0; both backends link (ip65 47,105 B, UCI 62,977 B).

    Read SPEC.md on main, not tags — main is now v0.8.0 (2026-08-14). The newest
    tag is still v0.4.0, and CLAUDE.md's "main is v0.7.2 (checked 2026-08-13)" is one
    MINOR stale. New since that check: §4 load-bearing cfg attributes (v0.8.0), which
    is a consumer obligation we do not currently discharge — see item P4.


    Part 1 — what shipped now (PR #92)

    src/lib_contract_asserts.s, a new zero-byte TU that converts four
    believed-but-unenforced facts about libs/nistcurves into link-time errors:

    clause assert
    §1 LIB_ABI_VERSION = 0 — exported-surface generation gate
    §3 (LIB_NISTCURVES_REU_BANKS_USED & $C0) = 0 — the library must not claim REU banks 6/7, which src/boot.s:840-841 uses for the P-384 overlay images
    §8.0 LIB_NISTCURVES_SHARED_PRIMITIVES = $0007 tripwire (see item P2 for why the contract's assert is not writable yet)
    §13.3 TCP_RECV_MASK is 2^n - 1 (assemble-time; TCP_RECV_MASK is a local equate, not an import)

    The PRG is byte-identical before and after (ip65 sha256 417c7085…), which is
    the point: nothing moves, the linker just gains four opinions.

    Negative controls, both reverted afterwards:

    # ABI expectation 0 -> 99
    ld65: Error: src/lib_contract_asserts.s(135): libs/nistcurves: exported-surface
    generation changed (LIB_ABI_VERSION != 0) — re-check the integration, then bump
    the expected value in src/lib_contract_asserts.s
    
    # REU mask $C0 -> $04
    ld65: Error: src/lib_contract_asserts.s(150): libs/nistcurves claims REU bank 6
    and/or 7 — reserved by c64-https for the P-384 overlay images (src/boot.s:840)
    

    Three upstream defects found while writing it

    1. SPEC §1's consumer-side snippet cannot assemble. It gates with .if on an
      .imported symbol:

      .import LIB_X25519_VERSION_MAJOR
      .if LIB_X25519_VERSION_MAJOR < 1 .and LIB_X25519_VERSION_MINOR < 8
          .error "this consumer needs c64-x25519 v0.8 or later"
      .endif

      An imported symbol has no value until link, so ca65 V2.18 rejects it:
      Error: Constant expression expected. The working form is
      .assert <expr>, lderror, "<msg>" — which the contract already uses in
      §13.0/§13.8 for exactly this reason ("NET_BACKEND_FAMILIES is .imported, so
      its value is not known until link"). §1 and §13 disagree with each other, and §1
      is the one that does not run. Same copy-paste-facing-snippet class as
      contract feat(crypto): c64-x25519 v0.4.0 optional sibling integration (Phase C.5) #41 / feat: c64-lib-contract alignment + nistcurves v0.3.0 / x25519 v0.5.0+5 bumps #50 / feat(uci): C64 Ultimate support — 64 MHz e2e + WiFi DHCP + fence retune #58 — worth a fourth entry.

    2. nistcurves v0.6.0 exports LIB_VERSION_MAJOR/MINOR/PATCH without :abs.
      Values fit in a byte → ca65 infers zeropage, a consumer .import defaults to
      absolute, and every link emits ld65: Warning: Address size mismatch per symbol.
      This is contract v0.7.4's §8.4 defect recurring in §1. Fixed upstream at
      v0.9.0 (all four gained :abs, alongside the prefixed forms). It is why the
      shipped gate is ABI-only and carries no MINOR floor: eating two warnings on every
      build is worse than the check is worth, and the contract explicitly rules out the
      obvious workaround (.import … : zeropage pins a constant to an address size
      that is an artifact of its current value). A >= v0.9.0 pin makes the floor
      assert free — add it in the same PR as the bump.

    3. SPEC §5's worked fit-check example does not work for a minimal-archive
      consumer.
      LIB_NISTCURVES_RESIDENT_BYTES = 27,000 describes the whole library,
      but §6 exists precisely so we can link lib-p256-verify instead. CRYPTO_HOT is
      16,384, so the spec's own .assert RESIDENT_BYTES < __CRYPTO_HOT_SIZE__ fails
      against a conformant consumer doing the conformant thing. §5 needs per-variant
      manifest numbers, or the example needs a caveat.


    Part 2 — the drift this issue tracks, re-measured

    D1. src/net_abi.inc is not part of the build. At all.

    $ grep -rn 'net_abi.inc' src/ tools/ Makefile
    # ... every hit is a comment. Zero `.include "net_abi.inc"`.
    

    This reframes item 1 of the issue body. The header is not an under-specified ABI
    declaration — it is a documentation file that no translation unit includes, so
    none of its twelve .imports is enforced by anything. That is why the next finding
    is possible.

    D2. The ip65 backend does not provide half of the declared surface

    src/net_abi.inc:8-22 declares 12 symbols. src/net/ip65/net.s:29-40 exports these
    six of them, and not net_dhcp_acquire, net_tcp_set_recv_cb, net_local_ip,
    net_resolved_ip, net_last_error, net_tcp_state. src/net/ip65/net.s:28 says so
    in a comment: "(net_dhcp_acquire, net_tcp_set_recv_cb, etc.) are deferred to Phase 7."
    Phase 7 came and went.

    Notably it exports net_dhcp — the exact name §13.1 tells c64-wireguard to
    rename away from. The UCI adapter exports both (net.s:30 and net.s:42), so the
    divergence §13 was written to stop also exists inside this repo, between our own two
    backends.

    net_last_error does not exist under ip65 in any form. So issue item 6 ("confirm
    no ip65-side code collides into $80-$BF") resolves trivially and unhelpfully: there
    are no ip65 error codes because there is no ip65 error channel. On the UCI side the
    values are already conformant — src/net/uci/uci_errors.inc:8-18 allocates $81-$89,
    inside §13.2's $80-$BF UCI-family range, and UCI_TCP_* at :21-24 are $00/$01/ $02/$03, matching §13.1's normative table exactly. Item 3 is a pure rename.

    D3. The real consumer-side surface (what TLS/HTTP/boot actually import)

    src/boot.s:107-114        net_init, net_dhcp, net_poll, net_print_ip,
                              net_dns_resolve, net_tcp_connect, net_tcp_close,
                              net_banner_str
    src/http.s:61-67          net_dns_resolve, net_tcp_connect, net_tcp_close,
                              net_tcp_send, net_send_len, net_poll, net_recv_byte
    src/tls_record_io.s:28-30 net_tcp_send, net_recv_byte, net_send_len
    src/tls13.s:96            net_poll
    

    Eleven symbols. Five of them (net_dhcp, net_print_ip, net_send_len,
    net_recv_byte, net_banner_str) are absent from net_abi.inc; six of
    net_abi.inc's are imported by nobody. The declared surface and the used surface
    overlap in six symbols out of seventeen.

    net_print_ip and net_banner_str are interesting: §13.1 lists net_print_ip under
    "deliberately not in the contract" as a consumer UI helper — yet src/boot.s:110
    imports it from the backend. Both are consumer-UI concerns living behind the net
    ABI, and both need to move rather than be declared.

    D3a. One sub-item of issue item 1 is already resolved and can be struck

    The body asks to "make http.s (which currently does raw masked reads) consistent
    with the choice" of drain entry point. That is stale: the #72 dual-source redesign
    already made the choice
    . src/http.s:44-46 records dropping the
    tcp_recv_head/tcp_recv_tail imports, and src/http.s:279 is now a bare
    jmp net_recv_byte. So net_recv_ready / net_recv_byte are de facto the blessed
    drain entry points across every consumer TU; item 1 reduces to declaring them.

    D4. §8.0 — we are the APP_OWNED case, and the manifest currently lies

    c64-https provides all three §8 shared primitives itself:

    primitive provider
    §8.1 sqtab src/crypto/poly1305.s:21 (sqtab_init), src/crypto/shared/mul_tables.s:32 (mul_tables_init)
    §8.2 reu_mul src/boot.s:31 (reu_mul_init), src/data.s:123-124 (mul_dma_lo/hi)
    §8.3 ct_mul_8x8 src/crypto/poly1305.s:213 (ct_mul_8x8), :24 (mul_8x8 alias)

    so APP_OWNED = $0007. But the linked library still exports
    LIB_NISTCURVES_SHARED_PRIMITIVES = $0007 — measured on both the REU and
    FP_ONCHIP_MUL archives:

    $ ar65 x build/lib/nistcurves-p256.a lib_manifest.o && od65 --dump-exports lib_manifest.o
      LIB_NISTCURVES_SHARED_PRIMITIVES  0x00000007
      LIB_NISTCURVES_REU_BANKS_USED     0x00000007
      LIB_NISTCURVES_RESIDENT_BYTES     0x00006978  (27000)
      LIB_NISTCURVES_COLD_BYTES         0x000009C4  (2500)
      LIB_NISTCURVES_ZP_USAGE_BYTES     0x0000001F  (31)
    # LIB_NISTCURVES_SHARED_CONSUMES: ABSENT (contract v0.5.0 requirement, post-dates the pin)
    

    The contract's disjointness assert (LIB_A & APP_OWNED) = 0 would therefore fail,
    and it would be right to. The cause is that we resolve the double ownership by
    deleting archive members — tools/integration/build_nistcurves_p256.sh:130 drops
    mul_8x8.o and data_shared.o — rather than by the contract's SHARED_* deferral
    switches. Member surgery is invisible to lib_manifest.o, so the §5 mask describes
    the upstream archive, not the archive we link.
    Any consumer-side reasoning built on
    it is unsound today. That is the single most load-bearing thing in this audit.

    The fix is small and the mechanism is already proven in-tree: the same wrapper
    already rebuilds zp_config.o with -D overrides (:99-118). See P2.

    D5. build_x25519.sh hand-drops lib_version.o

    tools/integration/build_x25519.sh:174 deletes the member; contract v0.7.0 provides
    ca65 -D LIB_NO_BARE_EXPORTS=1 as the supported mechanism. Low priority: the
    duplicate-identifier trap the workaround exists for is currently unreachable —
    USE_X25519_SIBLING=1 does not link on either backend, so exactly one contract
    library is ever in a link, which is also why importing the bare LIB_ABI_VERSION
    above is safe today. This becomes load-bearing the moment the sibling links.


    Part 3 — sequenced plan

    Ordered so each step is independently verifiable and nothing blocks on hardware.

    P1 — net_abi.inc becomes real, or is deleted. (prereq for all §13 work)
    Pick one: either every TLS/HTTP/boot TU .includes it and the ip65 adapter grows the
    six missing symbols (D2), or the file is deleted as misleading. Declaring more symbols
    in a header nothing includes is strictly negative value. Recommend: make it real,
    because the alternative loses the only written record of the intended surface.
    Verify: both backends link; ip65 VICE e2e (tests/test_vice_https_macos.py)
    unchanged.

    P2 — fix the §8.0 manifest lie. (independent of §13; do this first if §13 slips)
    Rebuild lib_manifest[_onchip].o in build_nistcurves_p256.sh with the deferral
    switches that match what the wrapper actually does — -D SHARED_SQTAB_INIT=1 -D SHARED_CT_MUL_8X8=1 -D SHARED_REU_MUL_INIT=1, mirroring the existing zp_config.o
    rebuild. libs/nistcurves/src/lib_manifest.s:216-234 already implements the §8.0
    conditional form, and LIB_NISTCURVES_SHARED_PRIMITIVES is itself .ifndef-guarded
    at :232, so -D LIB_NISTCURVES_SHARED_PRIMITIVES=$0000 is an even blunter
    alternative. Then replace the tripwire in src/lib_contract_asserts.s with the
    contract's real assert. Per-profile accuracy needs a look — under FP_ONCHIP_MUL the
    library still supplies reu_fetch_mul_row while boot supplies the init, which §8.0's
    three-state model does not cleanly express; that split is worth raising upstream.
    Verify: four-command evidence bar; the assert must pass, and must fail when the
    -Ds are removed.

    P3 — §13 symbol surface (the large one; do not start before P1)
    Issue items 1/2/3 as written, plus D3's finding that net_print_ip and
    net_banner_str are consumer-UI concerns to move out rather than declare.

    P4 — §4 load-bearing cfg attributes (NEW, contract v0.8.0).
    We author SEGMENTS{} for every library segment in cfg/c64-https-{ip65,uci}.cfg,
    so v0.8.0's obligation ("a consumer placing library segments MUST preserve them")
    lands squarely on us. Two of the four measured failure modes are silent: dropping
    align = $100 on a page-aligned LUT only warns, and type = rw → type = bss
    emits nothing while the initialised bytes vanish. This is worth a pass over both
    cfgs against whatever the bumped libraries declare in their example cfgs.

    P5 — §13.0/§13.7/§13.8 manifests + asserts (issue items 4/5; blocked on P1)
    Cheap in isolation but not honest before P1: NET_BACKEND_FAMILIES for ip65
    cannot claim NET_FAMILY_CORE while the backend lacks net_local_ip and
    net_last_error. The §13.7 blob equates are free and already correct —
    measured ip65-build/ip65-c64.bin = 6,951 B = $1B27, base $2000, BSS
    $4000-$4F8B = $0F8C, i.e. bit-for-bit the values SPEC §13.7 uses in its own
    example, because it was written from this blob.

    P6 — retire the build_x25519.sh member drop (D5). Bundle with whatever PR first
    makes the x25519 sibling link.


    What this does not touch

    No behavioural change, no cfg change, no library bump, no hardware run. libs/ and
    cfg/*.cfg are being changed concurrently by the library-bump lanes; the assert file
    is deliberately the only new artifact so those merges stay clean.

  2. JC-000 commented on Aug 14, 2026

    @JC-000
    OwnerAuthor

    Two follow-ups to the audit above.

    1. The net_abi.inc finding is now corrected in-repo

    986b22d on the #92 branch. CLAUDE.md stated:

    Public net API is fronted by src/net_abi.inc. TLS/HTTP sources consume
    networking only through those symbols.

    That reads as a build-enforced boundary. It is not one, and the difference changes the
    size of item P1 substantially — the starting point for §13 is prose, not an
    interface, so P1 is "build the interface" rather than "extend it".

    Corrected in three places, since all three repeated the claim:

    • src/net_abi.inc's own header block — the highest-leverage spot, because it
      described itself as a "Drop-in contract: any backend ... must export these exact
      symbols". It now opens with the fact that no TU includes it, lists the three
      measured consequences, and keeps the original wording explicitly labelled as
      intent. It also warns that adding a symbol there changes nothing.
    • CLAUDE.md "Networking backend ABI" — the claim replaced with the measurement,
      and the symbol list now marks with * the six of twelve that ip65 actually
      exports.
    • src/net/ip65/README.md — same claim, plus a stale "Port of src/net.asm lands
      here in Phase 3 Batch D. Until then this directory holds only this README" line
      that has been wrong for many phases.

    Doc-only; both PRGs byte-identical across the commit (ip65 417c7085…, UCI
    741f0e8c…).

    The three facts, restated so they are findable from this issue:

    Nothing includes it grep -rn 'net_abi' src/ tools/ cfg/ Makefile → comments only. None of the twelve .imports is checked by ca65 or ld65.
    Declared ∩ used = 6 of 17 Used: net_init, net_dhcp, net_poll, net_print_ip, net_dns_resolve, net_tcp_connect, net_tcp_close, net_tcp_send, net_send_len, net_recv_byte, net_banner_str. Five absent from the header; six of the header's imported by nobody.
    ip65 provides 6 of 12 Missing net_dhcp_acquire, net_tcp_set_recv_cb, net_local_ip, net_resolved_ip, net_last_error, net_tcp_state. net_last_error exists only under UCI — ip65 has no error channel at all.

    2. The three upstream defects are filed

    All three carry the measurements from this audit rather than a second-hand summary,
    and each carries the fix that was verified to work:

    Not filed separately: the §8.0 manifest-vs-member-surgery finding, which is already
    #72 — same measurement, same
    build_nistcurves_p256.sh:130 line, already written up there.

    Plan items P1-P6 above are unchanged.

  3. added a commit that references this issue on Aug 14, 2026
  4. JC-000 commented on Aug 15, 2026

    @JC-000
    OwnerAuthor

    §13 plan refresh against contract v0.10.0 — 2026-08-15

    Re-scoped this issue against the current spec text rather than the v0.5.0/v0.6.0 text it was written from. Everything below was measured at master 3a43f61, ca65/ld65 V2.18, pins libs/nistcurves@v0.9.1 / libs/x25519@v0.6.0.

    Headline for anyone about to start: the plan does not need re-deriving. §13 has not moved.


    1. §13 is unchanged since contract v0.6.1

    Contract main is now v0.10.0 (2026-08-15), eleven versions past the v0.8.0 this repo was aligned to. But git diff 2d9429d..edf8dcb -- SPEC.md touches no §13 clause — the only diff line matching §13 is a v0.8.2 changelog sentence about §13 having been missed by an earlier audit. §13.0-§13.8 are byte-identical to f4af347/6da2cc2 (v0.6.0/v0.6.1, 2026-07-28 / 2026-08-12).

    So items 1-6 in the issue body are still correctly scoped, and no work done against them will be invalidated by the contract catching up. That is worth stating explicitly because the surrounding chapters moved a great deal in the same window.

    Two corrections to the body while we are here:

    • It cites "c64-lib-contract SPEC §13 (v0.5.0)" twice. §13 landed at v0.6.0 (f4af347); v0.5.0 is the SHARED_CONSUMES release. Minor, but the body is what people pin against.
    • Its closing claim — "No behavioral changes required — c64-https's semantics are what §13 codified" — is false as measured, in two independent places. Details in §3 below. This matters for sizing: the issue currently reads as a declarations-and-renames job, and it is not.

    2. The net_abi.inc measurement re-verified at HEAD

    The earlier finding (nothing .includes it; declared and used surfaces overlap in 6 of 17) is exact and unchanged at master 3a43f61. Re-derived rather than trusted:

    • grep -rn 'net_abi' src/ tools/ cfg/ Makefile → comments only. Not in the build; none of its 12 .imports is checked by anything.
    • Declared: 12. Imported by TLS/HTTP/boot: 11. Intersection: 6 (net_init, net_poll, net_dns_resolve, net_tcp_connect, net_tcp_send, net_tcp_close). Union 17.
    • Imported but undeclared (5): net_banner_str, net_dhcp, net_print_ip, net_recv_byte, net_send_len.
    • Declared but imported by nobody (6): net_dhcp_acquire, net_last_error, net_local_ip, net_resolved_ip, net_tcp_set_recv_cb, net_tcp_state.

    Backend-provided surfaces, measured from .exports:

    ip65 UCI
    exports 13 17
    has net_dhcp yes yes
    has net_dhcp_acquire no yes
    has net_last_error / net_tcp_state / net_local_ip / net_resolved_ip no yes
    has net_recv_ready / net_tcp_recv_cb yes no
    has net_tcp_set_recv_cb no yes (RTS stub)

    So the divergence §13 exists to stop runs between our own two backends in both directions — ip65 is missing six symbols UCI has, and exports two UCI does not.

    3. Two behavioral gaps the body does not capture

    Both are new to this issue, both measured.

    3a. ip65 has no net_last_error at all. §13.1 puts it in the core family, which §13.0 makes mandatory ("a backend that cannot init, acquire an address, and be pumped is not a backend"). ip65 exports neither net_last_error nor net_dhcp_acquire nor net_local_ip — three of the five core-family symbols. src/net/ip65/net.s:28 calls them "deferred to Phase 7"; Phase 7 shipped. This is not a rename: it is an error channel that has to be built, plus a $40-$7F code allocation per §13.2.

    Consequence for item 4 as written: ip65 cannot honestly export NET_FAMILY_CORE today. The manifest would be a false claim, and §13.8's own core-family assert would pass while the surface behind it is absent. Item 4 therefore has a hard dependency on 3a — shipping the manifest first would produce a green assert over a non-conformant backend, which is the failure shape this project has repeatedly called out.

    3b. UCI never sets tcp_recv_overflow. §13.3 specifies it as "set to 1 by the backend when the ring fills; the backend then drops." tcp_recv_overflow exists (src/data.s:160-163) and ip65 imports and sets it (src/net/ip65/net.s:46). The UCI adapter imports tcp_recv_head/tcp_recv_tail (net.s:71-72) but not tcp_recv_overflow, and its ring-full path (net.s:263-278) is:

            jmp @done_data          ; ring full — drop the rest

    — a silent drop. This is the backend that carries every hardware e2e run, so a ring-full truncation there currently presents as a TLS or HTTP parse failure with no diagnostic pointing at the transport. Cheap to fix (one lda #1 / sta tcp_recv_overflow) and it makes the §13.3 diagnostic real on the path that needs it most.

    4. What v0.9.0/v0.10.0 changed around §13

    §13's own text is static, but its "Scope (level 1 vs level 2)" note defers backend packaging as LIB_NET_<B>_* archives to "a future promotion... per §4/§6". §6 was 11 lines when that was written; it is now a six-clause chapter with archive-level obligations (§6.1 targets and no-member-surgery, §6.2 CONTRACT_DEFINES/CONTRACT_ZP_DEFINES, §6.3 reachability, §6.4 per-variant manifests, §6.5 name surface, §6.6 footprint asserts, §6.7 non-segment reservations).

    Practical effect: level 2 is now fully specified, so if the backends are ever promoted to archives the obligations are known up front rather than discovered. Nothing forces that promotion — §13.7 already handles the part that can never be §4-relocatable (the position-linked ip65 blob) — but item 4's §13.7 blob equates are the natural first step toward it and are worth doing in the level-1 pass regardless.

    Also relevant: §6.6's consumer footprint-assert pattern landed in PR #114 for libs/nistcurves; the same shape applies to a future LIB_NET_IP65_BLOB_SIZE fit check against NET_CODE, which is item 4's other half.

    5. Sequenced plan

    Ordered so nothing green-lights a surface that is not there. Items map to the body's numbering where they exist.

    P0 — cheap, independent, no dependencies. Do these first; each is self-contained and testable.

    • 3b — UCI sets tcp_recv_overflow on ring-full (§13.3). ~2 instructions.
    • Body item 2 — delete the net_tcp_set_recv_cb RTS stub from the UCI adapter and the header (§13.1 retires it; zero callers, and ip65 never had it).
    • Body item 6 — error-namespace audit (§13.2). UCI $81-$89 grandfathered; confirm nothing else lands in $80-$BF.

    P1 — build the interface (the actual bulk). This is "build", not "extend" — the starting point is prose. Make src/net_abi.inc a real TU that both backends and the consumers include, reconciling the 17-symbol union: declare the 5 used-but-undeclared, and for each of the 6 declared-but-unused decide retire or implement. Note net_print_ip is explicitly not backend surface per §13.1 ("a consumer UI helper") — it should move consumer-side rather than be declared. Pick one drain model for net_recv_ready/net_recv_byte vs http.s's raw masked reads and make it consistent.

    P2 — ip65 core family (3a). net_last_error + $40-$7F codes, net_dhcp_acquire (renaming from net_dhcp, dual-named per §6.5's window rule), net_local_ip. Largest behavioral item; gates P3.

    P3 — manifests (body item 4). src/net/net_families.inc (copied verbatim, unexported, per §13.0) + per-backend net_manifest.s exporting NET_BACKEND_FAMILIES. ip65 also gets the §13.7 blob equates. Only honest after P2.

    P4 — asserts (body item 5). src/net_abi_asserts.s per §13.8. Note the ring-mask assert of that trio already ships — src/lib_contract_asserts.s:222 carries .assert (TCP_RECV_MASK & (TCP_RECV_MASK + 1)) = 0 — so this item is the two family asserts plus moving that one across for cohesion.

    P5 — NET_TCP_* state names (body item 3). Pure rename with UCI_TCP_* aliases retained; values normative and unchanged. Deliberately last: it touches the most call sites for the least behavioral gain, and doing it before P1 means renaming twice.

    Not scheduled: level-2 archive packaging (§13 scope note). No forcing function, and §13.7 already covers the blob.

    6. Not release-blocking

    Stated for whoever picks this up under time pressure: none of P0-P5 is required for a release. §13 conformance buys cross-consumer propagation of the hardened UCI adapter (its stated payoff case is c64-wireguard's adapter predating our bounded waits, fence floor and phantom-socket detection) — real value, but not on the path to shipping artifacts from this repo. 3b is the one exception worth pulling forward, because it is two instructions and it closes a live diagnostic hole on the hardware path.

    Measured for PR #114 (contract v0.8.0 → v0.10.0 alignment); no §13 code changed there.

    🤖 Generated with Claude Code

  5. JC-000 commented on Aug 16, 2026

    @JC-000
    OwnerAuthor

    Re-checked at master 39a43eb against SPEC v0.10.6 — still fully relevant; nothing here has been overtaken. Item-by-item, measured rather than recalled:

    # item state
    1 declare the de-facto surface in net_abi.inc partial — net_send_len and net_recv_byte are declared; tcp_recv_buf, TCP_RECV_MASK, tcp_recv_head, tcp_recv_tail, tcp_recv_overflow and net_recv_ready are all still missing
    2 delete the net_tcp_set_recv_cb stub not done — still in src/net/ip65/net.s, src/net/uci/net.s and src/net_abi.inc
    3 adopt NET_TCP_* state names not done — no NET_TCP_* symbol anywhere in src/
    4 ship net_manifest.s / NET_BACKEND_FAMILIES not done — no such file; the identifier appears only inside a comment in lib_contract_asserts.s
    5 ship net_abi_asserts.s not done — file absent
    6 error-namespace audit not evidenced either way

    And the header this issue exists to make real is still .included by nothing — grep -rn 'include.*net_abi' src/ returns empty, so none of its declarations is checked by the assembler or the linker.

    The spec has not moved under it

    Worth confirming, since the issue cites §13 at contract v0.5.0 and the contract is now v0.10.6. §13's structure is unchanged — §13.0 through §13.8 are all present with the same clause numbers and the same meanings, and the restructuring at v0.10.1 explicitly renumbered nothing (§13 was moved as a domain chapter with its number preserved, precisely because renumbering would have broken the fleet's ~900 cross-references). So every item above still maps to the clause it was written against.

    Why none of the recent work touched it

    The §6.1/§8.0 alignment (#119, PR #122) was entirely archive- and crypto-side: CONTRACT_DEFINES, member sets, manifest attestation, ZP slots. It shares no surface with the networking ABI. Likewise the VIC blanking and comb-lineup work.

    The one thing that has changed: this is now the only open contract gap

    With §6.1 satisfied by construction and the §8.0 disjointness/coverage asserts live, §13 is the sole remaining non-conformance in c64-https. That does not make it more urgent — it is interface hygiene rather than correctness, nothing ships differently because of it, and it has never blocked a release — but it does mean the consumers.md row can no longer point at anything else.

    Sizing note for whoever takes it: items 2–5 are mechanical and PRG-neutral-ish, but item 1 forces a real decision the issue already flags — net_recv_ready/net_recv_byte as blessed drain entry points versus documented raw ring access, with http.s currently doing raw masked reads. That choice should be made before the declarations are written, not after.

  6. JC-000 commented on Aug 24, 2026

    @JC-000
    OwnerAuthor

    Coordination note with c64-wireguard#48 — 2026-08-24

    Re-measured both sides at c64-https master ab5de2b and the c64-wireguard checkout at v1.0.0-34-gdfe12bd (2026-08-24), against c64-lib-contract origin/main (v0.11.1; v0.10.3 is the newest tag).

    1. The contract has not moved §13

    git diff v0.10.0..origin/main -- SPEC.md touches no §13 clause — only the reading-order preamble and changelog lines that mention it. §13.0–§13.8 remain byte-identical to v0.6.1. The changes since v0.10.0 (§6.3 loud-fail consequences, §6.5/§1 zero-consumer carve-outs, §8.3 provider-surface enumeration) do not re-scope items 1–6 here. The plan refresh above still stands.

    2. c64-wireguard is ahead on the §13 mechanics, not behind

    Measured from its tree, not from the issue text:

    c64-https c64-wireguard
    net_abi.inc .included by a TU no (comments only) yes (src/boot.s)
    UCI_FENCE_INNER 217 217
    UCI_ERR_NO_SOCKET $88 / UCI_ERR_WAIT_TIMEOUT $89 yes yes
    TOD-bounded waits in uci_cmd.s yes yes (31 TOD references)
    ip65 net_last_error / net_dhcp_acquire / net_local_ip absent n/a (UDP blob)

    So wg#48 workstream B (port the hardening) is substantially landed on their side, and the item that blocks §13 manifests is ours: P1 (make net_abi.inc real; ip65 cannot honestly claim NET_FAMILY_CORE without an error channel).

    3. Two cross-repo items not in either issue

    3a. $8A UCI_ERR_LONG_READ — namespace reservation. c64-wireguard's src/net/uci/uci_errors.inc:28 allocates UCI_ERR_LONG_READ = $8A ("SOCKET_READ claimed more bytes than we asked for"). §13.2 grandfathers $80-$BF as one UCI-family namespace shared by every consumer; c64-https's uci_errors.inc stops at $89. Proposal: c64-https reserves $8A with the same name and meaning (our net_poll has the identical over-claim check to attach it to), and the next allocation in either repo is filed against the contract first so the two adapters cannot collide silently. The contract could carry the allocation table in §13.2 — that is a one-line spec change I can file if the wireguard maintainer agrees.

    3b. The net_poll ring-drop fix has not travelled. c64-https d9cd021 found that the UCI adapter requested a fixed 512 B per SOCKET_READ while its ring-fill loop silently dropped bytes past the ring's free space — a permanent hole one ring-capacity past the first wrap on any flight over 4 KB. Fix: clamp the request to min(ring_free - 1, 512) and skip the read when nothing fits (src/net/uci/net.s). Not a firmware bug. This is direct §13.3 context for c64-wireguard#46 (the 512-byte inbound SOCKET_READ cap): whatever MTU policy that issue settles on, the request-vs-free-space clamp is the part that has to hold regardless, and it is what §13's purpose statement — hardening travels instead of being re-discovered — exists for.

    4. Sequencing proposal across both repos

    1. c64-https P1 (net_abi.inc becomes real; ip65 grows net_last_error + net_dhcp_acquire + net_local_ip, codes from $40-$7F). No cross-repo dependency.
    2. Both repos adopt the shared src/net/net_families.inc header and the $8A reservation (3a) — one small PR each, can land in either order.
    3. c64-wireguard evaluates 3b against wg#46.
    4. Manifests + net_abi_asserts.s (items 4/5 here, wg#48 A.7) — only after 1, so the core-family assert is not green over an absent surface.

    Cross-referenced from c64-wireguard#48.

  7. JC-000 commented on Aug 24, 2026

    @JC-000
    OwnerAuthor

    Reply from the c64-wireguard side — 2026-08-24

    Agreeing to 3a and the sequencing, correcting 3b, and adding a measurement that closes one of the open questions.

    3a. $8A UCI_ERR_LONG_READ — agreed, please reserve it

    Agreed, with the history since it explains why the value is $8A and not something tidier.

    UCI_ERR_LONG_READ was allocated at $88 by wg PR #62 (the unbounded-read fix: net_poll trusted SOCKET_READ's returned length and copied ~18 KB from udp_recv_buf through $D000, leaving WireGuard packet bytes in the VIC registers). That collided head-on with your $88 = UCI_ERR_NO_SOCKET. Taking your table verbatim would have silently redefined a code this adapter emits during healthy operation on fw 3.14d, so LONG_READ moved to $8A and $88/$89 now match c64-https exactly.

    Worth noting for the contract discussion: $88 as we had it was conformant with §13.2 as written. That clause reserves the $80-$BF range and says "a backend MUST emit only $00 or codes from its own family range" — it publishes no name→value registry and says nothing about a consumer that already occupies a value with a different meaning. Two adapters agreeing on $81-$87 and silently disagreeing on $88 is exactly the drift §13 exists to prevent, and the spec as written does not prevent it.

    So yes to the one-line spec change, and please make it the actual allocation table rather than only reserving $8A — $00, $81-$89 shared, $8A UCI_ERR_LONG_READ, plus the rule that the next allocation in either repo is filed against the contract first. Reserving $8A bilaterally fixes today's collision and leaves the mechanism that produced it in place.

    3b. The ring-drop clamp does not port — we have no ring

    This one needs correcting before someone spends a day on it.

    d9cd021 is a genuinely good fix and I read it closely, but it is specific to a wrapping TCP ring: the hole appears one ring-capacity past the wrap origin because the fill loop drops bytes past tcp_recv_head/tcp_recv_tail free space while the firmware counts them delivered.

    c64-wireguard has no such structure. Inbound is one datagram into a flat 1500-byte udp_recv_buf, interlocked by udp_recv_ready (§13.3's UDP shape, not the TCP-ring shape). There is no wrap, no free-space computation, and no TCP_RECV_MASK — the consumer drains the whole datagram before the next poll is allowed to deliver. So min(ring_free - 1, 512) has nothing to clamp against.

    The transfer runs the other way here. c64-wireguard's net_poll validates the firmware's claimed length before trusting it (src/net/uci/net.s, the @hdr_done block): if actual_len exceeds UCI_READ_CHUNK_MAX it drops the datagram rather than trimming it, and reports $8A. c64-https takes the response header straight into uci_poll_rem and copies, surviving a bogus length only incidentally because the inner loop re-checks ring-full and DATA_AV per byte. Given this firmware returns 0xFFFF for a 1500-byte request, it can equally return a bogus length for a 512-byte one — worth lifting our check upstream. Happy to open that as an issue here.

    New measurement: the inbound cap is a hardware ceiling, and it is worse than "truncates"

    Ran tools/test_uci_udp_size_probe.py on U64E fw 3.14d today. This settles the open question behind wg#46, which had been asserted but never measured:

    requested received remainder on a later net_poll
    512 512 — (exact)
    600 512 never arrives
    768 512 never arrives
    1280 512 never arrives
    1024 0 nothing delivered at all
    1500 0 nothing delivered at all

    The remainder is discarded, not queued, so multi-read reassembly cannot recover it. Received tails are f0f1…feff — byte offsets 496-511 — i.e. exactly the first 512 bytes. And the failure is not monotonic: 1024 and 1500 deliver nothing while 1280 still delivers 512.

    Consequence for wg#46: WG_MTU is pinned at 480 (512 - 32 for the Type-4 header and Poly1305 tag), enforced by an assembly-time assert against UCI_READ_CHUNK_MAX. Raising it needs a firmware change, not a code change. If c64-https ever grows a UDP path, this is the ceiling it inherits.

    4. Sequencing — agreed as written

    No objection to the order. Two notes:

    • P1 is genuinely yours and genuinely blocking. Neither repo can ship a NET_BACKEND_FAMILIES manifest honestly while ip65 has no error channel, so a NET_FAMILY_CORE claim would be green over an absent surface. Agreed that manifests come last.
    • On step 2, when we adopt the shared src/net/net_families.inc: note it is a v0.6.1 addition that post-dates both issue texts — the four NET_FAMILY_* constants must live in that one header, be copied verbatim, and be never .exported (only NET_BACKEND_FAMILIES is exported; unexported constants cannot collide at link). The v0.6.1 change also moved §13.0's consumer assert from error to lderror, since NET_BACKEND_FAMILIES is imported and unknown until link time. Both are easy to miss when implementing from the issue text rather than the current spec.

    Contract version — minor correction

    You cite "v0.11.1; v0.10.3 is the newest tag". The newest tag is v0.11.0 (v0.10.4 through v0.10.7 and v0.11.0 all exist). It does not change your conclusion — I diffed §13 at v0.11.0 against origin/main and the chapter is byte-identical, so v0.11.1's §6.3-only delta is irrelevant to this work. Recommend both repos pin v0.11.0 rather than tracking main, since v0.11.1 is untagged.

    Cross-referenced from c64-wireguard#48.

  8. JC-000 commented on Aug 24, 2026

    @JC-000
    OwnerAuthor

    Correction to my measurement above — 2026-08-24

    Re-ran the size probe on a freshly power-cycled unit with the large sizes first and each repeated, because the first pass ran them last and this device is known to degrade after ~5 cycles. One row in my table was my own test-order artifact:

    requested received remainder on a later net_poll
    ≤ 512 exact —
    600 512 never arrives
    768 512 never arrives
    1024 512 never arrives
    1280 512 never arrives
    1500 0 nothing delivered at all

    1024 delivers 512, not nothing. My earlier 1024 → 0 was the device degradation creeping in mid-run, so my remark that "the failure is not even monotonic" was wrong — please disregard it. The real behaviour is orderly: exact up to 512, truncated to 512 from 600 through 1280, total loss at 1500 (consistent with the known 0xFFFF-for-1500 quirk).

    The conclusion is unchanged and now better supported. 2nd? was 0 in all ten oversized trials — the remainder is discarded, never queued. Multi-read reassembly is impossible; WG_MTU = 480 is a hardware ceiling on fw 3.14d.

    Methodological note, since it will bite anyone reproducing this: probe large-first on a freshly power-cycled device. Sizes measured late in a session can report a degradation as if it were firmware behaviour. Separately, that degradation is real and independent of adapter code — c64-wireguard's echo test gives P P P P P T T T from a fresh power cycle on unmodified master and on the fully-hardened branch alike, so it is device-side and not something either adapter's hardening addresses.

  9. JC-000 commented on Aug 24, 2026

    @JC-000
    OwnerAuthor

    Resolution — 2026-08-24

    c64-https PR #142 lands the alignment (closes this issue on merge):

    • src/net_abi.inc is now the build-enforced boundary: boot.s, http.s, tls_record_io.s, tls13.s .include it and import no net_* directly. Both backends export the full core/TCP/DNS surface or the link fails by name.
    • P1 closed: ip65 gained net_dhcp_acquire, net_local_ip, net_resolved_ip, net_last_error ($41-$45, the §13.2 ip65 range) and net_tcp_state. LOADER ends with more room than before (58 B vs 16) because net_print_ip (→ consumer-side print_local_ip) and net_recv_ready left the adapter.
    • Items 2/3/4/5/6: net_tcp_set_recv_cb deleted; NET_TCP_* primary (byte-neutral, aliases kept); net_manifest.s per backend (ip65 with §13.7 blob equates link-asserted against the .incbin span); net_abi_asserts.s; error namespaces audited — ip65 had none and now has $41-$45, UCI reserves c64-wireguard's $8A.
    • 3b from the refresh: UCI sets tcp_recv_overflow on ring-full.
    • Evidence: 4 profile/knob builds clean; VICE 65/65 + 61/61 + 21/21; U64E uci-onchip 48 MHz boot_check/phase2_check PASS (net_local_ip read back over DMA via net_dhcp_acquire), rig_https_live github.com HTTP 200.

    c64-lib-contract PR #139 (v0.12.0 draft): §13.2 allocation table (ip65 $41-$45, UCI $81-$8A incl. wireguard's $8A) + allocate-here-first rule; canonical net_families.inc at the repo root; §13.8 records c64-https as aligned.

    Found on the way, filed as #141: rig_https_local.py has failed on every UCI image since #135 — it sends the listener's IP as the hostname and the SAN check correctly rejects it. Master fails identically (control run); not a §13 effect.

    Not done here, by design: P6 (build_x25519.sh member drop) stays gated on the sibling linking; net_print_ip/net_banner_str — the banner string stays a documented c64-https extension in net_abi.inc rather than a contract symbol.

  10. added a commit that references this issue on Aug 27, 2026
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