From 4e7430253cfa2ae61dd3927a675b96c19ed770ee Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Thu, 13 Aug 2026 06:55:42 -0500 Subject: [PATCH 1/6] =?UTF-8?q?docs:=20the=20ip65=20blob=20is=20not=20comm?= =?UTF-8?q?itted=20=E2=80=94=20document=20the=20fresh-clone=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md's Build section described `ip65-build/ip65-c64.bin` as "the committed blob" and `make ip65-libs` as "only needed if the ip65 submodule changes". Both are wrong, and they reinforce each other: the reader is told the artifact ships with the repo (so no rebuild) and that the library build is conditional (so skip it) — which routes them past the only step that works. The blob is gitignored (`.gitignore: ip65-build/*.bin`; `git ls-files ip65-build/` returns only ip65.cfg and ip65_stub.s). A fresh clone has no blob, `make` tries to build one, and the link step consumes ip65 `.lib` archives the submodule does not ship, so it dies with: ld65: Error: Input file '../ip65/ip65/ip65_tcp.lib' not found which reads as a broken checkout rather than a missing one-time step. Reproduced in two independent fresh worktrees; `make ip65-libs && make` then links clean and yields the 47,105 B ip65 PRG. The blob rebuild is deterministic — 6,951 B, sha256 cf1a5ff7…0c2d90dd in both worktrees. - Build gains a "first build in a fresh clone" block with the working order, and states what "normally reused" actually means: `clean` only removes `build/`, so a once-built blob persists. - ip65-libs / ip65-blob target descriptions corrected. - Same false claim removed from the Makefile's own comments (two sites), which is where it was being copied from. - The VICE ip65 rig's prerequisite list gains the blob step; it is the one rig that needs an ip65 PRG. - While in the section: the C64_SKIP_BUILD count said 7, it is 14 (13 under tools/, plus tests/test_vice_https_macos.py). Replaced the number with the grep that regenerates the list. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 44 ++++++++++++++++++++++++++++++++++++++------ Makefile | 14 +++++++++----- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 51d3e7b..3eb82d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,28 @@ Dependencies: - GNU make - VICE (`x64sc`) only for `make run` / the test harness +First build in a fresh clone or worktree (ip65 backend only — the UCI +backend needs none of this): + + git submodule update --init --recursive + make ip65-libs # once per clone + make + +`ip65-build/ip65-c64.bin` is a **gitignored local build artifact** +(`.gitignore` line `ip65-build/*.bin`; `git ls-files ip65-build/` returns +only `ip65.cfg` and `ip65_stub.s`), *not* a committed file. A plain `make` +does try to build it, but the link step consumes ip65 `.lib` archives that +the submodule does not ship — it ships the sources for them — so without +`make ip65-libs` first it dies with: + + ld65: Error: Input file '../ip65/ip65/ip65_tcp.lib' not found + +`make clean` only removes `build/`, so once built the blob survives and is +never rebuilt; that persistence, not a committed file, is why the rebuild +targets are normally invisible. The rebuild is deterministic: 6,951 B, +sha256 `cf1a5ff7809af4e4655e385b378b936054f41046ff2b7604828af3240c2d90dd` +(reproduced byte-identically in two independent worktrees, 2026-08-13). + Targets: - `make` — default, produces `build/c64-https.prg`, `build/labels.txt` (VICE label format), and @@ -23,10 +45,14 @@ Targets: agents; P-384 overlays get `.dbg` sidecars too) - `make clean` — remove build artifacts - `make run` — autostart the PRG in VICE - - `make ip65-libs` — rebuild ip65 object libraries from the submodule - (only needed if the ip65 submodule changes) + - `make ip65-libs` — build ip65's object libraries from the + submodule. Required once per fresh clone (see + above), and again whenever the ip65 submodule + changes. - `make ip65-blob` — rebuild `ip65-build/ip65-c64.bin` from those - libraries (the committed blob is normally reused) + libraries. A plain `make` already builds the + blob on demand and then reuses it, so this + target is only for forcing a rebuild. Variables: - `BACKEND=ip65|uci` — select networking backend cfg @@ -60,8 +86,10 @@ Variables: Test harness expectations: - Most `tools/test_*.py` scripts run `make clean && make` themselves before launching VICE. Set `C64_SKIP_BUILD=1` in the environment to - reuse the already-built PRG (7 scripts currently honor the var — - see the "Honor C64_SKIP_BUILD" commit for the list). + reuse the already-built PRG. 14 scripts honor it as of 2026-08-13 + (13 under `tools/`, plus `tests/test_vice_https_macos.py`); the + current list is `grep -ln 'environ.*C64_SKIP_BUILD' tools/test_*.py + tests/test_*.py` rather than a number that goes stale here. - Use the `c64-test-harness` Python package to launch VICE; never run `x64sc` directly from tests. @@ -1099,8 +1127,12 @@ the REU-less stock-C64 numbers above were measured. Knobs: `E2E_TIMEOUT`, `HTTPS_PORT` (the PRG's port is a build knob — `make HTTPS_PORT=4433` — so the listener can run unprivileged). -Two prerequisites that are easy to lose: +Three prerequisites that are easy to lose: + - **An ip65 PRG that builds at all.** This is the one backend that + needs the ip65 blob, which is a gitignored artifact — on a fresh + clone `make` fails at the blob link until `make ip65-libs` has run + once. See "First build in a fresh clone" under Build. - **An ethernet-capable VICE.** Stock macOS VICE binaries (official and Homebrew) compile ethernet in but gate the pcap driver on `geteuid()==0`, so unprivileged `-ethernetiodriver pcap` is diff --git a/Makefile b/Makefile index c67cec4..f425912 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,8 @@ # make — default, produces build/c64-https.prg + build/labels.txt # make clean — remove build artifacts # make run — launch the PRG in VICE x64sc -# make ip65-libs — rebuild ip65 object libraries from the submodule +# make ip65-libs — build ip65 object libraries from the submodule +# (required once per fresh clone, BACKEND=ip65 only) # make ip65-blob — rebuild ip65-build/ip65-c64.bin (requires ip65-libs first) # # Variables: @@ -418,13 +419,16 @@ ifeq ($(USE_OVERLAY_P384_EMBED),1) build/crypto/ecdsa_verify_384.o: build/p384_overlay_equates.inc endif -# Build ip65 object libraries from the submodule. Only needed if the ip65 -# submodule changes; the prebuilt blob is committed to ip65-build/. +# Build ip65 object libraries from the submodule. Required once per fresh +# clone — the submodule ships sources, and the ip65-blob link below needs +# the .lib archives this target produces. Re-run when the submodule moves. ip65-libs: cd $(IP65_DIR) && $(MAKE) -C ip65 && $(MAKE) -C drivers -# Build the ip65 binary blob (ip65-build/ip65-c64.bin). The resulting file is -# committed to the repo so a normal `make` does not need to rebuild it. +# Build the ip65 binary blob (ip65-build/ip65-c64.bin). The file is a +# gitignored build artifact (.gitignore: ip65-build/*.bin), NOT committed. +# `all` depends on it under BACKEND=ip65, so a normal `make` builds it once +# and then reuses it — `clean` only removes build/, so it survives. ip65-blob: $(IP65_BIN) $(IP65_BIN): $(IP65_BUILD)/ip65_stub.s $(IP65_BUILD)/ip65.cfg From c9e2b95198af59c193db709dd2d60a23d20bf0d1 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Thu, 13 Aug 2026 06:59:25 -0500 Subject: [PATCH 2/6] docs: the blob reproduction is three worktrees over three months, not two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correction routed from Lane G, verified here before restating: the determinism evidence for ip65-c64.bin is stronger than the sentence claimed. Three independent worktrees rebuilt it byte-identically on 2026-08-13 (Lane C's, Lane G's, mine), and the 2026-05-06 copies carried in three repo snapshots hash the same cf1a5ff7...0c2d90dd — I hashed those three directly rather than take the report. The sentence carries the load of the whole section: if the rebuild is deterministic then nobody ever needed a committed copy, and the fix is to name the one-time prerequisite. Four agreeing artifacts across three months make that point better than two, so it is worth the accuracy. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3eb82d0..5ceb472 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,10 @@ the submodule does not ship — it ships the sources for them — so without never rebuilt; that persistence, not a committed file, is why the rebuild targets are normally invisible. The rebuild is deterministic: 6,951 B, sha256 `cf1a5ff7809af4e4655e385b378b936054f41046ff2b7604828af3240c2d90dd` -(reproduced byte-identically in two independent worktrees, 2026-08-13). +— rebuilt byte-identically in three independent worktrees on 2026-08-13, +and identical to a local copy built 2026-05-06. Three months and four +artifacts agree, so a stale blob is not a failure mode worth designing +around; a missing one is. Targets: - `make` — default, produces `build/c64-https.prg`, From 07192aa09fb9a82b3969fabcaf8d5e54c23b84c5 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Thu, 13 Aug 2026 07:03:22 -0500 Subject: [PATCH 3/6] docs: switching BACKEND without `make clean` silently links a mixed PRG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while verifying my own branch's PRG-neutrality: an incremental `make` after a `BACKEND=uci` build produced an ip65 PRG of exactly the right size (47,105 B) and the wrong contents — d483d46f… against the clean tree's db311110…, with no warning. The final make ran ld65 alone; every object was "up to date" by timestamp. The mechanism is worth naming because it is wider than the flag rule the Packaging section already states. BACKEND= is not a -D flag, it selects `-I src/net/$(BACKEND)`, and src/tls13.s includes net_tuning.inc from that path. So the stale object carries the *other* backend's post-ServerHello drain budget: an ip65 PRG built this way gets UCI's 1x16 instead of 8x250, which is precisely the #73 regression that #74 fixed — reintroduced by a build order rather than by a code change, in the one backend where the drain is load-bearing (ip65 sends no MSS and ACKs only when polled). Documented at the top of Variables, where someone about to type `make BACKEND=...` will read it, with the two hashes as evidence. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5ceb472..41fddaf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,9 +57,22 @@ Targets: blob on demand and then reuses it, so this target is only for forcing a rebuild. +**`make clean` when you change `BACKEND=` or any flag.** make tracks +source timestamps, not the command line, so an object built for the other +backend is "up to date" and gets linked as-is. This is not only about +`-D` flags: `BACKEND=` also selects the `-I src/net/$(BACKEND)` include +path, and `src/tls13.s` pulls `net_tuning.inc` from there. Building +`BACKEND=uci` and then plain `make` re-links a *UCI-tuned* `tls13.o` +into an ip65 PRG — post-ServerHello drain budget 1x16 instead of 8x250, +i.e. issue #73's regression, silently reintroduced. Observed 2026-08-13: +the mixed image is the same 47,105 B as the clean one and differs only +in content (`d483d46f…` vs the correct `db311110…`), and nothing in the +build output says so — the final `make` runs `ld65` alone. + Variables: - `BACKEND=ip65|uci` — select networking backend cfg - (`cfg/c64-https-$(BACKEND).cfg`; default ip65) + (`cfg/c64-https-$(BACKEND).cfg`; default ip65). + Changing it requires `make clean` — see above. - `USE_X25519_SIBLING=1` — swap the in-tree X25519 for the `libs/x25519@v0.6.0` sibling (UCI only — ip65 has a tracked code/rodata overflow; see From a0039b10e392a5a782cfbd5fbb0ab97d53490718 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Thu, 13 Aug 2026 07:10:03 -0500 Subject: [PATCH 4/6] =?UTF-8?q?docs:=20the=20stale-object=20trap=20has=20a?= =?UTF-8?q?=20second=20mode=20=E2=80=94=20make=20skips=20the=20link=20enti?= =?UTF-8?q?rely?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lane G reproduced the mixed link deliberately from a third worktree, so I went back to reproduce it cleanly here rather than rely on the accidental first sighting — and the deliberate run failed a different way, which is worth recording because it defeats the obvious check. make clean && make BACKEND=uci then plain make -> 4 ca65 lines, NO ld65 line, exit 0, build/c64-https.prg still the 62,977 B UCI image macOS ships GNU Make 3.81, which compares mtimes at 1-second resolution. The recompiled ip65 objects were 39 ms newer than the previous link; make's own -d output says "Prerequisite build/net/ip65/net.o is older than target build/c64-https.prg" and skips the link. A second `make` then says "Nothing to be done for `all'". So a BACKEND switch without clean gives you either a mixed PRG at the right size (the first mode, d483d46f... vs db311110...) or no rebuild at all and the wrong backend's PRG entirely — which of the two depends on whether the compile crosses a second boundary. Size catches the second, hash catches both, exit code catches neither. Object hashes are deliberately not cited: --debug-info embeds source paths, so .o hashes differ across worktrees even when the linked PRG is identical (confirmed — my clean ip65 tls13.o does not match Lane G's, while both link to db311110...). PRG hashes are the portable evidence. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 41fddaf..3c07658 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,16 +58,27 @@ Targets: target is only for forcing a rebuild. **`make clean` when you change `BACKEND=` or any flag.** make tracks -source timestamps, not the command line, so an object built for the other -backend is "up to date" and gets linked as-is. This is not only about -`-D` flags: `BACKEND=` also selects the `-I src/net/$(BACKEND)` include -path, and `src/tls13.s` pulls `net_tuning.inc` from there. Building -`BACKEND=uci` and then plain `make` re-links a *UCI-tuned* `tls13.o` -into an ip65 PRG — post-ServerHello drain budget 1x16 instead of 8x250, -i.e. issue #73's regression, silently reintroduced. Observed 2026-08-13: -the mixed image is the same 47,105 B as the clean one and differs only -in content (`d483d46f…` vs the correct `db311110…`), and nothing in the -build output says so — the final `make` runs `ld65` alone. +source timestamps, not the command line, so an object built for the +other backend counts as up to date. This is not only about `-D` flags: +`BACKEND=` also selects the `-I src/net/$(BACKEND)` include path, and +`src/tls13.s` pulls `net_tuning.inc` from there. Both failure modes were +observed in one worktree on 2026-08-13: + + - **Mixed link.** An ip65 PRG built from a UCI-compiled `tls13.o` + carries drain budget 1x16 instead of 8x250 — issue #73's regression, + silently reintroduced. Same 47,105 B as the clean image; only the + content differs (`d483d46f…` vs the correct `db311110…`), and the + build output is a bare `ld65` line. + - **No link at all.** macOS ships **GNU Make 3.81**, which compares + mtimes at 1-second resolution. Objects recompiled inside the same + second as the previous link count as older (measured: 39 ms newer, + make said "Prerequisite ... is older than target"), so `make` + exits 0 having left the *other backend's* PRG in place — a + 62,977 B UCI image where an ip65 build was asked for. + +So neither exit code nor file size distinguishes a good build from a bad +one here. After any flag or `BACKEND` change, `make clean`; if a build +matters, check its sha256. Variables: - `BACKEND=ip65|uci` — select networking backend cfg From dc016a7c8cc441b9055db619a6a95a313c4b1dd6 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Thu, 13 Aug 2026 07:16:13 -0500 Subject: [PATCH 5/6] docs: why PRG hashes are checkable and object hashes are not MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correcting my own reasoning in a0039b1. That commit said object hashes differ across worktrees because `--debug-info` embeds source paths. That is wrong, and I checked it this time instead of reasoning about it: `strings build/tls13.o` shows only relative paths (`src/tls13.s`, `src/net/ip65/net_tuning.inc`) and zero absolute ones, so two worktrees would agree. The real mechanism is worse for the conclusion's sake, and better for the advice. ca65 stamps the build's wall-clock time into the object header — a little-endian 32-bit field at offset 107 in build/tls13.o that matches `date +%s` of the build exactly (searched the header for a LE32 within +/-20 s of the build time; one hit, delta 0). So rebuilding identical source in the SAME worktree changes the object hash: 6cd91553... -> cfef8378... over a `touch src/tls13.s && make`, four bytes differing. Object hashes are therefore not weak evidence, they are not evidence: nobody can reproduce their own twice. The useful half is the asymmetry — ld65 does not propagate that field, and build/c64-https.prg stayed at db31111031e2... across every one of those rebuilds. PRG-hash comparison works because of a checkable toolchain property, which is worth stating where the neutrality checks in this session all quietly relied on it. Credit to Lane G for catching the wrong mechanism; the timestamp offset and the same-worktree reproduction are measured here. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3c07658..8748563 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,7 +78,17 @@ observed in one worktree on 2026-08-13: So neither exit code nor file size distinguishes a good build from a bad one here. After any flag or `BACKEND` change, `make clean`; if a build -matters, check its sha256. +matters, check the **PRG's** sha256. + +Specifically the PRG's, not an object's: **ca65 stamps the build's +wall-clock time into every `.o`** (LE32 in the object header — offset +107 in `build/tls13.o`, exactly `date +%s` of the build). Two clean +builds of identical source therefore produce different object hashes, +so a `.o` hash is not evidence of anything. `ld65` does not propagate +that field, so the PRG *is* deterministic: `build/c64-https.prg` held at +`db31111031e2…` across every rebuild while `build/tls13.o` changed hash +each time. That asymmetry is what makes PRG-hash comparison a usable +check — it is a property of the toolchain, not a convention. Variables: - `BACKEND=ip65|uci` — select networking backend cfg From 64c0e78de4ec5f0c8f041558cc0dc77e4ab30e50 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Thu, 13 Aug 2026 07:19:27 -0500 Subject: [PATCH 6/6] docs: drop the byte offset from the object-timestamp note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dc016a7 quoted "LE32 at offset 107 in build/tls13.o". The offset should not be in the doc: it is a cc65-version detail, and someone reproducing on another toolchain who finds nothing at 107 would reasonably conclude the whole finding was wrong. The reproduction that always works is two clean builds and a hash comparison, so that is what the section now says. For the record, since three readings of this effect are now in circulation (100, 100/101, 107): measured across three objects in one tree — build/tls13.o, build/boot.o, build/crypto/sha256.o — every build changes bytes at 0-based 100-101 AND 107-108, and in all three objects the LE32 at 107 equals `date +%s` of the build exactly. So the offsets were consistent across objects here, and the divergent readings look like different bytes of the same effect rather than an object-dependent layout. That detail is worth knowing and not worth documenting, which is the point. Conclusion unchanged and now confirmed three ways: object hashes are not evidence, the PRG hash is deterministic, and every neutrality claim in this remediation rests on the latter. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8748563..77633c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,14 +81,19 @@ one here. After any flag or `BACKEND` change, `make clean`; if a build matters, check the **PRG's** sha256. Specifically the PRG's, not an object's: **ca65 stamps the build's -wall-clock time into every `.o`** (LE32 in the object header — offset -107 in `build/tls13.o`, exactly `date +%s` of the build). Two clean -builds of identical source therefore produce different object hashes, -so a `.o` hash is not evidence of anything. `ld65` does not propagate -that field, so the PRG *is* deterministic: `build/c64-https.prg` held at -`db31111031e2…` across every rebuild while `build/tls13.o` changed hash -each time. That asymmetry is what makes PRG-hash comparison a usable -check — it is a property of the toolchain, not a convention. +wall-clock time into every `.o` header**, so two clean builds of +identical source produce different object hashes and a `.o` hash is not +evidence of anything. `ld65` does not propagate that field, so the PRG +*is* deterministic: `build/c64-https.prg` held at `db31111031e2…` across +every rebuild while every `.o` changed hash each time. That asymmetry is +what makes PRG-hash comparison a usable check — a property of the +toolchain, not a convention. + +No byte offset is quoted here on purpose: it is a cc65-version detail, +and three people reading three different offsets out of the same effect +is how a checkable finding turns into a disputed one. The reproduction +is `make clean && make` twice and comparing hashes, which holds whatever +the layout. Variables: - `BACKEND=ip65|uci` — select networking backend cfg