From e3ef15e472d02f442491d1e14f5b93267af9b9e0 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:48:30 -0500 Subject: [PATCH 1/3] fix(build): stop set -e from eating this script's own diagnostics (#217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four `set -eo pipefail` swallow sites in tools/integration/build_nistcurves_p256.sh, each one killing the shell inside the error path written for exactly that condition, so the caller gets a bare non-zero exit and no message. 1. `lib_preflight_fail`'s `pin=` line (the reported #217 defect) was the only one of three adjacent `git` calls without `|| true`. A PROJECT_ROOT that is not a git repository makes `git ls-tree` exit 128, pipefail carries it through `awk`, and the shell dies two lines before the `cat >&2` heredoc — in exactly the context (`tools/test_build_flags_stamp.py`'s tempdir "Farm" tree, which has no `.git`) where the #124 remedy message is most useful. RED, Farm-shaped tree (non-repo root, libs -> uninitialised submodule): $ bash /tools/integration/build_nistcurves_p256.sh onchip exit=128 <- no output whatsoever $ cd && git ls-tree HEAD libs/nistcurves fatal: not a git repository (or any of the parent directories): .git ls-tree rc=128 GREEN: the full #124 message, exit=1. 2. `ZP_MEMBER="$( ar65 t ... | grep '^zp_config' )"` — same shape, worse consequence: with no matching member `grep` exits 1, the assignment dies, and the `0)` arm that reports it never runs. A silently dropped zp_config member is the #124 runtime-corruption class (zp_ptr2 reverts to $fd and collides with zp_temp/zp_count, no link error). RED (fake ar65 filtering `^zp_config` out of the listing): $ AR65= bash tools/integration/build_nistcurves_p256.sh onchip [p256/onchip] building libs/nistcurves lib-p256-verify-onchip ... exit=1 <- the 0) arm never printed GREEN: ERROR: no zp_config member in nistcurves-p256-verify-onchip.a exit=1 3. `check_zp_slot`'s `got=$(od65 ... | awk ...)` — a failing od65 kills the script and the `${got:-}` empty case is unreachable. RED (OD65 -> a stub that exits 3): $ OD65= bash tools/integration/build_nistcurves_p256.sh onchip exit=3 <- no message GREEN: ERROR: zp_config_p256verify.o exports nistcurves_zp_ptr2 = , expected 0x0000003D (CONTRACT_ZP_DEFINES did not take) exit=1 4. With `pin` empty, the diagnosis branches then asserted a WRONG cause. A non-repo root whose libs/nistcurves is populated (a symlink into a real checkout, as the test farms use) leaves head non-empty and pin empty, so `[ "$head" != "$pin" ]` is trivially true and the message blamed a stale submodule and prescribed a `git submodule update` that cannot run there. An empty pin now gets its own branch, first, with its own remedy text. RED (stub repo at libs/nistcurves without src/zp_config.s, non-repo root): submodule checkout : 88a12b5 this repo pins : The submodule working tree is NOT the commit this repo pins. Fix it with: git submodule update --init --recursive GREEN: This repo's gitlink for libs/nistcurves could not be read, so there is nothing to compare the checkout against: either is not a git checkout, or its HEAD carries no libs/nistcurves gitlink. Fix it wherever this tree's libs/nistcurves comes from, with: git submodule update --init --recursive Running that against THIS root will not help: there is no gitlink here to update, whatever the reason. The new branch is narrowed to what was actually tested. An earlier draft blamed a non-repo root outright; that over-claims, because a REAL checkout whose HEAD carries no libs/nistcurves gitlink reaches the same branch with `.git` present — constructed and confirmed (a git repo containing only tools/, with libs/nistcurves a stub checkout): this repo pins : ... either is not a git checkout, or its HEAD carries no libs/nistcurves gitlink. And where BOTH conditions hold (the #217 Farm shape: non-repo root, empty submodule directory) the message now states both, so the original issue's case keeps its most actionable sentence: submodule checkout : this repo pins : ... either is not a git checkout, or its HEAD carries no libs/nistcurves gitlink. The submodule is not checked out at all, either. Also fixes the literal `\n` in the zp_config.s call-site string — the one site using `\n` inside a `cat < --- tools/integration/build_nistcurves_p256.sh | 74 ++++++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/tools/integration/build_nistcurves_p256.sh b/tools/integration/build_nistcurves_p256.sh index 3f8b32b..32b0912 100755 --- a/tools/integration/build_nistcurves_p256.sh +++ b/tools/integration/build_nistcurves_p256.sh @@ -151,7 +151,13 @@ lib_preflight_fail() { local what="$1" local head; head="$(git -C "$LIB_DIR" rev-parse HEAD 2>/dev/null || true)" local found; found="$(git -C "$LIB_DIR" describe --tags --always 2>/dev/null || echo '')" - local pin; pin="$(git -C "$PROJECT_ROOT" ls-tree HEAD libs/nistcurves 2>/dev/null | awk '{print $3}')" + # `|| true` is load-bearing, exactly as on the two lines above: under + # `set -eo pipefail` a non-repo PROJECT_ROOT makes `git ls-tree` exit 128, + # pipefail carries that through `awk`, and the shell dies here — killing + # the very diagnostic this function exists to print (#217). The caller + # then sees a bare `Error 128`. An empty `pin` is a state the message + # below already handles. + local pin; pin="$(git -C "$PROJECT_ROOT" ls-tree HEAD libs/nistcurves 2>/dev/null | awk '{print $3}' || true)" local diagnosis # The tag for the PINNED sha, resolved rather than hardcoded: this line # used to carry a literal "(v0.11.2)" that had to be edited by hand at @@ -160,7 +166,29 @@ lib_preflight_fail() { # the checkout cannot resolve it (which is itself one of the states this # message is printed in), and the sha still identifies the pin. local pintag; pintag="$(git -C "$LIB_DIR" describe --tags --exact-match "$pin" 2>/dev/null || true)" - if [ -z "$head" ]; then + # An empty $pin gets its own branch, and it must come FIRST. Without + # it, a non-repo PROJECT_ROOT whose libs/nistcurves IS populated (a + # symlink into a real checkout, as the test farms use) leaves head + # non-empty and pin empty, so `[ "$head" != "$pin" ]` is trivially true + # and the message blames a stale submodule and prescribes a + # `git submodule update` that cannot run there. Say what is actually + # unknown instead — and only that. The reachable shapes are not just a + # non-repo root: a real checkout whose HEAD has no libs/nistcurves + # gitlink reaches this branch too (an unborn HEAD, a commit predating + # the submodule, or another repo entirely as PROJECT_ROOT), and there + # `.git` is present. Naming a cause we did not establish is the defect + # this branch exists to fix, one layer up. + local remedy="submodule-update" + if [ -z "$pin" ]; then + remedy="no-gitlink" + diagnosis="This repo's gitlink for libs/nistcurves could not be read, so there is +nothing to compare the checkout against: either $PROJECT_ROOT is not a git +checkout, or its HEAD carries no libs/nistcurves gitlink." + # Independent condition, and the actionable one in the #217 Farm + # case where both are true. Do not let the pin branch swallow it. + [ -z "$head" ] && diagnosis="$diagnosis +The submodule is not checked out at all, either." + elif [ -z "$head" ]; then diagnosis="The submodule is not checked out at all." elif [ "$head" != "$pin" ]; then diagnosis="The submodule working tree is NOT the commit this repo pins." @@ -180,7 +208,20 @@ ERROR: libs/nistcurves checkout is unusable for this build (c64-https#124). submodule checkout : $found this repo pins : ${pin:0:12}${pintag:+ ($pintag)} -$diagnosis Fix it with: +$diagnosis +EOF + if [ "$remedy" = "no-gitlink" ]; then + cat >&2 <&2 <&2; exit 1 ;; @@ -397,8 +456,11 @@ esac check_zp_slot() { local name="$1" want="$2" got + # `|| true`: without it a failing od65 kills the script under + # `set -eo pipefail` and the `${got:-}` empty case below — the + # message written for precisely that state — is unreachable (#217). got=$("${OD65:-od65}" --dump-exports "$LIB_BUILD/$ZP_MEMBER" \ - | awk -v n="\"$name\"" '$1=="Name:" && $2==n {f=1; next} f && $1=="Value:" {print $2; exit}') + | awk -v n="\"$name\"" '$1=="Name:" && $2==n {f=1; next} f && $1=="Value:" {print $2; exit}' || true) if [ "$got" != "$want" ]; then echo "ERROR: $ZP_MEMBER exports $name = ${got:-}, expected $want (CONTRACT_ZP_DEFINES did not take)" >&2 exit 1 From efe2dc275af09dcb517542821c25ccbba3921307 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:48:46 -0500 Subject: [PATCH 2/3] docs(test): describe the unarmed build the P-384 test runs on (#208) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/test_tls_p384_negotiation.py attributed sub-test [1b]'s C=1 to "ecdsa_verify_384_tls's DER parse rejecting the 48-zero-byte dummy signature". That is the ENABLE_P384_VERIFY=1 mechanism. In the build the test runs against the flag is off, ecdsa_verify's P-384 arm is a bare `sec`, and ecdsa_verify_384_tls is neither called nor linked. Confirmed from artifacts, not reworded: - src/crypto/ecdsa_verify.s: the arm is .ifdef ENABLE_P384_VERIFY jsr ecdsa_verify_384_tls .else sec ; C=1 — unsupported curve, clean reject .endif jmp @done - Makefile:388-393 — P384_SRCS (ecdsa_verify_384.s, p384_force_link.s) are `filter-out`ed from CRYPTO_SRCS_EFFECTIVE unless the flag is 1, so the object never reaches the link line. - In the very PRG this run tested (default ip65 build), ecdsa_verify is at $6B41 and reads 20 95 3B AD 7F B6 F0 04 38 4C 54 6B jsr lda beq sec jmp @done and `ecdsa_verify_384_tls` appears 0 times in build/c64-https.map. - Same shape on BACKEND=uci USE_NISTCURVES_ONCHIP_COMB=1 ($69C8: 20 81 28 AD 7F B0 F0 04 38 4C DB 69). All eight stale sites in the file are corrected, not just the two [1b] docstrings: - :11-13 module docstring said the branch is "a `sec / rts` stub that Phase 4a fills in" — Phase 4a landed and is gated, not pending. - :26-40 [1b] summary docstring (the reported site). - :250-258 test_cert_verify_p384_dispatch docstring (the reported site). - :265 "Phase 4a's dispatcher composes ..." now says "when armed". - :335-343 the FAIL-branch comment and its message string "(DER rejection / stub rejection)" — the retracted mechanism, sitting in the branch a reader reaches when the test FAILS. Now "(gated-off P-384 arm should `sec`)". - :348 PASS string said "Phase 4a dispatcher reached"; the dispatcher reached is ecdsa_verify, so it now says so. - :294 "value irrelevant under the stub" — the same retired vocabulary; the 48 dummy bytes are never read because the gated-off arm rejects on curve_id before any signature parse. - :6-8 pre-existing, inside the docstring this commit edits: "This file verifies\nalongside ecdsa_secp256r1_sha256 (0x0403) in the ClientHello" had lost the clause naming what is verified, leaving the sentence without an object. Restored to say the ClientHello advertises 0x0403 and NOT 0x0503, which is what [1a] asserts. Byte-count correction from review: the earlier wording called the arm "three bytes". `38 4C 54 6B` is four, and the gated arm proper is one (`38`) — the `jmp @done` is shared with the P-256 path. Reworded to say that rather than count. src/tls_cert.s:670-674 carries the same stale mechanism. Left out because this is a docs/test commit and that is source — not for any cost reason; an earlier draft argued one and it did not survive checking. Follow-up. Test after the edits: $ python3 tools/test_tls_p384_negotiation.py [1a] PASS: schemes advertised = 0x0403 (0x0503 correctly absent) [1b] PASS: cv_sig_scheme=1, ecdsa_curve_id=1, C=1 (ecdsa_verify dispatcher reached) Passed: 2/2 Failed: 0/2 Comments and strings only; all five profiles byte-identical to master. Co-Authored-By: Claude Opus 5 (1M context) --- tools/test_tls_p384_negotiation.py | 74 +++++++++++++++++++----------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/tools/test_tls_p384_negotiation.py b/tools/test_tls_p384_negotiation.py index faaac63..9177156 100644 --- a/tools/test_tls_p384_negotiation.py +++ b/tools/test_tls_p384_negotiation.py @@ -3,14 +3,16 @@ Phase 4b originally verified that the TLS layer OFFERS ecdsa_secp384r1_sha384 (0x0503). That assertion is inverted now: advertising 0x0503 was the bug, since -the client answers it destructively. This file verifies -alongside ecdsa_secp256r1_sha256 (0x0403) in the ClientHello, and that the +the client answers it destructively. This file verifies that the ClientHello +advertises ecdsa_secp256r1_sha256 (0x0403) and NOT 0x0503, and that the CertificateVerify handler accepts a 0x0503 signature_scheme by routing through the ecdsa_verify dispatcher with curve_id=1. -This test does NOT require a successful P-384 verification; the -ecdsa_verify dispatcher's P-384 branch is still a `sec / rts` stub that -Phase 4a fills in. Successful negotiation = the carry-set return came +This test does NOT require a successful P-384 verification. Phase 4a's +dispatcher landed but is gated off: with ENABLE_P384_VERIFY unset (the +default, and the only configuration anyone ships) ecdsa_verify's P-384 +branch is a bare `sec`, and src/crypto/ecdsa_verify_384.s is filtered out +of the link. Successful negotiation = the carry-set return came out of the dispatcher (curve_id was set to 1, cv_sig_scheme was set to 1, the routine entered the short-circuit branch). @@ -21,14 +23,22 @@ [1b] tls_handle_cert_verify with a synthesized CertificateVerify handshake message whose signature_scheme = 0x0503 sets cv_sig_scheme = 1, ecdsa_curve_id = 1, and returns C=1. - Pre-Phase-4a this came from the `sec / rts` stub in - ecdsa_verify; post-Phase-4a (commit-this-PR) it comes from - ecdsa_verify_384_tls's DER parse rejecting the 48-zero-byte - dummy signature (first byte must be 0x30 SEQUENCE; rejection - still propagates C=1). The negotiation contract under test - (cv_sig_scheme=1, ecdsa_curve_id=1, dispatcher reached) is - unchanged. Phase 5 will replace this synthetic test with a - real-signature test once a SHA-384 transcript path lands. + In the build this test actually runs against, that C=1 comes + from the `sec` in ecdsa_verify's gated-off P-384 arm -- + ENABLE_P384_VERIFY is off by default, so the arm assembles as + a single `sec` (38) followed by the shared `jmp @done`, with no + call anywhere in it, and the Makefile + filters src/crypto/ecdsa_verify_384.s out of the link + entirely. ecdsa_verify_384_tls is never called, and appears + in no shipped PRG's Imports list. Only under + ENABLE_P384_VERIFY=1 does the arm become + `jsr ecdsa_verify_384_tls`, where the C=1 would instead come + from that routine's DER parse rejecting the 48-zero-byte dummy + signature (first byte must be 0x30 SEQUENCE). Either way the + negotiation contract under test (cv_sig_scheme=1, + ecdsa_curve_id=1, dispatcher reached) is unchanged. Phase 5 + will replace this synthetic test with a real-signature test + once a SHA-384 transcript path lands. Usage: /Users/someone/.local/share/c64-test-harness/venv/bin/python \\ @@ -236,17 +246,24 @@ def test_cert_verify_p384_dispatch(transport, labels): signature_scheme = 0x0503, calls tls_handle_cert_verify, and asserts: - cv_sig_scheme = 1 - ecdsa_curve_id = 1 - - C=1 (carry set). Post-Phase-4a this comes from the dispatcher's - DER parse rejecting the 48-byte all-zero dummy signature (the - first byte must be the DER SEQUENCE tag 0x30); pre-Phase-4a it - came from the `sec / rts` stub in ecdsa_verify. Either path - proves cv_sig_scheme=1, ecdsa_curve_id=1, and dispatcher - reachability -- the contract this subtest exercises. + - C=1 (carry set). In the unarmed build this test runs against, + that carry is set by the `sec` in ecdsa_verify's P-384 arm, + which is what `.ifdef ENABLE_P384_VERIFY` compiles to when the + flag is off (the default); ecdsa_verify_384_tls is not called + and src/crypto/ecdsa_verify_384.s is not even linked. Under + ENABLE_P384_VERIFY=1 the arm becomes + `jsr ecdsa_verify_384_tls` and the C=1 would come from that + routine's DER parse rejecting the 48-byte all-zero dummy + signature (first byte must be the DER SEQUENCE tag 0x30). + Either path proves cv_sig_scheme=1, ecdsa_curve_id=1, and + dispatcher reachability -- the contract this subtest + exercises. The signature payload itself is irrelevant for the negotiation plumbing under test -- a real-signature P-384 verify needs both a real ECDSA-P384 cert + signature AND a SHA-384 transcript hash - (Phase 5). Phase 4a's dispatcher composes the dual-overlay swap + (Phase 5). When armed, Phase 4a's dispatcher composes the + dual-overlay swap (sha384 -> curve) + sibling ecdsa_verify_384, but the SHA-384 transcript source is a 32 B SHA-256 placeholder zero-padded to 48 B until Phase 5 wires up tls_transcript_384. @@ -273,7 +290,8 @@ def test_cert_verify_p384_dispatch(transport, labels): # [4..5] signature_scheme = 0x0503 # [6..7] signature length (16-bit BE; high byte must be 0) # [8..] signature bytes (untouched by the P-384 short-circuit) - sig = bytes(48) # 48 dummy bytes — value irrelevant under the stub + sig = bytes(48) # 48 dummy bytes — never read: the gated-off arm + # rejects on curve_id alone, before any signature parse msg = bytearray() msg.append(0x0F) # handshake type msg.extend(b"\x00\x00\x00") # 24-bit length placeholder @@ -316,17 +334,19 @@ def test_cert_verify_p384_dispatch(transport, labels): print(f" FAIL: ecdsa_curve_id = {curve_id:#x}, expected 0x01") ok = False if carry != 1: - # Phase 4a's dispatcher should also reject a 48-zero-byte sig at - # the DER parse step (first byte must be 0x30 SEQUENCE). Phase 5 - # will replace this with a real-signature test once SHA-384 - # transcript wiring lands. + # C=1 is set by the `sec` in ecdsa_verify's gated-off P-384 arm. + # If this fires, that arm was not reached or no longer sets the + # carry -- NOT a DER-parse question: ecdsa_verify_384_tls's DER + # rejection is reachable only under ENABLE_P384_VERIFY=1, which + # nothing ships. Phase 5 will replace this with a + # real-signature test once SHA-384 transcript wiring lands. print(f" FAIL: carry = {carry}, expected 1 " - f"(DER rejection / stub rejection)") + f"(gated-off P-384 arm should `sec`)") ok = False if ok: print(" PASS: cv_sig_scheme=1, ecdsa_curve_id=1, " - "C=1 (Phase 4a dispatcher reached)") + "C=1 (ecdsa_verify dispatcher reached)") return 1, 0 return 0, 1 From 7ae180425b61eb3828fe65c40817a0de76f9b5f1 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:49:14 -0500 Subject: [PATCH 3/3] docs: re-measure every post-#211 region margin CLAUDE.md still cites stale (#193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md contradicted itself on three region margins, each in the same way: #211's fix moved ~27 B into HTTP_AUX_CODE2, one paragraph was updated and the others were not. This is load-bearing, not a doc nit — tools/uci/_memory_policy.py hands rig scratch out of the comb CRYPTO_OVERLAY tail, so an over-stated figure is a silent crypto-corrupting collision, and :805 sits in the paragraph that reads as the authoritative ip65 sizing block. All measured here off build/c64-https.map, per profile, P-384 objects gated out, at the v0.14.0 pin. uci-comb (make BACKEND=uci USE_NISTCURVES_ONCHIP_COMB=1; ld65 line confirms cfg/c64-https-uci-onchip.cfg, and neither ecdsa_verify_384.o nor p384_force_link.o is on it): cfg:163 CRYPTO_OVERLAY: start = $4200, size = $1E00 -> ends $5FFF map LIB_NISTCURVES_MUL_CODE 005EE0 005F81 (last tenant; next segment UCI_BSS starts 006000, i.e. CRYPTO_HOT) $5FFF - $5F81 = $7E = 126 B free :558 already said 126 B; :560 and :755 said 153 B. Corrected. ip65 (measured on BOTH ip65 profiles — plain `make`, REU_BANKS_USED=0003, and `make USE_NISTCURVES_ONCHIP=1`, REU_BANKS_USED=0000; the two figures below are identical across them, but see the CRYPTO_RESIDENT note further down, which is NOT): cfg:90 LOADER: start = $0801, size = $17FF -> ends $1FFF map CODE 00080D 001FEE (last tenant) $1FFF - $1FEE = $11 = 17 B free :311 and :805 said 21 B; :625 said 17 B. Corrected. cfg:103 CRYPTO_OVERLAY: start = $4F8C, size = $1074 -> ends $5FFF map HTTP_AUX_CODE2 005EBF 005F82 (last tenant) $5FFF - $5F82 = $7D = 125 B free :805 said 152 B; :626 said 125 B. Corrected. Re-measured and NOT changed as figures, because they were already right: NET_CODE $2000 + $2000 -> $3FFF; HTTPS_TARGET_RODATA ends $3FC7 $3FFF - $3FC7 = $38 = 56 B (matches :804; same on both ip65 profiles) CRYPTO_RESIDENT $6000 + $4000 -> $9FFF; LIB_NISTCURVES_MUL_CODE ends $9F6E; $9FFF - $9F6E = $91 = 145 B (matches :805) CRYPTO_RESIDENT is the one figure here that is NOT profile-independent, and the paragraph carrying it named no profile: ip65 REU LIB_NISTCURVES_MUL_CODE 009EF2 009F18 ($27) -> 231 B free ip65 onchip LIB_NISTCURVES_MUL_CODE 009ECD 009F6E ($A2) -> 145 B free 145 B is the ip65-onchip value, i.e. the shipped c64-https-ip65-onchip.prg, so the number was correct and conservative but presented as universal in a file whose own maintenance note says margins are per profile. The block is now labelled ip65-onchip and states which of its figures the REU profile shares (LOADER, CRYPTO_OVERLAY, NET_CODE) and which it does not. Also corrected, same cause, lower stakes: :820 named "HTTP_AUX_CODE2's 169 B". That segment is $C4 = 196 B today (169 B was its pre-#211 size). The surrounding claim is unaffected and stands — LIB_NISTCURVES_MUL_CODE is $A2 = 162 B on ip65-onchip and is still the smallest segment in the CRYPTO_OVERLAY+CRYPTO_RESIDENT pool. :612's warning ("took ip65's LOADER from 21 B free to zero; the next byte anyone added would not link") kept its 21 B, which was correct for that episode, and now states the 17 B measured today alongside it. The 4 B difference is now measured rather than left open: rebuilding ip65-onchip with ONLY src/http.s reverted to 60022de moves http.o's CODE from $35E to $35A and the whole CODE segment from $17E2 to $17DE, i.e. CODE ends $1FEA and LOADER is back at 21 B free. So the shrink is http.o's own CODE growth in #211's fix. Attribution is to the module and the commit; nobody has disassembled which four bytes, and the file says exactly that. Also corrected, same block, same family, found in review: :828 said "LOADER_OVERFLOW carries ~125 B of http.s that outgrew LOADER". src/http.s has no `.segment "LOADER_OVERFLOW"` at all — src/http.s:1096-1097 records the eviction ("W4: moved from LOADER_OVERFLOW to HTTP_AUX_CODE"). The segment is 332 B and its measured tenants are boot.o 110 B, vic.o 18 B, crypto_swap.o 204 B ($6E + $12 + $CC = $14C). Wrong module and wrong number; the line now names the three real tenants. Prose only; all five profiles byte-identical to master. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 65 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 080a3f3..f48f067 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -308,8 +308,10 @@ drops a symbol fails the link by name on both backends. Surface: `net_dhcp` (alias), and `net_print_ip` — IP printing is consumer UI and is now `print_local_ip` in `boot.s`, one copy for both backends. - Byte accounting on ip65 (the tight one): LOADER went from 16 B free to - 58 B **at the time of #142**; it is **21 B** at the v0.14.0 pin, which - is the number the Memory layout section carries and the one to use. + 58 B **at the time of #142**; it is **17 B** at the v0.14.0 pin with + #211's fix in (21 B before it — the 4 B is `http.o`'s `CODE` growth in + that fix, measured), which is the number the + Memory layout section carries and the one to use. `print_local_ip` rides LOADER_OVERFLOW, so the NET_CODE tail that is `HTTPS_HOST`/`HTTPS_PATH`'s ip65 budget shrank from 170 to ~60 B beyond the default strings (56 B measured; wikipedia's +46 B still builds on @@ -556,9 +558,10 @@ already refused a step later, as `DF_ERR_TYPE = $04`). Test: (server-name validation took the comb tail from 714 to 223 B and broke `rig_https_wiki.py`, which now drives the menu instead; that tail is **126 B today** — see the margin note under Known issues, and do not - quote the 223 as current). Those are the - numbers from that episode; the comb tail is **153 B** at the v0.14.0 - pin — see the memory map below, and never size rig scratch from this + quote the 223 as current). The 714 and the 223 are the + numbers from that episode; the comb tail is **126 B** at the v0.14.0 + pin, re-measured off `build/c64-https.map` — see the memory map + below, and never size rig scratch from this bullet's historical figures. The harness write guard raises `MemoryPolicyError` before the wire. - `CRYPTO_HOT` margin under UCI is **per profile, and the one number this @@ -608,8 +611,16 @@ already refused a step later, as `DF_ERR_TYPE = $04`). Test: `LOADER_OVERFLOW`.** #211's verdict routine (~27 B) was placed twice before it landed, and both wrong homes linked cleanly on the default target: - - inline in `CODE` took ip65's LOADER from 21 B free to **zero** — - it fit exactly, so the *next* byte anyone added would not link; + - inline in `CODE` took ip65's LOADER from the 21 B free it had + *then* to **zero** — it fit exactly, so the *next* byte anyone + added would not link. The routine landed in `HTTP_AUX_CODE2` + instead, and LOADER measures **17 B** free today. The 4 B is + `http.o`'s own `CODE` growth in that same fix: rebuilding + ip65-onchip with only `src/http.s` reverted to `60022de` moves + `http.o`'s `CODE` from $35E to $35A and the whole `CODE` segment + from $17E2 to $17DE, putting LOADER back at 21 B. Attribution is + to the module and the commit; nobody has disassembled *which* + four bytes; - `LOADER_OVERFLOW` lands in `NET_CODE`, whose tail is a **joint budget with `HTTPS_TARGET_RODATA`**, and 27 B there took the wikipedia-target margin from 14 B to **−13 B**, breaking @@ -752,11 +763,15 @@ UCI (`cfg/c64-https-uci.cfg`, W1 hot/cold split — the reference): build: TLS_DEFRAME_CODE (~1.4 KB), CERT_BUF_BSS (2,048 B), HTTPS_TARGET_RODATA, x509_name; comb adds RODATA/LIMLEE_BSS - (**153 B** tail free measured at the + (**126 B** tail free, re-measured at the v0.14.0 pin with the P-384 objects gated - out; it was 120 B before gating returned - 33 B of CRYPTO_RODATA here, and the - ~223 B this file used to claim was stale + out: last tenant + `LIB_NISTCURVES_MUL_CODE` ends $5F81, + region ends $5FFF, $5FFF-$5F81 = $7E. + It was 153 B before #211's fix, and + 120 B before gating returned 33 B of + `CRYPTO_RODATA` here; the ~223 B this + file used to claim was stale — it was 159 B at v0.11.2). Also the slot for the (broken) overlay-embed flags. @@ -795,11 +810,17 @@ deriving the others by arithmetic — the deltas are not uniform, because a segment recovered in one profile can land in a different region in another (comb's `CRYPTO_RODATA` is the worked example: see the 33 B note above). -ip65 is essentially full. Measured at the v0.14.0 pin, with the P-384 -objects gated out of the link: 56 B NET_CODE tail (on top of the 20 B the -default target strings already use), 145 B CRYPTO_RESIDENT, 152 B -CRYPTO_OVERLAY, 21 B LOADER, and a 43 B hole below TABLES_BSS in -CRYPTO_COLD_SHADOW. PRG size is not a headroom gauge. **CRYPTO_OVERLAY and +ip65 is essentially full. Measured on **ip65-onchip** — the shipped +product — at the v0.14.0 pin, with the P-384 objects gated out of the +link and #211's fix in: 56 B NET_CODE tail (on top of the 20 B the +default target strings already use), 145 B CRYPTO_RESIDENT, 125 B +CRYPTO_OVERLAY, 17 B LOADER, and a 43 B hole +below TABLES_BSS in +CRYPTO_COLD_SHADOW. The unshipped ip65 REU profile matches on three of +those (LOADER, CRYPTO_OVERLAY, NET_CODE) but **not** on CRYPTO_RESIDENT, +where it has 231 B free: `LIB_NISTCURVES_MUL_CODE` is $27 there against +$A2 onchip, so the onchip figure is the conservative one and the one to +size against. PRG size is not a headroom gauge. **CRYPTO_OVERLAY and CRYPTO_RESIDENT are ADJACENT ($4F8C-$5FFF and $6000-$9FFF), so they are one pool of 20,596 B, and no amount of shuffling segments between them creates space** — that is why the contiguous-region cfg restructure is only worth @@ -812,9 +833,15 @@ CRYPTO_OVERLAY only rebalances the two halves of the pool, and does it to match what both UCI cfgs already do. The other free blocks are not reachable from the pool: NET_CODE's tail is the `HTTPS_HOST`/`HTTPS_PATH` budget, and the smallest segment in the pool is `LIB_NISTCURVES_MUL_CODE` -at 162 B (not `HTTP_AUX_CODE2`'s 169 B, which this file used to name). - - - `LOADER_OVERFLOW` carries ~125 B of `http.s` that outgrew LOADER. +at 162 B (not `HTTP_AUX_CODE2`, which this file used to name; that one +is 196 B today, and was 169 B before #211). + + - `LOADER_OVERFLOW` is 332 B and `http.s` is **not** in it. Measured + tenants (ip65-onchip map, per module): `boot.o` 110 B, `vic.o` 18 B, + `crypto_swap.o` 204 B. W4 moved `http.s`'s share out to + `HTTP_AUX_CODE` (`src/http.s`, grep "W4: moved from LOADER_OVERFLOW"), + so the segment has no `http.s` tenant at all and this line's old + "~125 B of `http.s`" named a module that had already left. - `src/loadaddr.s` (PRG load address) and `src/exports.s` (promotes equates to `labels.txt`, incl. `cert_buf_size` — rigs must read it, not hardcode 2048/1536) are intentional stubs; ip65-only exports live in