Skip to content

docs: the ip65 blob is not committed — document the fresh-clone build order - #86

Merged
JC-000 merged 6 commits into
docs/benchmark-refreshfrom
docs/build-section-fresh-clone
Aug 13, 2026
Merged

docs: the ip65 blob is not committed — document the fresh-clone build order#86
JC-000 merged 6 commits into
docs/benchmark-refreshfrom
docs/build-section-fresh-clone

Conversation

@JC-000

@JC-000JC-000 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Follow-up to #80, kept on its own branch so #80 stays merge-ready — the two
touch different parts of CLAUDE.md and do not conflict. Routed from Lane C via
the coordinator, reproduced independently by Lane G and by me.

The defect

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 false, and they compound: the reader is told the artifact ships with
the repo (so no rebuild needed) and that the library build is conditional (so
skip it) — which is precisely the path that does not work.

The blob is gitignored:

$ grep -n 'ip65-build' .gitignore
5:ip65-build/*.o
6:ip65-build/*.bin
7:ip65-build/*.map
$ git ls-files ip65-build/
ip65-build/ip65.cfg
ip65-build/ip65_stub.s

Reproduction (fresh worktree, verbatim)

$ make
cd ip65-build && ld65 -C ip65.cfg -o ip65-c64.bin -m ip65-c64.map \
ip65_stub.o ../ip65/ip65/ip65_tcp.lib \
../ip65/drivers/ip65_c64.lib c64.lib
ld65: Error: Input file '../ip65/ip65/ip65_tcp.lib' not found
make: *** [ip65-build/ip65-c64.bin] Error 1 (rc=2)

The submodule ships ip65's sources; the blob link wants .lib archives that
only make ip65-libs produces. Lane G saw an earlier variant of the same wall
with submodules uninitialised (Cannot open include file '../ip65/inc/common.inc').

Recovery, verified here end to end:

$ make ip65-libs && make ip65-blob # rc=0, rc=0
$ ls -l ip65-build/ip65-c64.bin # 6951 bytes
$ make # rc=0 -> build/c64-https.prg, 47,105 B

47,105 B matches the audit's ip65 baseline PRG. The blob is byte-identical
across three independent worktrees rebuilt today — Lane C's, Lane G's and
mine — at sha256
cf1a5ff7809af4e4655e385b378b936054f41046ff2b7604828af3240c2d90dd, and the
2026-05-06 copies preserved in three repo snapshots hash the same value (I
hashed those directly). Four agreeing artifacts spanning three months: the
rebuild path is deterministic, so nobody needs a committed copy — they need the
one-time step named. A stale blob is not a failure mode worth designing around;
a missing one is.

What changed

  • Build gains a "first build in a fresh clone or worktree" block with the
    working order (git submodule update --init --recursivemake ip65-libs
    make), states that the blob is a gitignored artifact, quotes the exact
    ld65 error so a search lands here, and explains what "normally reused"
    actually meant: make clean is rm -rf build, so a once-built blob survives
    and is never rebuilt. That persistence — not a committed file — is why the
    rebuild targets were invisible for so long.
  • make ip65-libs / make ip65-blob descriptions corrected.
  • The Makefile's own comments carried the same false claim in two places
    ("the prebuilt blob is committed to ip65-build/", "The resulting file is
    committed to the repo"). That is where CLAUDE.md was copying it from, so both
    are fixed; leaving them would have re-seeded the error.
  • The VICE ip65 rig's prerequisite list ("two prerequisites that are easy to
    lose") gains the blob step — it is the one rig that needs an ip65 PRG.
  • While in the section: the C64_SKIP_BUILD claim said 7 scripts honor the
    variable and pointed at a commit for the list. It is 14 (13 under tools/,
    plus tests/test_vice_https_macos.py), so the number is replaced with the
    grep that regenerates the list — a count in prose is a claim that decays.

Third commit: a mixed-binary hazard found while verifying this PR

Lane G's cross-PR matrix asserts every remediation branch is PRG-neutral. I went
to confirm that for my own branches rather than accept it, and my first build
disagreed: 47,105 B, but d483d46f… against their db311110…. make clean && make in the same tree then reproduced their hash exactly.

The first build was mixed. I had built BACKEND=uci earlier in the session;
the later plain make found every object up to date by timestamp and ran
ld65 alone:

ld65 -C cfg/c64-https-ip65.cfg … build/tls13.o … build/net/ip65/net.o …

That tls13.o was compiled with -I src/net/uci. BACKEND= is not a -D
flag — it selects the include path, and src/tls13.s does
.include "net_tuning.inc". So the ip65 PRG carried UCI's
post-ServerHello drain budget, NET_SH_DRAIN 1x16 instead of 8x250: issue
#73's regression, reintroduced by build order rather than by a code change, in
the one backend where that drain is load-bearing (ip65 sends no MSS option and
ACKs only when polled, so the budget is what keeps an impatient peer from
RSTing during the multi-minute crypto stall).

Same size, different bytes, no warning. CLAUDE.md stated the make clean rule
only in the Packaging section and only about flags; commit 3 states it at the
top of Variables, where someone about to type make BACKEND=… will read it,
with both hashes as evidence.

Fourth commit: a second mode, found trying to reproduce the first

Lane G reproduced the mixed link deliberately from a third worktree, so I went
back to reproduce it cleanly here rather than rest on an accidental sighting.
The deliberate run failed a different way:

$ make clean && make BACKEND=uci && make
… 4 ca65 lines for the ip65 net objects, no ld65 line, exit 0
$ ls -l build/c64-https.prg
62977 bytes # still the UCI image
$ make
make: Nothing to be done for `all'.

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
(1786622893.195373 vs …893.156535), and make's own -d output says
Prerequisite 'build/net/ip65/net.o' is older than target 'build/c64-https.prg'. So it skips the link and leaves the wrong backend's
PRG in place.

A BACKEND switch without clean therefore yields either a mixed PRG at
exactly the right size, or no rebuild at all and the other backend's PRG
entirely — which one depends on whether the compile crosses a second boundary.
Size catches the second, hash catches both, exit code catches neither.

Fifth commit: why PRG hashes are checkable and object hashes are not

Object hashes were floated as a proof chain for the trap above. They cannot
serve. My first explanation for that was wrong too — I said --debug-info
embeds source paths, making .o hashes worktree-local — and commit 5 corrects
it with a measurement rather than a second guess:

  • strings build/tls13.o contains only relative paths (src/tls13.s,
    src/net/ip65/net_tuning.inc) and zero absolute ones. Worktrees would
    agree.
  • ca65 stamps the build's wall-clock time into the object header.
  • So touch src/tls13.s && make changes the object hash in the same worktree —
    6cd91553…cfef8378…, four bytes differing. Nobody can reproduce their
    own object hash twice.
  • ld65 does not propagate it. build/c64-https.prg stayed at db31111031e2…
    across every one of those rebuilds.

That asymmetry is the reason PRG-hash comparison works at all, and every
neutrality check in this remediation quietly depended on it. It is now stated
in the Build section rather than assumed.

Commit 6 removes the byte offset commit 5 had quoted, and the reason it was
worth removing turned out to be better than the reason I gave. Three of us read
three different offsets out of this effect. The resolution, measured:

  • 0-based 100 is a ca65 variable-length integer (7-bit groups, LSB first,
    continuation in bit 7) decoding to date +%s of the build exactly — 5 bytes,
    delta 0. Lane G decoded this; I confirmed it in my tree.
  • 0-based 107 is a LE32 source-file mtime, not a build stamp. It matched
    the build time in my earlier samples only because my method was
    touch src/tls13.s && make, which set the source mtime to the build second.
    My own test manufactured the coincidence it then reported.
  • A plain make clean && make with no touch changes two bytes, both in
    the varlen field; the count varies with how many 7-bit groups roll over
    between builds, which is why one observer saw 1 byte and I saw 4.

Verified here: LE32@107 == src mtime (true) == build time (false);
varlen@100 == build time (delta 0). Confirmed independently in a second tree,
where LE32@107 likewise equals that tree's source mtime and stays constant
across builds. So the doc quotes no offset and gives the reproduction —
make clean && make twice and compare — which holds whatever the encoding. The conclusion is untouched by all of it: tls13.o had a
different hash on every one of ~8 builds today; c64-https.prg was
db31111031e2… on every one.

Blast radius

Anyone building BACKEND=ip65 from a clean checkout, including the
hardware-free VICE ip65 rig. BACKEND=uci is unaffected (no blob). Cost is
already measured, not hypothetical: two lanes in this session lost time to it,
and one propagated a wrong workaround before it was corrected.

Not fixed here, flagged for whoever next passes through: src/constants.asm
and src/entropy.asm are tracked but dead — the build globs src/*.s — and
tools/test_http.py:87 still explains its expectations in terms of "what ACME
actually emitted". Neither misleads the way this one did, so they did not seem
worth widening the diff for.

JC-000and others added 3 commits August 13, 2026 06:55
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) <noreply@anthropic.com>
… two
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@JC-000

Copy link
Copy Markdown
OwnerAuthor

Reproduced independently by the supervising session — and the hazard is worse than commit 3 documents.

Fresh copy of the tree at f0127a0, no branches applied:

1. make clean && make BACKEND=uci -> 62,977 B (uci)
2. make (no clean) -> 62,977 B sha 2f43b6f3201a... <- still UCI-sized
3. make clean && make -> 47,105 B sha db31111031e2... (correct ip65)

Commit 3 describes a mixed binary — right size, wrong contents, because tls13.o was compiled against -I src/net/uci. What I got at step 2 is the blunter version: the plain make did not produce an ip65 PRG at all. build/c64-https.prg was newer than every object, so the link never re-ran and the previous backend's binary simply stayed in place.

Both failures share the root cause this commit identifies — BACKEND= selects an include path rather than being a -D flag, so make's dependency graph cannot see it — but they fail differently, and the second is easier to miss in one specific way: a size check catches it only if the two backends differ in size. They do here (62,977 vs 47,105), which is luck, not protection.

Worth adding to the note, because it changes what a reader should conclude: after a BACKEND switch, make may rebuild nothing at all. A test invoked with C64_SKIP_BUILD=1, or one that simply trusts make to have done its job, can therefore exercise the other backend's binary entirely. That is the same failure the audit's F4 finding is about — a check that cannot tell which backend it is looking at — arriving by a different route.

This also retroactively justifies the make clean discipline used for every build hash quoted across these ten PRs. The PRG-neutrality result stands precisely because those builds were clean; it is a property of the source trees, not of whatever happens to be in build/, exactly as this commit says.

JC-000and others added 3 commits August 13, 2026 07:10
…ntirely
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@JC-000
JC-000 merged commit c0bbf84 into docs/benchmark-refreshAug 13, 2026
@JC-000
JC-000 deleted the docs/build-section-fresh-clone branch August 13, 2026 13:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@JC-000