From d4e67153aa0e9cd3c265a9dba26356568bdf6cdb Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:05:46 -0500 Subject: [PATCH] =?UTF-8?q?feat(cfg):=20ip65=20refit=20=E2=80=94=20un-brea?= =?UTF-8?q?k=20BACKEND=3Dip65=20via=20cert=5Fbuf/verify-scratch=20union?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plain `make BACKEND=ip65` has failed since the nistcurves bump with `BSS overflows CRYPTO_COLD_SHADOW by 1406 bytes`. The gap is exactly LIB_NISTCURVES_P256_BSS (1,312 B) — pure ECDSA-verify-time scratch (fp_wide/fp_r0/fp_inv_*/ec_*/ecdsa_* in libs/nistcurves data_p256.s). Close it by time-sharing that scratch with cert_buf's 1,536 B, whose lifetime is disjoint: - cert_buf is written+parsed only inside x509_parse_cert / x509_extract_pubkey (via tls_handle_certificate); the pubkey is extracted into CRYPTO_BSS slots at Certificate-processing time. - cert_tbs_ptr/cert_tbs_len point into cert_buf but have zero in-tree readers (exported, never imported). - tls_handle_cert_verify (where the lib scratch is live) reads only tls_rec_buf + the transcript + the pubkey slots — audited src/tls_cert.s lines 535-743: sig parse from tls_rec_buf+8, signed-content rebuild from transcript hash. - tools/test_x509.py rewrites cert_buf before every parse use; its verify tests (3a-3d) stage inputs directly and never re-read cert_buf. tools/test_x25519.py doesn't touch it. Changes: - cfg/c64-https-ip65.cfg: new SCRATCH_UNION region ($A000, $600, non-file) overlapping CRYPTO_COLD_SHADOW's head; LIB_NISTCURVES_P256_BSS routed there; new CERT_BUF_BSS segment pinned start=$A000 (tls_rec_buf stays in BSS_TAIL at $A600 — it is live during verify and must not be in the union span); TABLES_BSS declared last so it packs at $BA00-$BFFF, putting mul_dma at $BA00/$BB00 and sqtab at $BC00 — the onchip profile's sqtab_reserved=$BC00 bake invariant holds with no extra pinning. - src/der_decode.s: cert_buf moved BSS_TAIL -> CERT_BUF_BSS with the lifetime contract documented at the declaration. - cfg/c64-https-uci{,-onchip}.cfg: declare CERT_BUF_BSS first in CRYPTO_COLD_SHADOW (same total occupancy; UCI needs no union). - src/net/ip65/exports.s: link-time .assert cert_buf=$A000 so the union anchor cannot silently drift. Verified: - make BACKEND=ip65 LINK OK (47,105 B) - make BACKEND=ip65 USE_NISTCURVES_ONCHIP=1 LINK OK, sqtab guard OK - make BACKEND=uci / +ONCHIP / +ONCHIP_COMB all LINK OK (62,977 B, byte-count identical to pre-change) - ip65 map: code tops out at $9EE8 (<$A000), cold shadow ends $BFFF, union layered at $A000-$A51F under CERT_BUF_BSS $A000-$A5FF - VICE KATs on the ip65 build (-reu): test_x25519.py 71/71, test_x509.py 11/11 (3c/3d full verifies scribble the union between the parse tests — the exact hazard the union design must survive) Co-Authored-By: Claude Fable 5 --- cfg/c64-https-ip65.cfg | 86 ++++++++++++++++++++++++------------ cfg/c64-https-uci-onchip.cfg | 9 +++- cfg/c64-https-uci.cfg | 9 +++- src/der_decode.s | 18 +++++--- src/net/ip65/exports.s | 11 +++++ 5 files changed, 94 insertions(+), 39 deletions(-) diff --git a/cfg/c64-https-ip65.cfg b/cfg/c64-https-ip65.cfg index 194268a..06b0fde 100644 --- a/cfg/c64-https-ip65.cfg +++ b/cfg/c64-https-ip65.cfg @@ -61,26 +61,21 @@ # CRYPTO_COLD_SHADOW $A000-$BFFF 8 KB file-backed (zero-filled). # BSS chunks land here. # -# RESIDUAL FIT ISSUE: the total c64-https + libs/nistcurves BSS claim -# (BSS 4,478 B + CRYPTO_BSS 21 B + TABLES_BSS 1,536 B page-aligned + -# BSS_TAIL 2,084 B + LIB_NISTCURVES_P256_BSS 1,573 B = ~9,692 B before -# alignment) exceeds the 8 KB CRYPTO_COLD_SHADOW budget by ~1.5 KB. -# ld65 surfaces a `BSS overflows CRYPTO_COLD_SHADOW by 1662 bytes` -# warning + a non-zero exit. Cfg-only relief is exhausted under the -# bumped library (see Worker I's trace at the W15 review). Resolution -# requires one of: -# (a) Library-side minimal-archive variant — drop data_p256 anchor -# RAM or audit data_p256.o for unused scratch (~1 KB+). -# (b) Source-level BSS split — partition src/data.s BSS into a -# hot/cold pair so cold-path BSS can be routed elsewhere -# (~1.5 KB+). -# (c) Defer-paging — SHA-384 / P-384 overlay-paging strategy -# extended to a c64-https in-tree BSS slice. +# HISTORICAL FIT ISSUE (RESOLVED by the ip65 refit / SCRATCH_UNION): +# the total c64-https + libs/nistcurves BSS claim (~9.6 KB incl. +# alignment) exceeded the 8 KB CRYPTO_COLD_SHADOW budget by 1,406 B — +# exactly the LIB_NISTCURVES_P256_BSS footprint (1,312 B + packing). +# Cfg-only relief short of a union was genuinely exhausted (total +# non-cold slack elsewhere ≈ 724 B). The refit closes the gap by +# time-sharing cert_buf's RAM with the lib's verify-time scratch via +# the SCRATCH_UNION region below — see its comment block for the +# lifetime argument. Library-side minimal-archive work +# (c64-nist-curves#54) is now optional headroom, not a blocker. # -# UCI is unaffected (62977 B PRG builds clean under W1) — under UCI -# the NET_CODE region absorbs TLS_CODE+CRYPTO_AUX_CODE+CRYPTO_AUX_CODE2 -# (the UCI adapter is ~1.7 KB vs ip65's 6.95 KB blob), opening enough -# CRYPTO_RESIDENT slack to leave BSS budget intact. +# UCI is unaffected — under UCI the NET_CODE region absorbs +# TLS_CODE+CRYPTO_AUX_CODE+CRYPTO_AUX_CODE2 (the UCI adapter is +# ~1.7 KB vs ip65's 6.95 KB blob) and LIB_NISTCURVES_P256_BSS rides +# NET_BSS_TAIL; no union is needed there. FEATURES { STARTADDRESS: default = $0801; @@ -110,6 +105,24 @@ MEMORY { # $A000-$BFFF cannot wipe any executable bytes. CRYPTO_RESIDENT: start = $6000, size = $4000, file = %O, define = yes, fill = yes, fillval = $00; CRYPTO_COLD_SHADOW: start = $A000, size = $2000, file = %O, define = yes, fill = yes, fillval = $00; + # SCRATCH_UNION: overlaps the head of CRYPTO_COLD_SHADOW ($A000-$A5FF) + # ON PURPOSE. It time-shares cert_buf's 1,536 B with the sibling + # library's 1,312 B of ECDSA verify scratch (LIB_NISTCURVES_P256_BSS: + # fp_wide / fp_r0 / fp_inv_* / ec_* / ecdsa_* staging in + # libs/nistcurves data_p256.s). Lifetimes are disjoint by + # construction: cert_buf is dead once the Certificate handler has + # extracted the pubkey (see the lifetime contract comment at + # cert_buf in src/der_decode.s), and the lib scratch is live only + # inside ecdsa_verify — which runs strictly after that point (CV + # handling) or from KAT harnesses that never re-read cert_buf. + # This union is what closes the historical `BSS overflows + # CRYPTO_COLD_SHADOW by 1406 bytes` gap (the overflow was exactly + # the lib BSS) without a library-side minimal archive + # (c64-nist-curves#54 becomes optional). Not file-backed — both + # tenants are runtime-zeroed/overwritten BSS. + # Size $600 caps the lib BSS at cert_buf's span: growth past it is + # an ld65 overflow error, never silent corruption of BSS_TAIL. + SCRATCH_UNION: start = $A000, size = $0600, type = rw, define = yes; # Phase 3: ip65 backend does NOT embed the P-384 split overlay # blobs (no room in main RAM after the existing layout, and ip65 @@ -143,17 +156,29 @@ SEGMENTS { CRYPTO_AUX_CODE2: load = NET_CODE, type = ro, optional = yes; NET_BSS: load = NET_BSS, type = bss, optional = yes; - # --- W1 hot/cold split: BSS catch-alls land in CRYPTO_COLD_SHADOW. --- - # The total in-tree + library BSS claim (~9.7 KB) exceeds the 8 KB - # CRYPTO_COLD_SHADOW budget. The residual overflow is surfaced at - # link time; the supervisor must decide between source-level BSS - # split, library-side minimal-archive variant, or another overlay - # paging strategy (see the MEMORY entry header). - LIB_NISTCURVES_P256_BSS: load = CRYPTO_COLD_SHADOW, type = bss, optional = yes; - # BSS_TAIL hosts the cert_buf (1.5 KB) + tls_rec_buf (548 B) per - # src/data.s and src/der_decode.s. + # --- W1 hot/cold split + ip65 refit: BSS lands in CRYPTO_COLD_SHADOW. --- + # Layout (declaration order is load-bearing — ld65 packs in order): + # CERT_BUF_BSS $A000-$A5FF pinned; unioned with the lib scratch + # BSS_TAIL $A600-... tls_rec_buf (548 B) — MUST stay outside + # the union span: it is live during + # verify (post-dispatch CV transcript + # fold + next-record reads) + # BSS/CRYPTO_BSS ... in-tree catch-alls + # TABLES_BSS $BA00-$BFFF declared LAST so the align=$100 pad + # is absorbed at $BA00 exactly: + # mul_dma_lo $BA00 / mul_dma_hi $BB00 / + # sqtab $BC00 — the onchip profile's + # sqtab_reserved=$BC00 bake invariant + # (Makefile grep guard) holds with no + # further pinning. + # cert_buf is pinned via start= so the SCRATCH_UNION overlap can + # never silently drift; a link-time .assert in + # src/net/ip65/exports.s double-checks it. + CERT_BUF_BSS: load = CRYPTO_COLD_SHADOW, type = bss, start = $A000; + LIB_NISTCURVES_P256_BSS: load = SCRATCH_UNION, type = bss, optional = yes; + # BSS_TAIL hosts tls_rec_buf (548 B) per src/data.s (cert_buf moved + # to CERT_BUF_BSS above under the ip65 refit). BSS_TAIL: load = CRYPTO_COLD_SHADOW, type = bss, optional = yes; - TABLES_BSS: load = CRYPTO_COLD_SHADOW, type = bss, align = $100; # --- Resident crypto + TLS code / rodata. --- # W1 hot/cold split: TLS_CODE + CRYPTO_AUX_CODE ride the historical @@ -206,6 +231,9 @@ SEGMENTS { # hot/cold split; CRYPTO_RESIDENT now carries code+rodata only. BSS: load = CRYPTO_COLD_SHADOW, type = bss; CRYPTO_BSS: load = CRYPTO_COLD_SHADOW, type = bss; + # TABLES_BSS declared last on purpose — see the cold-shadow layout + # comment above (must pack at $BA00 for the sqtab=$BC00 invariant). + TABLES_BSS: load = CRYPTO_COLD_SHADOW, type = bss, align = $100; # Phase 3: ip65 backend stays at the historical 47 KB PRG size -- # USE_OVERLAY_P384_EMBED is gated off in the Makefile under ip65, diff --git a/cfg/c64-https-uci-onchip.cfg b/cfg/c64-https-uci-onchip.cfg index 4eb06f7..f258fbb 100644 --- a/cfg/c64-https-uci-onchip.cfg +++ b/cfg/c64-https-uci-onchip.cfg @@ -210,8 +210,13 @@ SEGMENTS { # for any future BSS routing or harness use. NET_BSS_TAIL: load = NET_BSS_TAIL, type = bss, optional = yes; # BSS_TAIL hosts the largest c64-https in-tree BSS buffers - # (tls_rec_buf 548 B + cert_buf 1.5 KB). Worker I routed it to - # CRYPTO_COLD_SHADOW ($A000-$BFFF) under the hot/cold split. + # (tls_rec_buf 548 B; cert_buf moved to CERT_BUF_BSS below). Routed + # to CRYPTO_COLD_SHADOW ($A000-$BFFF) under the hot/cold split. + # ip65 refit: cert_buf moved from BSS_TAIL into its own segment so + # the ip65 cfg can pin + union it with the lib verify scratch (see + # cfg/c64-https-ip65.cfg SCRATCH_UNION). Under UCI it simply packs + # here, first in the cold shadow — same total occupancy as before. + CERT_BUF_BSS: load = CRYPTO_COLD_SHADOW, type = bss, optional = yes; BSS_TAIL: load = CRYPTO_COLD_SHADOW, type = bss, optional = yes; # --- Overlay slot. --- diff --git a/cfg/c64-https-uci.cfg b/cfg/c64-https-uci.cfg index 1d4b2df..d5110a8 100644 --- a/cfg/c64-https-uci.cfg +++ b/cfg/c64-https-uci.cfg @@ -196,8 +196,13 @@ SEGMENTS { # for any future BSS routing or harness use. NET_BSS_TAIL: load = NET_BSS_TAIL, type = bss, optional = yes; # BSS_TAIL hosts the largest c64-https in-tree BSS buffers - # (tls_rec_buf 548 B + cert_buf 1.5 KB). Worker I routed it to - # CRYPTO_COLD_SHADOW ($A000-$BFFF) under the hot/cold split. + # (tls_rec_buf 548 B; cert_buf moved to CERT_BUF_BSS below). Routed + # to CRYPTO_COLD_SHADOW ($A000-$BFFF) under the hot/cold split. + # ip65 refit: cert_buf moved from BSS_TAIL into its own segment so + # the ip65 cfg can pin + union it with the lib verify scratch (see + # cfg/c64-https-ip65.cfg SCRATCH_UNION). Under UCI it simply packs + # here, first in the cold shadow — same total occupancy as before. + CERT_BUF_BSS: load = CRYPTO_COLD_SHADOW, type = bss, optional = yes; BSS_TAIL: load = CRYPTO_COLD_SHADOW, type = bss, optional = yes; # --- Overlay slot. --- diff --git a/src/der_decode.s b/src/der_decode.s index f7a4581..20719e5 100644 --- a/src/der_decode.s +++ b/src/der_decode.s @@ -573,12 +573,18 @@ cert_sig_r: .res 48 ; signature r component (max 48 for P-38 cert_sig_s: .res 48 ; signature s component (max 48 for P-384) cert_sig_len: .res 1 ; 32 (P-256) or 48 (P-384) cert_curve_id: .res 1 ; 0=P-256, 1=P-384 -; W1 partial: cert_buf (1.5 KB) lives in BSS_TAIL — the same offload -; region as src/data.s::tls_rec_buf — routed to NET_BSS_TAIL under -; UCI. Keeps the cert parse staging out of the CRYPTO_HOT overflow -; path. ip65 cfg aliases BSS_TAIL to BSS so the relocation is -; invisible there. -.segment "BSS_TAIL" +; ip65 refit: cert_buf gets its own segment (was BSS_TAIL alongside +; src/data.s::tls_rec_buf) so the ip65 cfg can pin it at $A000 and +; time-share the RAM with LIB_NISTCURVES_P256_BSS (verify-time scratch). +; LIFETIME CONTRACT: cert_buf (and cert_tbs_ptr/len, which point into +; it) are dead once x509_parse_cert / x509_extract_pubkey returns — +; the pubkey is extracted at Certificate-processing time, and the +; CertificateVerify path reads only tls_rec_buf + the transcript + +; the CRYPTO_BSS pubkey slots. ecdsa_verify may therefore scribble +; cert_buf freely. Any future cert-chain validation that re-reads +; cert_buf (or hashes TBS via cert_tbs_ptr) after Certificate +; processing MUST first break the union in cfg/c64-https-ip65.cfg. +.segment "CERT_BUF_BSS" cert_buf: .res 1536 ; certificate DER buffer .segment "BSS" cert_buf_len: .res 2 ; certificate length diff --git a/src/net/ip65/exports.s b/src/net/ip65/exports.s index df19101..0547d6c 100644 --- a/src/net/ip65/exports.s +++ b/src/net/ip65/exports.s @@ -11,3 +11,14 @@ .export ip65_init .export ip65_process + +; --- ip65 refit: SCRATCH_UNION drift guard ----------------------------- +; cfg/c64-https-ip65.cfg time-shares cert_buf's RAM with +; LIB_NISTCURVES_P256_BSS (verify-time scratch) via the SCRATCH_UNION +; region at $A000. Both anchors are pinned in the cfg; this link-time +; assert catches any silent drift (e.g. someone dropping the start= +; pin or reordering segments) before it becomes runtime corruption. +; ip65-only file, so the UCI backends (where cert_buf floats) are +; unaffected. +.import cert_buf +.assert cert_buf = $A000, lderror, "ip65 refit: cert_buf must sit at $A000 (SCRATCH_UNION anchor) - see cfg/c64-https-ip65.cfg"