Closed
Description
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
- 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 tonet_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 makehttp.s(which currently does raw masked
reads) consistent with the choice.
- Delete the
net_tcp_set_recv_cbstub (RTS, zero callers) from both
adapters and the ABI header — §13.1 retires it from the contract. - Adopt
NET_TCP_*state names (NET_TCP_CLOSED/CONNECTED/ERROR/ CONNECT_FAIL) as the primary names; keepUCI_TCP_*as aliases. Values
are unchanged (§13.1 pins the existing values as normative). - Ship
src/net/<backend>/net_manifest.sexporting
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). - Ship
src/net_abi_asserts.s(§13.8): core-family assert, required-
families assert, ring-mask power-of-two assert. - Error-namespace audit (§13.2): UCI codes
$81-$89are grandfathered;
confirm no ip65-side code collides into$80-$BFand 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
Metadata
Metadata
Assignees
Labels
No labels
Activity
JC-000 commentedon Aug 14, 2026
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.mdonmain, not tags — main is now v0.8.0 (2026-08-14). The newesttag is still v0.4.0, and
CLAUDE.md's "main is v0.7.2 (checked 2026-08-13)" is oneMINOR 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 fourbelieved-but-unenforced facts about
libs/nistcurvesinto link-time errors:LIB_ABI_VERSION = 0— exported-surface generation gate(LIB_NISTCURVES_REU_BANKS_USED & $C0) = 0— the library must not claim REU banks 6/7, whichsrc/boot.s:840-841uses for the P-384 overlay imagesLIB_NISTCURVES_SHARED_PRIMITIVES = $0007tripwire (see item P2 for why the contract's assert is not writable yet)TCP_RECV_MASKis2^n - 1(assemble-time;TCP_RECV_MASKis a local equate, not an import)The PRG is byte-identical before and after (ip65 sha256
417c7085…), which isthe point: nothing moves, the linker just gains four opinions.
Negative controls, both reverted afterwards:
Three upstream defects found while writing it
SPEC §1's consumer-side snippet cannot assemble. It gates with
.ifon an.imported symbol: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_FAMILIESis.imported, soits 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.
nistcurvesv0.6.0 exportsLIB_VERSION_MAJOR/MINOR/PATCHwithout:abs.Values fit in a byte → ca65 infers
zeropage, a consumer.importdefaults toabsolute, and every link emits
ld65: Warning: Address size mismatchper 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 theshipped 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 … : zeropagepins a constant to an address sizethat is an artifact of its current value). A
>= v0.9.0pin makes the floorassert free — add it in the same PR as the bump.
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-verifyinstead.CRYPTO_HOTis16,384, so the spec's own
.assert RESIDENT_BYTES < __CRYPTO_HOT_SIZE__failsagainst 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.incis not part of the build. At all.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 findingis possible.
D2. The ip65 backend does not provide half of the declared surface
src/net_abi.inc:8-22declares 12 symbols.src/net/ip65/net.s:29-40exports thesesix 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:28says soin 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 tellsc64-wireguardtorename away from. The UCI adapter exports both (
net.s:30andnet.s:42), so thedivergence §13 was written to stop also exists inside this repo, between our own two
backends.
net_last_errordoes not exist under ip65 in any form. So issue item 6 ("confirmno ip65-side code collides into
$80-$BF") resolves trivially and unhelpfully: thereare 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-18allocates$81-$89,inside §13.2's
$80-$BFUCI-family range, andUCI_TCP_*at:21-24are$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)
Eleven symbols. Five of them (
net_dhcp,net_print_ip,net_send_len,net_recv_byte,net_banner_str) are absent fromnet_abi.inc; six ofnet_abi.inc's are imported by nobody. The declared surface and the used surfaceoverlap in six symbols out of seventeen.
net_print_ipandnet_banner_strare interesting: §13.1 listsnet_print_ipunder"deliberately not in the contract" as a consumer UI helper — yet
src/boot.s:110imports 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) consistentwith the choice" of drain entry point. That is stale: the #72 dual-source redesign
already made the choice.
src/http.s:44-46records dropping thetcp_recv_head/tcp_recv_tailimports, andsrc/http.s:279is now a barejmp net_recv_byte. Sonet_recv_ready/net_recv_byteare de facto the blesseddrain entry points across every consumer TU; item 1 reduces to declaring them.
D4. §8.0 — we are the
APP_OWNEDcase, and the manifest currently liesc64-https provides all three §8 shared primitives itself:
sqtabsrc/crypto/poly1305.s:21(sqtab_init),src/crypto/shared/mul_tables.s:32(mul_tables_init)reu_mulsrc/boot.s:31(reu_mul_init),src/data.s:123-124(mul_dma_lo/hi)ct_mul_8x8src/crypto/poly1305.s:213(ct_mul_8x8),:24(mul_8x8alias)so
APP_OWNED = $0007. But the linked library still exportsLIB_NISTCURVES_SHARED_PRIMITIVES = $0007— measured on both the REU andFP_ONCHIP_MULarchives:The contract's disjointness assert
(LIB_A & APP_OWNED) = 0would 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:130dropsmul_8x8.oanddata_shared.o— rather than by the contract'sSHARED_*deferralswitches. Member surgery is invisible to
lib_manifest.o, so the §5 mask describesthe 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.owith-Doverrides (:99-118). See P2.D5.
build_x25519.shhand-dropslib_version.otools/integration/build_x25519.sh:174deletes the member; contract v0.7.0 providesca65 -D LIB_NO_BARE_EXPORTS=1as the supported mechanism. Low priority: theduplicate-identifier trap the workaround exists for is currently unreachable —
USE_X25519_SIBLING=1does not link on either backend, so exactly one contractlibrary is ever in a link, which is also why importing the bare
LIB_ABI_VERSIONabove 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.incbecomes real, or is deleted. (prereq for all §13 work)Pick one: either every TLS/HTTP/boot TU
.includes it and the ip65 adapter grows thesix 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].oinbuild_nistcurves_p256.shwith the deferralswitches 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 existingzp_config.orebuild.
libs/nistcurves/src/lib_manifest.s:216-234already implements the §8.0conditional form, and
LIB_NISTCURVES_SHARED_PRIMITIVESis itself.ifndef-guardedat
:232, so-D LIB_NISTCURVES_SHARED_PRIMITIVES=$0000is an even blunteralternative. Then replace the tripwire in
src/lib_contract_asserts.swith thecontract's real assert. Per-profile accuracy needs a look — under
FP_ONCHIP_MULthelibrary still supplies
reu_fetch_mul_rowwhile boot supplies the init, which §8.0'sthree-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_ipandnet_banner_strare 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 incfg/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 = $100on a page-aligned LUT only warns, andtype = rw→type = bssemits 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_FAMILIESfor ip65cannot claim
NET_FAMILY_COREwhile the backend lacksnet_local_ipandnet_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 ownexample, because it was written from this blob.
P6 — retire the
build_x25519.shmember drop (D5). Bundle with whatever PR firstmakes the x25519 sibling link.
What this does not touch
No behavioural change, no cfg change, no library bump, no hardware run.
libs/andcfg/*.cfgare being changed concurrently by the library-bump lanes; the assert fileis deliberately the only new artifact so those merges stay clean.
docs: net_abi.inc is documentation, not an enforced interface (#70)
JC-000 commentedon Aug 14, 2026
Two follow-ups to the audit above.
1. The
net_abi.incfinding is now corrected in-repo986b22don the #92 branch.CLAUDE.mdstated: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 itdescribed 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 actuallyexports.
src/net/ip65/README.md— same claim, plus a stale "Port ofsrc/net.asmlandshere 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…, UCI741f0e8c…).The three facts, restated so they are findable from this issue:
grep -rn 'net_abi' src/ tools/ cfg/ Makefile→ comments only. None of the twelve.imports is checked by ca65 or ld65.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.net_dhcp_acquire,net_tcp_set_recv_cb,net_local_ip,net_resolved_ip,net_last_error,net_tcp_state.net_last_errorexists 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:
JC-000/c64-lib-contract#73
— §1's consumer-side version-guard snippets cannot assemble. Stronger than reported
above: both snippets fail, including the ABI-generation gate that §7 calls "the
load-bearing breakage gate". Copied verbatim from
main(v0.8.0), ca65 V2.18 givesError: Constant expression expectedon each, exit 1. Filed with the.assert/lderrorreplacement, verified to link clean on agreement and to firewith a named error on a seeded disagreement, plus a note that
.if MAJOR < 1 .and MINOR < 8is independently wrong (it accepts 1.0, and per contract feat(crypto): c64-x25519 v0.4.0 optional sibling integration (Phase C.5) #41.andisboolean not bitwise). Also flags that §1 and §13 contradict each other on this and
that §13 is the one that runs.
JC-000/c64-lib-contract#74
— §1's
src/lib_version.sPattern block omits the: absexport hint, so acompliant library plus a compliant consumer emits one
ld65: Warning: Address size mismatchper imported version equate. This is contract feat(uci): C64 Ultimate support — 64 MHz e2e + WiFi DHCP + fence retune #58's v0.7.4 fix recurringin a clause it did not cover. Filed with a three-warning reproduction from the spec's
own Pattern block, the silent link after adding
: abs, and the negative controlshowing the hint costs nothing. Notes that
c64-nist-curvesalready fixed this atv0.9.0 independently, so the spec is behind an adopter and a new adopter copying
§1 would reintroduce it — and that this is precisely why chore(contract): link-enforced c64-lib-contract asserts (#70) #92 ships no MINOR floor
assert.
JC-000/c64-lib-contract#62 (comment)
— the §5
RESIDENT_BYTESfinding, filed as a comment rather than a new issuebecause it is the same root cause as feat(crypto): comb-accelerated onchip profile — 31.0 s handshake @64 MHz #62 (manifests not built per variant) in a
field feat(crypto): comb-accelerated onchip profile — 31.0 s handshake @64 MHz #62's three cases do not cover.
RESIDENT_BYTES= 27,000 (REU) / 28,200(onchip) describes the whole library while we link a §6 minimal variant into a
16,384 B
CRYPTO_HOT, so §5's own worked example fails against a conformantconsumer. Argues fix(crypto): REU-less X25519 under USE_NISTCURVES_ONCHIP — fe_mul rows via og_common #69 should be gated on feat(crypto): comb-accelerated onchip profile — 31.0 s handshake @64 MHz #62, since a declared-footprint mechanism
inherits the defect.
Not filed separately: the §8.0 manifest-vs-member-surgery finding, which is already
#72 — same measurement, same
build_nistcurves_p256.sh:130line, already written up there.Plan items P1-P6 above are unchanged.
Merge pull request #92 from JC-000/chore/contract-alignment
JC-000 commentedon Aug 15, 2026
§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
master3a43f61, ca65/ld65 V2.18, pinslibs/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
mainis now v0.10.0 (2026-08-15), eleven versions past the v0.8.0 this repo was aligned to. Butgit diff 2d9429d..edf8dcb -- SPEC.mdtouches 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 tof4af347/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:
f4af347); v0.5.0 is theSHARED_CONSUMESrelease. Minor, but the body is what people pin against.2. The
net_abi.incmeasurement re-verified at HEADThe earlier finding (nothing
.includes it; declared and used surfaces overlap in 6 of 17) is exact and unchanged atmaster3a43f61. 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.net_init,net_poll,net_dns_resolve,net_tcp_connect,net_tcp_send,net_tcp_close). Union 17.net_banner_str,net_dhcp,net_print_ip,net_recv_byte,net_send_len.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:net_dhcpnet_dhcp_acquirenet_last_error/net_tcp_state/net_local_ip/net_resolved_ipnet_recv_ready/net_tcp_recv_cbnet_tcp_set_recv_cbSo 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_errorat 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 neithernet_last_errornornet_dhcp_acquirenornet_local_ip— three of the five core-family symbols.src/net/ip65/net.s:28calls 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-$7Fcode allocation per §13.2.Consequence for item 4 as written: ip65 cannot honestly export
NET_FAMILY_COREtoday. 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_overflowexists (src/data.s:160-163) and ip65 imports and sets it (src/net/ip65/net.s:46). The UCI adapter importstcp_recv_head/tcp_recv_tail(net.s:71-72) but nottcp_recv_overflow, and its ring-full path (net.s:263-278) is:— 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.2CONTRACT_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 futureLIB_NET_IP65_BLOB_SIZEfit check againstNET_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.
tcp_recv_overflowon ring-full (§13.3). ~2 instructions.net_tcp_set_recv_cbRTS stub from the UCI adapter and the header (§13.1 retires it; zero callers, and ip65 never had it).$81-$89grandfathered; 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.inca 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. Notenet_print_ipis explicitly not backend surface per §13.1 ("a consumer UI helper") — it should move consumer-side rather than be declared. Pick one drain model fornet_recv_ready/net_recv_bytevshttp.s's raw masked reads and make it consistent.P2 — ip65 core family (3a).
net_last_error+$40-$7Fcodes,net_dhcp_acquire(renaming fromnet_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-backendnet_manifest.sexportingNET_BACKEND_FAMILIES. ip65 also gets the §13.7 blob equates. Only honest after P2.P4 — asserts (body item 5).
src/net_abi_asserts.sper §13.8. Note the ring-mask assert of that trio already ships —src/lib_contract_asserts.s:222carries.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 withUCI_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
JC-000 commentedon Aug 16, 2026
Re-checked at
master39a43eb against SPEC v0.10.6 — still fully relevant; nothing here has been overtaken. Item-by-item, measured rather than recalled:net_abi.incnet_send_lenandnet_recv_byteare declared;tcp_recv_buf,TCP_RECV_MASK,tcp_recv_head,tcp_recv_tail,tcp_recv_overflowandnet_recv_readyare all still missingnet_tcp_set_recv_cbstubsrc/net/ip65/net.s,src/net/uci/net.sandsrc/net_abi.incNET_TCP_*state namesNET_TCP_*symbol anywhere insrc/net_manifest.s/NET_BACKEND_FAMILIESlib_contract_asserts.snet_abi_asserts.sAnd 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_byteas blessed drain entry points versus documented raw ring access, withhttp.scurrently doing raw masked reads. That choice should be made before the declarations are written, not after.JC-000 commentedon Aug 24, 2026
Coordination note with c64-wireguard#48 — 2026-08-24
Re-measured both sides at c64-https master
ab5de2band the c64-wireguard checkout atv1.0.0-34-gdfe12bd(2026-08-24), against c64-lib-contractorigin/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.mdtouches 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:
net_abi.inc.included by a TUsrc/boot.s)UCI_FENCE_INNERUCI_ERR_NO_SOCKET$88/UCI_ERR_WAIT_TIMEOUT$89uci_cmd.snet_last_error/net_dhcp_acquire/net_local_ipSo 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.increal; ip65 cannot honestly claimNET_FAMILY_COREwithout an error channel).3. Two cross-repo items not in either issue
3a.
$8A UCI_ERR_LONG_READ— namespace reservation. c64-wireguard'ssrc/net/uci/uci_errors.inc:28allocatesUCI_ERR_LONG_READ = $8A("SOCKET_READ claimed more bytes than we asked for"). §13.2 grandfathers$80-$BFas one UCI-family namespace shared by every consumer; c64-https'suci_errors.incstops at$89. Proposal: c64-https reserves$8Awith the same name and meaning (ournet_pollhas 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_pollring-drop fix has not travelled. c64-httpsd9cd021found that the UCI adapter requested a fixed 512 B perSOCKET_READwhile 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 tomin(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 inboundSOCKET_READcap): 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
net_abi.incbecomes real; ip65 growsnet_last_error+net_dhcp_acquire+net_local_ip, codes from$40-$7F). No cross-repo dependency.src/net/net_families.incheader and the$8Areservation (3a) — one small PR each, can land in either order.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.
JC-000 commentedon Aug 24, 2026
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 itAgreed, with the history since it explains why the value is
$8Aand not something tidier.UCI_ERR_LONG_READwas allocated at$88by wg PR #62 (the unbounded-read fix:net_polltrustedSOCKET_READ's returned length and copied ~18 KB fromudp_recv_bufthrough$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, soLONG_READmoved to$8Aand$88/$89now match c64-https exactly.Worth noting for the contract discussion:
$88as we had it was conformant with §13.2 as written. That clause reserves the$80-$BFrange and says "a backend MUST emit only$00or 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-$87and silently disagreeing on$88is 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-$89shared,$8AUCI_ERR_LONG_READ, plus the rule that the next allocation in either repo is filed against the contract first. Reserving$8Abilaterally 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.
d9cd021is 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 pasttcp_recv_head/tcp_recv_tailfree 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 byudp_recv_ready(§13.3's UDP shape, not the TCP-ring shape). There is no wrap, no free-space computation, and noTCP_RECV_MASK— the consumer drains the whole datagram before the next poll is allowed to deliver. Somin(ring_free - 1, 512)has nothing to clamp against.The transfer runs the other way here. c64-wireguard's
net_pollvalidates the firmware's claimed length before trusting it (src/net/uci/net.s, the@hdr_doneblock): ifactual_lenexceedsUCI_READ_CHUNK_MAXit drops the datagram rather than trimming it, and reports$8A. c64-https takes the response header straight intouci_poll_remand copies, surviving a bogus length only incidentally because the inner loop re-checks ring-full andDATA_AVper byte. Given this firmware returns0xFFFFfor 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.pyon U64E fw 3.14d today. This settles the open question behind wg#46, which had been asserted but never measured:net_pollThe 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_MTUis pinned at 480 (512 - 32for the Type-4 header and Poly1305 tag), enforced by an assembly-time assert againstUCI_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:
NET_BACKEND_FAMILIESmanifest honestly while ip65 has no error channel, so aNET_FAMILY_COREclaim would be green over an absent surface. Agreed that manifests come last.src/net/net_families.inc: note it is a v0.6.1 addition that post-dates both issue texts — the fourNET_FAMILY_*constants must live in that one header, be copied verbatim, and be never.exported (onlyNET_BACKEND_FAMILIESis exported; unexported constants cannot collide at link). The v0.6.1 change also moved §13.0's consumer assert fromerrortolderror, sinceNET_BACKEND_FAMILIESis 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.0againstorigin/mainand the chapter is byte-identical, so v0.11.1's §6.3-only delta is irrelevant to this work. Recommend both repos pinv0.11.0rather than trackingmain, since v0.11.1 is untagged.Cross-referenced from c64-wireguard#48.
JC-000 commentedon Aug 24, 2026
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:
net_poll1024delivers 512, not nothing. My earlier1024 → 0was 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 known0xFFFF-for-1500 quirk).The conclusion is unchanged and now better supported.
2nd?was0in all ten oversized trials — the remainder is discarded, never queued. Multi-read reassembly is impossible;WG_MTU = 480is 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 Tfrom 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.JC-000 commentedon Aug 24, 2026
Resolution — 2026-08-24
c64-https PR #142 lands the alignment (closes this issue on merge):
src/net_abi.incis now the build-enforced boundary:boot.s,http.s,tls_record_io.s,tls13.s.includeit and import nonet_*directly. Both backends export the full core/TCP/DNS surface or the link fails by name.net_dhcp_acquire,net_local_ip,net_resolved_ip,net_last_error($41-$45, the §13.2 ip65 range) andnet_tcp_state. LOADER ends with more room than before (58 B vs 16) becausenet_print_ip(→ consumer-sideprint_local_ip) andnet_recv_readyleft the adapter.net_tcp_set_recv_cbdeleted;NET_TCP_*primary (byte-neutral, aliases kept);net_manifest.sper backend (ip65 with §13.7 blob equates link-asserted against the.incbinspan);net_abi_asserts.s; error namespaces audited — ip65 had none and now has$41-$45, UCI reserves c64-wireguard's$8A.tcp_recv_overflowon ring-full.boot_check/phase2_checkPASS (net_local_ipread back over DMA vianet_dhcp_acquire),rig_https_livegithub.com HTTP 200.c64-lib-contract PR #139 (v0.12.0 draft): §13.2 allocation table (ip65
$41-$45, UCI$81-$8Aincl. wireguard's$8A) + allocate-here-first rule; canonicalnet_families.incat the repo root; §13.8 records c64-https as aligned.Found on the way, filed as #141:
rig_https_local.pyhas 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.shmember drop) stays gated on the sibling linking;net_print_ip/net_banner_str— the banner string stays a documented c64-https extension innet_abi.incrather than a contract symbol.Merge pull request #142 from JC-000/feat/70-net-abi-s13